Une place pour une véritable innovation. Partagez vos propres utilitaires créés avec la communauté Manjaro.
Questions et discussions sur la programmation et le codage.
Répondre

prompt bash avec son programme

#1Messageil y a 2 ans

:bjr: bonjour

Suite à cette discussion, un écrit m'a fait tilt
Smurf a écrit : il y a 2 ans Il n'y a aucun intérêt d'utiliser ces "usines à gaz" ...
En fait, si ces programmes sont des usines à gaz, c'est du fait de la conception du développeur et non d'un besoin réel.
Ce type de programme ne fait QUE générer une chaine qui va décrire notre prompt
echo $PS1 nous donne cette chaine qui peut-être par exemple \[\033[01;32m\][\u@\h\[\033[01;37m\] \w\[\033[01;32m\]]\$\[\033[00m\]

Par simplicité, ce script prend une chaine du même type en paramètre et va la transformer pour en faire un prompt personnalisé.
Ici, le programme ne fait que récupérer des sous-chaines (balises), les remplacer par des valeurs et retourner cette même chaine modifiée. Donc, même pas une usine à gaz à coder.

Pour la démo, existe 3 types de balises "perso" (toute la syntaxe PS1 reste valide !)
- couleurs au format rvb :
{#ff0000} mettre une couleur au texte
{#0} annuler la couleur

- variables (environnement et certaines internes au programme)
{$USER} utilisateur
{$ARCH} architecture
{$HEURE} heure:minutes
...

- commandes shell diverses, par exemple :
{ls -1 | wc -l} nombre de fichiers visibles dans le répertoire courant
{ls -1a | wc -l} nombre de fichiers total dans le répertoire courant

Nous avons donc un programme simple pour l'utilisateur (pas plus compliqué que l'existant) et simple au niveau programmation :
1) récupérer la chaine passée en premier paramètre
2) récupérer les balises de cette chaine
3) interpréter ces balises
4) remplacer les balises par la valeur "interprétée"
5) retourner la nouvelle chaine

-------------------

Réalisation

ps: ici, le programme est écrit en python :o C'est clairement une mauvaise idée puisque python est lent et le charger à chaque affichage du prompt ne va pas arranger les choses. Attention, cette lenteur est très relative et ne devrait pas être visible pour un utilisateur.

Version très simplifiée avec uniquement les couleurs :

# conversion de couleur #rrvvbb en code couleur pour console
def rgb_fg(r: int, g: int, b: int) -> str:
    return '\x1b[38;2;' + str(r) + ';' + str(g) + ';' + str(b) + 'm'

def html_color(html : str) -> str:
    return rgb_fg(int('0x'+html[0:2], 0), int('0x'+html[2:4], 0), int('0x'+html[4:6], 0))

# on récupère la chaine passée en paramètre
ARG = sys.argv[1]

# on cherche les balises couleurs et les converties
COLORS = {"{#0}" : '\x1b[0m'}   # code remise à zéro
for match in re.finditer(r'{#[a-fA-F0-9]{6}}', ARG, flags=0):
    COLORS[match.group()] = html_color(match.group()[2:-1])

# on remplace ces balises couleur trouvées à la ligne précédente, par le code compréhensible par notre console
for section in COLORS:
    ARG = ARG.replace( section, COLORS[section])

# et au final on ressort notre toute nouvelle chaine
print(f"{ARG}{COLORS['{#0}']}")
Voilà, notre programme est fini :pompom: on peut lui passer une chaine du type

