Fonction bash avec état

16

Je voudrais implémenter une fonction dans Bash qui augmente (et renvoie) un nombre à chaque appel. Malheureusement, cela semble non trivial puisque j'appelle la fonction à l'intérieur d'un sous-shell et qu'il ne peut par conséquent pas modifier les variables de son shell parent.

Voici ma tentative:

PS_COUNT=0

ps_count_inc() {
    let PS_COUNT=PS_COUNT+1
    echo $PS_COUNT
}

ps_count_reset() {
    let PS_COUNT=0
}

Cela serait utilisé comme suit (et donc mon besoin d'appeler les fonctions à partir d'un sous-shell):

PS1='$(ps_count_reset)> '
PS2='$(ps_count_inc)   '

De cette façon, j'aurais une invite multi-lignes numérotée:

> echo 'this
1   is
2   a
3   test'

Mignonne. Mais en raison de la limitation mentionnée ci-dessus ne fonctionne pas.

Une solution qui ne fonctionnerait pas serait d'écrire le nombre dans un fichier au lieu d'une variable. Cependant, cela créerait un conflit entre plusieurs sessions s'exécutant simultanément. Je pourrais bien sûr ajouter l'ID de processus du shell au nom du fichier. Mais j'espère qu'il y a une meilleure solution qui n'encombrera pas mon système avec beaucoup de fichiers.

Konrad Rudolph
la source
Collisions WRT utilisant un fichier caché voir man 1 mktemp.
goldilocks
Vous devriez voir mon montage - je pense que vous l'aimerez.
mikeserv

Réponses:

14

entrez la description de l'image ici

Pour obtenir la même sortie que vous notez dans votre question, tout ce qui est nécessaire est le suivant:

PS1='${PS2c##*[$((PS2c=0))-9]}- > '
PS2='$((PS2c=PS2c+1)) > '

Vous n'avez pas besoin de se contorsionner. Ces deux lignes feront tout dans n'importe quel shell qui prétend être proche de la compatibilité POSIX.

- > cat <<HD
1 >     line 1
2 >     line $((PS2c-1))
3 > HD
    line 1
    line 2
- > echo $PS2c
0

Mais j'ai aimé ça. Et je voulais démontrer les principes fondamentaux de ce qui rend ce travail un peu meilleur. J'ai donc édité ça un peu. Je l'ai coincé /tmppour l'instant mais je pense que je vais aussi le garder pour moi. C'est ici:

cat /tmp/prompt

ÉCRITURE RAPIDE:

ps1() { IFS=/
    set -- ${PWD%"${last=${PWD##/*/}}"}
    printf "${1+%c/}" "$@" 
    printf "$last > "
}

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'
PS2='$((PS2c=PS2c+1)) > '

Remarque: ayant récemment appris le yash , je l'ai construit hier. Pour une raison quelconque, il n'imprime pas le premier octet de chaque argument avec la %cchaîne - bien que les documents soient spécifiques sur les extensions à caractères larges pour ce format et donc il peut être lié - mais cela fonctionne très bien avec%.1s

Voilà tout. Il y a deux choses principales qui se passent là-haut. Et voici à quoi ça ressemble:

/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 >

PARSING $PWD

Chaque fois qu'il $PS1est évalué, il analyse et imprime $PWDpour s'ajouter à l'invite. Mais je n'aime pas que tout le monde $PWDencombre mon écran, donc je veux juste la première lettre de chaque fil d'Ariane dans le chemin actuel vers le répertoire actuel, que j'aimerais voir en entier. Comme ça:

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cd /
/ > cd ~
/h/mikeserv > 

Il y a quelques étapes ici:

IFS=/

nous allons devoir diviser le courant $PWDet le moyen le plus fiable de le faire est de le $IFSdiviser /. Plus besoin de s'en préoccuper par la suite - tout fractionnement à partir d'ici sera défini par le $@tableau de paramètres positionnels du shell dans la commande suivante, comme:

