Funzione bash stateful


16

Vorrei implementare una funzione in Bash che aumenta (e restituisce) un conteggio ad ogni chiamata. Sfortunatamente questo sembra non banale poiché sto invocando la funzione all'interno di una subshell e di conseguenza non può modificare le variabili della shell madre.

Ecco il mio tentativo:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

Questo verrebbe usato come segue (e quindi la mia necessità di invocare le funzioni da una subshell):

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

In questo modo, avrei un prompt numerato su più righe:

> echo 'this
1   is
2   a
3   test'

Carino. Ma a causa della limitazione di cui sopra non funziona.

Una soluzione non funzionante sarebbe quella di scrivere il conteggio in un file anziché in una variabile. Tuttavia, ciò creerebbe un conflitto tra più sessioni contemporaneamente in esecuzione. Potrei aggiungere l'ID di processo della shell al nome del file, ovviamente. Ma spero che ci sia una soluzione migliore che non ingombrerà il mio sistema con molti file.


Vedi le collisioni WRT usando un file stash man 1 mktemp.
Riccioli d'oro,

Dovresti vedere la mia modifica - penso che ti piacerà.
Mikeserv,

Risposte:


14

inserisci qui la descrizione dell'immagine

Per ottenere lo stesso risultato che noti nella tua domanda, tutto ciò che serve è questo:

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

Non è necessario contorcersi. Queste due linee faranno tutto in qualsiasi shell che finge di qualcosa di simile alla compatibilità POSIX.

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

Ma questo mi è piaciuto. E volevo dimostrare i fondamenti di ciò che rende questo lavoro un po 'migliore. Quindi l'ho modificato un po '. L'ho bloccato /tmpper ora, ma penso che lo terrò anche per me stesso. È qui:

cat /tmp/prompt

SCRITTTO PRONTO:

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

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

Nota: dopo aver appreso di yash , l'ho costruito ieri. Per qualsiasi motivo, non stampa il primo byte di ogni argomento con la %cstringa, sebbene i documenti fossero specifici sulle estensioni di caratteri generici per quel formato e quindi potrebbe essere correlato, ma funziona bene con%.1s

Questo è tutto. Ci sono due cose principali che succedono lassù. E questo è come appare:

/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

Ogni volta che $PS1viene valutato, analizza e stampa $PWDper aggiungere al prompt. Ma non mi piace l'intero $PWDaffollamento del mio schermo, quindi voglio solo la prima lettera di ogni briciola di pane nel percorso corrente fino alla directory corrente, che mi piacerebbe vedere per intero. Come questo:

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

Ci sono alcuni passaggi qui:

IFS=/

dovremo dividere l'attuale $PWDe il modo più affidabile per farlo è con $IFSsplit on /. Non c'è bisogno di preoccuparsene in seguito - tutte le divisioni da qui in avanti saranno definite $@dall'array di parametri posizionali della shell nel comando successivo come:

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

Quindi questo è un po 'complicato, ma la cosa principale è che stiamo splitting $PWDsu /simboli. Uso anche l'espansione dei parametri per assegnare a $lasttutto dopo che si è verificato un valore tra la /barra più a sinistra e quella più a destra . In questo modo so che se io sono solo a /ed avere un solo /allora $lastsarà ancora uguale tutto $PWDe $1sarà vuota. Questo conta. Mi spoglio anche $lastdalla fine della coda $PWDprima di assegnarlo a $@.

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

Quindi qui - fintanto che ${1+is set}siamo printfi primi a %cbattere gli argomenti di ogni nostra shell - che abbiamo appena impostato su ciascuna directory nella nostra attuale $PWD- meno la directory superiore - divisi /. Quindi essenzialmente stiamo solo stampando il primo carattere di ogni directory $PWDma quello in alto. È importante però rendersi conto che ciò accade solo se $1viene impostato affatto, che non accadrà alla radice /o a uno rimosso da /come in /etc.

printf "$last > "

$lastè la variabile che ho appena assegnato alla nostra directory principale. Quindi ora questa è la nostra directory principale. Stampa indipendentemente dall'ultima affermazione. E ci vuole un po 'di pulito >per una buona misura.

MA CHE COSA RIGUARDA L'INCREMENTO?

E poi c'è la questione del $PS2condizionale. Ho mostrato in precedenza come si può fare ciò che è ancora possibile trovare di seguito - questo è fondamentalmente un problema di portata. Ma c'è un po 'di più a meno che tu non voglia iniziare a fare un sacco di printf \bspazi aperti e quindi provare a bilanciare il loro conteggio dei personaggi ... ugh. Quindi faccio questo:

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