{#ff0000}\u{#0}@\h {#aacc00} \w  \n$ 
--------------

Reste à faire le même chose pour les variables d'environnement et pour les commandes. Mais en fait, c'est exactement la même chose.
À noter que ici, les commandes shell vont être lancées en parallèle mais, c'est juste histoire de donner un peu de crédibilité à ce type de programme en python.

----------------

Tester / Configurer
Puisque ici, le but est la simplicité, pour tester notre futur beau prompt, il suffit de lancer notre script avec divers paramètres.

./prompter '{$USER} {#ff0000} {$PWD} \n $' -v
./prompter '{$USER}@{$MACHINE} {#ff0000} {$PWD} \n{$HEURE} $' -v
...
L'option -v (verbose) va afficher un log sur le fonctionnement de cette application.

----------------

Installer ce programme ?
ps: le script ici, propose une option -i qui donne la marche à suivre

# dans ~/.bashrc

function _update_ps1() {
    PS1="$(prompter -pbash -e$? 2>/dev/null)"
}
PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND"
Il faut juste adapter la chaine passée au script et éventuellement ajouter le bon chemin pour appeler le script

# Avec fish :

function fish_prompt
    set -l last_pipestatus $pipestatus
    eval prompter -pfish -e$last_pipestatus 2>/dev/null
end

Code du programme "prompter"
Attention, ici le prompt par défaut, si nous ne passons pas de paramètre, est chargé et sur 5 lignes :mrgreen:

#!/usr/bin/python3
import os
import sys
from datetime import datetime
import re
import platform
import socket
from pathlib import Path
import subprocess
import concurrent.futures
try:
    import psutil
except ModuleNotFoundError:
    print("il manque un paquet: pacman -S python-psutil --asdeps")
    exit(8)

# prompt par défaut
ARG = r'''\n➖{#779977}{$USER}{#777777}@{$MACHINE}:{$IP} Manjaro {$release}
💿{$RAM used}/{$RAM} 🏍 {$CPU freq}  {$BATTERY}
🌡 {$TEMP} - {$TEMP nouveau}
{#779977}{$PWD}{#777777}{$GIT} ({$PWD count}) - {$DISK free} {$DISK writed}
{$HEURE}{#aa0000}{$ERROR}{#779977}{#f} 🟢 '''
version = "0.3"

def log(*args):
    if VERBOSE:
        print(*args, file=sys.stderr)


def rgb_fg(r: int, g: int, b: int) -> str:
    return '\x1b[38;2;' + str(r) + ';' + str(g) + ';' + str(b) + 'm'


def html_color(html : str) -> str:
    return rgb_fg(int('0x'+html[0:2], 0), int('0x'+html[2:4], 0), int('0x'+html[4:6], 0))


def run_command(index: str):
    output = ""
    try:
        proc = subprocess.run(index[1:-1],
            shell=True,
            text=True,
            capture_output=True,
            check=True,
            )
        output = proc.stdout.rstrip()
    except subprocess.CalledProcessError:
        pass
    return index, output

VERBOSE = True if "-v" in sys.argv else False

if "-i" in sys.argv:
    print(""" # dans ~/.bashrc
        function _update_ps1() {
            PS1="$(prompter -e$? -pbash 2>/dev/null)"
        }
        PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND"
 # avec fish:
        function fish_prompt
            set -l last_pipestatus $pipestatus
            eval prompter -pfish -e$last_pipestatus 2>/dev/null
        end
        """)
    exit(0)

if "-h" in sys.argv:
    print("""
{#ff0000} mettre une couleur au texte
{#0} annuler la couleur

# variables environnement et particulières
{$ARCH} architecture
{$BATTERY} % de la batterie
{$CPU freq} fréquence actuelle des cpu
{$DISK free} Espace disponible sur /
{$DISK writed} Ecritures depuis le boot sur tous les disques
{$GIT} branche active
{$HEURE} heure:minutes
{$IP} ip locale de la machine
[$PARAM] libre parametre -xXXX passé au script (ex: -pBASH...-pZSH)
{$RAM} Mémoire totale
{$RAM used} mémoire utilisée
{$TEMP nouveau} température de la carte "nouveau"
{$TEMP} température des cpu
{$USER} utilisateur

# commandes shell
{ls -1 | wc -l} exemple: nombre de fichiers
        """)
    exit(0)

try:
    log("read prompt in ~/.config/prompt.conf")
    ARG = Path.home().joinpath('.config/prompt.conf').read_text()
except FileNotFoundError:
    pass

try:
    ARG = [x for x in sys.argv[1:] if x[0] != "-"][0]
except IndexError:
    pass

VARS_ENV = {}
COLORS = {"{#0}": '\x1b[0m', "{#f}": ""}
COMMANDS = {}


try:

    log(ARG)
    # variables
    log("Vars :")
    for tag in (match.group() for match in re.finditer(r'{\$[|a-zA-Z0-9_ ]*}', ARG)):
        #log('match: ', tag)
        output = ""
        match_str = tag[2:-1].split()
        match_str[0] = match_str[0].upper()
        match match_str:
            case ["ARCH"]:
                output = platform.uname().machine
            case ["BATTERY"]:
                bat = psutil.sensors_battery()
                if bat:
                    output = f"{bat.percent:.0f}%"
            case ["CPU", "freq"]:
                output = f"{psutil.cpu_freq().current:.2f}Mhz"
            case ["CPU"] | ["TEMP"]:
                # pas possible % des cpu car il faut une tempo de 1 seconde mini
                # output = f"{psutil.cpu_percent(interval=1, percpu=False):0.1f}%"
                if VERBOSE:
                    log("psutil.sensors_temperatures()", psutil.sensors_temperatures())
                temps = [f"{x.current:.0f}" for x in psutil.sensors_temperatures()['coretemp']]
                output = f"{'/'.join(temps)}°"
            case ["DISK", ask]:
                disk_partitions = psutil.disk_partitions()
                for partition in disk_partitions:
                    if partition.mountpoint != "/":
                        continue
                    match ask.lower():
                        case "free":
                            disk_usage = psutil.disk_usage("/")
                            output = f"{disk_usage.free / 1024 ** 3:0.1f}Go"
                        case "writed":
                            disk_rw = psutil.disk_io_counters()
                            output = f"{disk_rw.write_bytes / 1024 ** 3:0.1f}Go"
                    break
            case ["ERROR"]:
                try:
                    output = [x[2:] for x in sys.argv[1:] if x[0:2] == "-e"][0]
                    if output == "0":
                        output = ""
                    else:
                        COLORS["{#f}"] = '\033[0;31m'
                except IndexError:
                    output = ""
            case ["FREE", "used"] | ["RAM", "used"]:
                output = f"{psutil.virtual_memory().used / 1024 ** 3:0.1f}Go"
                if VERBOSE:
                    log("psutil.virtual_memory()", psutil.virtual_memory())
            case ["GIT"]:
                deep = 0
                for parent in Path.cwd().joinpath('None').parents:
                    if parent.joinpath('.git').exists():
                        proc = subprocess.run("git rev-parse --abbrev-ref HEAD",
                            shell=True, text=True, capture_output=True, check=False)
                        output = f"[{'../' * deep if deep > 0 else ''}{proc.stdout.strip()}]"
                        break
                    deep += 1
            case ["GPU"]:
                output = f"{psutil.sensors_temperatures()['nouveau'][0].current:0.0f}°"
            case ["HEURE"]:
                now_date = datetime.now()
                output = f"{now_date.hour:02d}:{now_date.minute:02d}"
            case ["HOSTNAME"]:
                output = socket.gethostname()
            case ["IP"]:
                output = socket.gethostbyname(socket.gethostname())
            case ["PARAM"]:
                try:
                    output = [x[2:] for x in sys.argv[1:] if x[0:2] == "-p"][0]
                except IndexError:
                    pass
            case ["PWD"]:
                pwd = str(Path.cwd())
                home = str(Path.home())
                if pwd.startswith(home):
                    pwd = f"~{pwd[len(home):]}"
                output = pwd
            case ['PWD', "count"]:
                output = len([True for x in Path.cwd().iterdir()])
            case ["RAM"] | ["FREE"]:
                output = str(round(psutil.virtual_memory().total / (1024.0 ** 3)))+"Go"
            case ["RELEASE"]:
                output = platform.uname().release
            case ["TEMP", name]:
                temps = [f"{x.current:.0f}" for x in psutil.sensors_temperatures()[name]]
                output = f"{'/'.join(temps)}°"
            case ["UNAME"] | ["MACHINE"]:
                output = platform.uname().node
            case _:
                output = os.getenv(match_str[0], "")
        VARS_ENV[tag] = str(output)
    log(VARS_ENV)
    log()


    # commandes
    log("Commands :")
    futures = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=12) as executor:
        for tag in (match.group() for match in re.finditer(r'{[^\$^\#][^}]+}', ARG)):
            COMMANDS[tag] = ""
            futures.append(executor.submit(run_command, tag))
            log('match: ', tag)
        log("A exécuter en //:", COMMANDS)
        for future in concurrent.futures.as_completed(futures):
            try:
                tag, value = future.result()
                COMMANDS[tag] = value
            except Exception as e:
                pass
    log("Résultats des commandes shell", COMMANDS)
    log()


    # couleurs
    log("colors :")
    for tag in (match.group() for match in re.finditer(r'{#[a-fA-F0-9]{6}}', ARG)):
        #log('match: ', tag)
        COLORS[tag] = html_color(tag[2:-1])
    log(COLORS)
    log()

    for section in VARS_ENV:
        ARG = ARG.replace( section, VARS_ENV[section])
    for section in COMMANDS:
        ARG = ARG.replace( section, COMMANDS[section])
    for section in COLORS:
        ARG = ARG.replace( section, COLORS[section])