set -- ${PWD%"${last=${PWD##/*/}}"}

Donc celui-ci est un peu délicat, mais l'essentiel est que nous nous séparons $PWDsur les /symboles. J'utilise également l'expansion des paramètres pour $lasttout affecter après toute valeur se produisant entre la /barre oblique la plus à gauche et la plus à droite . De cette façon, je sais que si je suis juste au point /et que je n'en ai qu'un, /il $lastsera toujours égal à l'ensemble $PWDet $1sera vide. C'est important. Je me déshabille également $lastde l'extrémité arrière $PWDavant de l'attribuer $@.

printf "${1+%c/}" "$@"

Donc, ici - tant que ${1+is set}nous sommes printfle premier caractère %cde chacun des arguments de notre shell - que nous venons de définir pour chaque répertoire de notre répertoire actuel $PWD- moins le répertoire supérieur - divisé /. Nous n'imprimons donc essentiellement que le premier caractère de chaque répertoire, $PWDmais le premier. Il est cependant important de réaliser que cela ne se produit que si la valeur $1est définie, ce qui ne se produira pas à la racine /ou à une suppression de /comme dans /etc.

printf "$last > "

$lastest la variable que je viens d'affecter à notre répertoire principal. Alors maintenant, c'est notre meilleur répertoire. Il affiche si oui ou non la dernière instruction a fait. Et il en faut un peu >pour faire bonne mesure.

MAIS QU'EN EST-IL DE L'AUGMENTATION?

Et puis il y a la question du $PS2conditionnel. J'ai montré plus tôt comment cela peut être fait, que vous pouvez toujours trouver ci-dessous - c'est fondamentalement une question de portée. Mais il y a un peu plus à moins que vous ne vouliez commencer à faire un tas d' printf \backspaces et ensuite essayer d'équilibrer leur nombre de personnages ... ugh. Alors je fais ça:

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'