Ancora una volta, ${parameter##expansion}salva la giornata. È un po 'strano qui - in realtà impostiamo la variabile mentre la rimuoviamo da sola. Usiamo il suo nuovo valore - set mid-strip - come il glob da cui rimuoviamo. Vedi? Ci ##*spogliamo tutti dal capo della nostra variabile di incremento per l'ultimo carattere che può essere qualsiasi cosa, da [$((PS2c=0))-9]. In questo modo siamo garantiti di non produrre il valore, eppure lo assegniamo ancora. È abbastanza bello - non l'ho mai fatto prima. Ma POSIX ci garantisce anche che questo è il modo più portatile per farlo.

Ed è grazie a POSIX specificato ${parameter} $((expansion))che mantiene queste definizioni nella shell corrente senza richiedere che le impostiamo in una subshell separata, indipendentemente da dove le valutiamo. Ed è per questo che funziona in dashe shproprio come in bashe zsh. Non usiamo escape dipendenti dalla shell / terminali e lasciamo che le variabili si testino da sole. Questo è ciò che rende veloce il codice portatile .

Il resto è abbastanza semplice: basta incrementare il nostro contatore per ogni volta che $PS2viene valutato fino a $PS1quando non lo si ripristina nuovamente. Come questo:

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

Quindi ora posso:

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

Funziona allo stesso modo in basho 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

Come ho detto sopra, il problema principale è che devi considerare dove fai il tuo calcolo. Non si ottiene lo stato nella shell padre, quindi non si calcola lì. Ottieni lo stato nella subshell - quindi è lì che calcoli. Ma fai la definizione nella shell genitore.

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 >

1
@mikeserv Stiamo girando in tondo. So tutto questo. Ma come posso usarlo nella mia definizione di PS2? Questa è la parte difficile. Non credo che la tua soluzione possa essere applicata qui. Se la pensi diversamente, per favore mostrami come.
Konrad Rudolph,

1
@mikeserv No, non è correlato, scusa. Vedi la mia domanda per i dettagli. PS1e PS2sono variabili speciali nella shell che vengono stampate come prompt dei comandi (provalo impostando PS1un valore diverso in una nuova finestra della shell), vengono quindi utilizzate in modo molto diverso dal tuo codice. Ecco alcune ulteriori informazioni sul loro utilizzo: linuxconfig.org/bash-prompt-basics
Konrad Rudolph,

1
@KonradRudolph cosa ti impedisce di definirli due volte? È quello che ha fatto la mia cosa originale ... Devo guardare la tua risposta ... Questo è fatto tutto il tempo.
Mikeserv,

1
@mikeserv Digitare echo 'thisa un prompt, quindi spiegare come aggiornare il valore PS2prima di digitare la virgoletta singola di chiusura.
Chepner,

1
Bene, questa risposta è ora ufficialmente sorprendente. Mi piace anche il pangrattato, anche se non lo adotterò poiché stampo
Konrad Rudolph

8

Con questo approccio (funzione in esecuzione in una subshell) non sarai in grado di aggiornare lo stato del processo della shell principale senza passare attraverso le contorsioni. Invece, organizzare l'esecuzione della funzione nel processo principale.

Il valore della PROMPT_COMMANDvariabile viene interpretato come un comando che viene eseguito prima di stampare il PS1prompt.

Perché PS2non c'è niente di paragonabile. Puoi invece usare un trucco: dal momento che tutto ciò che vuoi fare è un'operazione aritmetica, puoi usare l'espansione aritmetica, che non implica una subshell.

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

Il risultato del calcolo aritmetico finisce nel prompt. Se si desidera nasconderlo, è possibile passarlo come un indice di array che non esiste.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '

4

È un po 'intensivo di I / O, ma dovrai usare un file temporaneo per contenere il valore del conteggio.

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

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

Se sei preoccupato di aver bisogno di un file separato per sessione di shell (che sembra una preoccupazione minore; dovresti davvero digitare comandi multilinea contemporaneamente in due diverse shell?), Dovresti usare mktempper creare un nuovo file per ogni uso.

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"
}

+1 L'I / O non è probabilmente molto significativo poiché se il file è piccolo e viene acceduto frequentemente, verrà memorizzato nella cache, cioè essenzialmente funzionerà come memoria condivisa.
Riccioli d'oro,

1

Non puoi usare una variabile di shell in questo modo e capisci già perché. Una subshell eredita le variabili esattamente allo stesso modo in cui un processo eredita il suo ambiente: qualsiasi modifica apportata si applica solo a essa e ai suoi figli e non a qualsiasi processo antenato.

Come per altre risposte, la cosa più semplice da fare è riporre i dati in un file.

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

Eccetera.


Ovviamente puoi impostare una variabile in questo modo. Non è necessario un file temporaneo. È possibile impostare la variabile nella subshell e stamparne il valore sulla shell padre in cui si assorbe quel valore. Ottieni tutto lo stato necessario per calcolarne il valore nella subshell, quindi è lì che lo fai.
Mikeserv,

1
@mikeserv Non è la stessa cosa, motivo per cui l'OP ha affermato che una tale soluzione non funzionerà (anche se questo avrebbe dovuto essere chiarito nella domanda). Quello a cui ti riferisci è passare un valore a un altro processo tramite IPC in modo che possa assegnare quel valore a qualunque cosa. Ciò che l'OP voleva / doveva fare era influenzare il valore di una variabile globale condivisa da una serie di processi e non è possibile farlo tramite l'ambiente; non è molto utile per IPC.
Riccioli d'oro,

Amico, o ho completamente frainteso ciò che è necessario qui, o tutti gli altri hanno. Mi sembra davvero semplice. Vedi la mia modifica? Che cosa c'è che non va?
Mikeserv,

@mikeserv Non penso che tu abbia frainteso ed essere onesti, quello che hai è una forma di IPC e potrebbe funzionare. Non mi è chiaro il motivo per cui a Konrad non piaccia, ma se non è abbastanza flessibile, la scorta di file è piuttosto semplice (e così sono i modi per evitare le collisioni, ad esempio mktemp).
Riccioli d'oro,

2
@mikeserv La funzione desiderata viene chiamata quando il valore di PS2viene espanso dalla shell. Non hai l'opportunità di aggiornare il valore di una variabile nella shell padre in quel momento.
Chepner,

0

Per riferimento, ecco la mia soluzione che utilizza i file temporanei, che sono univoci per processo di shell, ed eliminati il ​​più presto possibile (per evitare disordine, come accennato nella domanda):

# 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"
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.