finally:
    print(f"{ARG}{COLORS['{#0}']}", end="")

prompt bash avec son programme

#2Messageil y a 2 ans

papajoke a écrit : il y a 2 ans Suite à cette discussion, un écrit m'a fait tilt
Smurf a écrit : il y a 2 ans Il n'y a aucun intérêt d'utiliser ces "usines à gaz" ...
En fait, si ces programmes sont des usines à gaz, c'est du fait de la conception du développeur et non d'un besoin réel.
C'est l'inverse selon moi, les besoins du développeur sont tels que la conception est complexe.

prompt bash avec son programme

#3Messageil y a 2 ans

Interressant. :clap
Bien vu l'integration de psutil pour obtenir le cpu, la ram, le ssd, battery, temp, etc... Je suis en plein dedans. A noter pour l'utiliser, il faut au préalable installer psutils et python-psutils. :siffle

Qu'est-ce que PS1 ?

Je ne l'ai pas encore tester sans ni avec arguments mais :
Est-ce moi qui dis une boulette mais dans la fonction def html_color avec un str tu demandes un str hors dans return tu as un integer ? Si tu obtiens un integer dans ce cas, ne faudrait il pas mettre int après str au lieu de str ? :roll:

prompt bash avec son programme

#4Messageil y a 2 ans