Encore une fois, ${parameter##expansion}sauve la mise. C'est un peu étrange ici cependant - nous définissons en fait la variable pendant que nous la supprimons d'elle-même. Nous utilisons sa nouvelle valeur - définie à mi-bande - comme le globe à partir duquel nous nous retirons. Tu vois? Nous ##*supprimons tout de la tête de notre variable d'incrémentation au dernier caractère qui peut être n'importe quoi [$((PS2c=0))-9]. Nous sommes ainsi garantis de ne pas afficher la valeur, et pourtant nous l'attribuons toujours. C'est plutôt cool - je n'ai jamais fait ça avant. Mais POSIX nous garantit également que c'est la manière la plus portable de procéder.

Et c'est grâce à POSIX-spécifié ${parameter} $((expansion))qui conserve ces définitions dans le shell actuel sans exiger que nous les définissions dans un sous-shell séparé, quel que soit l'endroit où nous les évaluons. Et c'est pourquoi cela fonctionne dans dashet shaussi bien que dans bashet zsh. Nous n'utilisons aucun échappement dépendant du shell / terminal et nous laissons les variables se tester. C'est ce qui rend le code portable rapide.

Le reste est assez simple - il suffit d'incrémenter notre compteur à chaque fois qu'il $PS2est évalué jusqu'à ce qu'il soit $PS1à nouveau réinitialisé. Comme ça:

PS2='$((PS2c=PS2c+1)) > '

Alors maintenant, je peux:

DASH DEMO

ENV=/tmp/prompt dash -i

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 > printf '\t%s\n' "$PS1" "$PS2" "$PS2c"
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
    0
/u/s/m/man3 > cd ~
/h/mikeserv >

SH DEMO

Cela fonctionne de la même manière dans bashou sh:

ENV=/tmp/prompt sh -i

/h/mikeserv > cat <<HEREDOC
1 >     $( echo $PS2c )
2 >     $( echo $PS1 )
3 >     $( echo $PS2 )
4 > HEREDOC
    4
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
/h/mikeserv > echo $PS2c ; cd /
0
/ > cd /usr/share
/u/share > cd ~
/h/mikeserv > exit

Comme je l'ai dit ci-dessus, le principal problème est que vous devez considérer où vous effectuez votre calcul. Vous n'obtenez pas l'état dans le shell parent - vous n'y calculez donc pas. Vous obtenez l'état dans le sous-shell - c'est donc là que vous calculez. Mais vous faites la définition dans le shell parent.

ENV=/dev/fd/3 sh -i  3<<\PROMPT
    ps1() { printf '$((PS2c=0)) > ' ; }
    ps2() { printf '$((PS2c=PS2c+1)) > ' ; }
    PS1=$(ps1)
    PS2=$(ps2)
PROMPT

0 > cat <<MULTI_LINE
1 > $(echo this will be line 1)
2 > $(echo and this line 2)
3 > $(echo here is line 3)
4 > MULTI_LINE
this will be line 1
and this line 2
here is line 3
0 >
mikeserv
la source
1
@mikeserv Nous tournons en rond. Je sais tout ça. Mais comment dois-je utiliser cela dans ma définition de PS2? Ceci est la partie délicate. Je ne pense pas que votre solution puisse être appliquée ici. Si vous pensez le contraire, montrez-moi comment.
Konrad Rudolph
1
@mikeserv Non, ce n'est pas lié, désolé. Voir ma question pour plus de détails. PS1et PS2sont des variables spéciales dans le shell qui sont imprimées en tant qu'invite de commande (essayez-le en définissant PS1une valeur différente dans une nouvelle fenêtre de shell), elles sont donc utilisées très différemment de votre code. Voici quelques informations supplémentaires sur leur utilisation: linuxconfig.org/bash-prompt-basics
Konrad Rudolph
1
@KonradRudolph qu'est-ce qui vous empêche de les définir deux fois? C'est ce qu'a fait mon truc original ... Je dois regarder votre réponse ... Cela se fait tout le temps.
mikeserv
1
@mikeserv Tapez echo 'thisà l'invite, puis expliquez comment mettre à jour la valeur de PS2avant de taper le guillemet simple de fermeture.
chepner
1
D'accord, cette réponse est maintenant officiellement incroyable. J'aime aussi le fil d'Ariane, bien que je ne l'adopterai pas car j'imprime de
Konrad Rudolph
8

Avec cette approche (fonction s'exécutant dans un sous-shell), vous ne pourrez pas mettre à jour l'état du processus de shell principal sans passer par des contorsions. Au lieu de cela, organisez l'exécution de la fonction dans le processus maître.

La valeur de la PROMPT_COMMANDvariable est interprétée comme une commande qui est exécutée avant l'impression de l' PS1invite.

Car PS2il n'y a rien de comparable. Mais vous pouvez utiliser une astuce à la place: puisque tout ce que vous voulez faire est une opération arithmétique, vous pouvez utiliser l'expansion arithmétique, qui n'implique pas de sous-shell.

PROMPT_COMMAND='PS_COUNT=0'
PS2='$((++PS_COUNT))  '

Le résultat du calcul arithmétique aboutit à l'invite. Si vous souhaitez le masquer, vous pouvez le passer comme un indice de tableau qui n'existe pas.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '
Gilles 'SO- arrête d'être méchant'
la source
4

C'est un peu intensif en E / S, mais vous devrez utiliser un fichier temporaire pour contenir la valeur du nombre.

ps_count_inc () {
   read ps_count < ~/.prompt_num
   echo $((++ps_count)) | tee ~/.prompt_num
}

ps_count_reset () {
   echo 0 > ~/.prompt_num
}

Si vous avez besoin d'un fichier séparé par session shell (ce qui semble être une préoccupation mineure; allez-vous vraiment taper des commandes multi-lignes dans deux shells différents en même temps?), Vous devez utiliser mktemppour créer un nouveau fichier pour chaque utilisation.

ps_count_reset () {
    rm -f "$prompt_count"
    prompt_count=$(mktemp)
    echo 0 > "$prompt_count"
}

ps_count_inc () {
    read ps_count < "$prompt_count"
    echo $((++ps_count)) | tee "$prompt_count"
}
chepner
la source
+1 L'E / S n'est probablement pas très importante car si le fichier est petit et fréquemment utilisé, il sera mis en cache, c'est-à-dire qu'il fonctionne essentiellement comme une mémoire partagée.
goldilocks
1

Vous ne pouvez pas utiliser une variable shell de cette façon et vous comprenez déjà pourquoi. Un sous-shell hérite des variables exactement de la même manière qu'un processus hérite de son environnement: toutes les modifications apportées s'appliquent uniquement à lui et à ses enfants et non à un processus ancêtre.

Selon d'autres réponses, la chose la plus simple à faire est de ranger ces données dans un fichier.

echo $count > file
count=$(<file)

Etc.

boucle d'or
la source
Bien sûr, vous pouvez définir une variable de cette façon. Vous n'avez pas besoin d'un fichier temporaire. Vous définissez la variable dans le sous-shell et imprimez sa valeur dans le shell parent où vous absorbez cette valeur. Vous obtenez tout l'état dont vous avez besoin pour calculer sa valeur dans le sous-shell, c'est donc là que vous le faites.
mikeserv
1
@mikeserv Ce n'est pas la même chose, c'est pourquoi le PO a déclaré qu'une telle solution ne fonctionnerait pas (bien que cela aurait dû être précisé dans la question). Ce à quoi vous faites référence, c'est de passer une valeur à un autre processus via IPC afin qu'il puisse attribuer cette valeur à n'importe quoi. Ce que l'OP voulait / devait faire était d'affecter la valeur d'une variable globale partagée par un certain nombre de processus, et vous ne pouvez pas le faire via l'environnement; ce n'est pas très utile pour IPC.
goldilocks
Mec, soit j'ai complètement mal compris ce qui est nécessaire ici, soit tout le monde l'a fait. Cela me semble vraiment simple. Vous voyez mon montage? Qu'est ce qui ne va pas avec ça?
mikeserv
@mikeserv Je ne pense pas que vous ayez mal compris et pour être juste, ce que vous avez est une forme d'IPC et pourrait fonctionner. Je ne comprends pas pourquoi Konrad ne l'aime pas, mais s'il n'est pas assez flexible, le fichier caché est assez simple (et il en va de même pour éviter les collisions, par exemple mktemp).
goldilocks
2
@mikeserv La fonction voulue est appelée lorsque la valeur de PS2est développée par le shell. Vous n'avez pas la possibilité de mettre à jour la valeur d'une variable dans le shell parent à ce moment.
chepner
0

Pour référence, voici ma solution en utilisant des fichiers temporaires, qui sont uniques par processus shell, et supprimés dès que possible (pour éviter l'encombrement, comme mentionné dans la question):

# Yes, I actually need this to work across my systems. :-/
_mktemp() {
    local tmpfile="${TMPDIR-/tmp}/psfile-$$.XXX"
    local bin="$(command -v mktemp || echo echo)"
    local file="$($bin "$tmpfile")"
    rm -f "$file"
    echo "$file"
}

PS_COUNT_FILE="$(_mktemp)"

ps_count_inc() {
    local PS_COUNT
    if [[ -f "$PS_COUNT_FILE" ]]; then
        let PS_COUNT=$(<"$PS_COUNT_FILE")+1
    else
        PS_COUNT=1
    fi

    echo $PS_COUNT | tee "$PS_COUNT_FILE"
}

ps_count_reset() {
    rm -f "$PS_COUNT_FILE"
}
Konrad Rudolph
la source