bonjour

Je viens de poster la version 0.3
- affichage des erreurs
- intégration avec fish
- prompt par défaut peut-être dans ~/.config/prompt.conf
Cenwen a écrit : il y a 2 ansQu'est-ce que PS1 ?
c'est une chaine que va justement interpréter le shell pour afficher le prompt - en gros il fait la même chose que mon script (remplace \w par $PWD,...) et ensuite, en plus, un echo -e de cette variable
dans la fonction def html_color avec un str tu demandes un str hors dans return tu as un integer ? Si tu obtiens un integer dans ce cas, ne faudrait il pas mettre int après str au lieu de str ?
pas tout compris ...
def html_color(html : str) -> str
Je lui passe ce que nous avons écrit : "#ff99cc" c'est bien une chaine
def rgb_fg(r: int, g: int, b: int) -> str:
je lui passe 3 découpages de la chaine passée à html_color()
par exemple pour le vert : int('0x'+html[2:4], 0) , 99 ici avec "#ff99cc"

rgb_fg() retourne une chaine, et html_color() retourne uniquement rgb_fg() donc une chaine
ps: pas d'intérêt d'avoir ici 2 fonctions, c'était de la récup d'un ancien code avec rgb_fg() et rgb_bg()

Bien vu l'integration de psutil
C'est un peu le but du truc, montrer que l'on peut ensuite intégrer x bibliothèques en fonction de nos besoins (et le truc simple devient une usine à gaz :rigole:;rale: )
il faut au préalable installer psutils et python-psutils
Merci, uniquement python-psutils à installer

try:
    import psutil
except ModuleNotFoundError:
    print("il manque un paquet: pacman -S python-psutil --asdeps")
    exit(8)

-----------------


Mon fichier ~/.config/prompt.conf

{#779977}╭ {#779977}{$USER}{#555555}@{$MACHINE}:{$IP} {$release} {#779977}{$PARAM}
{#779977}│ {#555555}💿{$RAM used}/{$RAM}    🏍 {$CPU freq}    🌡 {$TEMP} - {$TEMP nouveau}
{#779977}│ {#779977}{$PWD}{#666666}{$GIT}{#555555} ({$PWD count}) - {$DISK free} {#444444}(w:{$DISK writed})
{#779977}╰ {#555555}{$HEURE}{#33aa33} {#aa0000}{$ERROR}{#779977}{#f}🟢 
Toujours chargé à max

Résultat sous fish et sous bash
Image
time prompter me retourne sur un i3, un peu moins de 0.1 seconde, ce qui est bien pour python et vu que j'utilise toutes les balises (sauf batterie)
Si je devais retourner toutes ces infos en bash sans ce script , il est clair que cela prendrait plus de temps. Attention, nous n'avons pas besoin de toutes ces infos, juste, à un moment donné, une application de prompt est plus rapide.

prompt bash avec son programme

#5Messageil y a 2 ans

Merci, uniquement python-psutils à installer
En fait, je me suis mal exprimé car c'est python-psutil qui necessite psutil.Malheureusment, je n'ai pas pu vérifier sous kde car ils sont déjà instllés. De mémoire, sous Cinnamon, quand j'avais commencé à faire mumuse avec python-psutil, j'avais été obligé d'installer (à l'époque) psutil.

prompt bash avec son programme

#6Messageil y a 2 ans

Existe psutils dans les dépots et est une application perl, donc rien à voir malgré le nom : ps ici pour PostScript
https://github.com/rrthomas/psutils
Utilities for manipulating PostScript documents
python-psutil sera installé avec ces dépendances comme tout paquet (et il ne nécessite que python)
Répondre