Stateful-Bash-Funktion


16

Ich möchte eine Funktion in Bash implementieren, die bei jedem Aufruf die Anzahl erhöht (und zurückgibt). Leider scheint dies nicht trivial zu sein, da ich die Funktion in einer Subshell aufrufe und folglich die Variablen der übergeordneten Shell nicht ändern kann.

Hier ist mein Versuch:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

Dies würde wie folgt verwendet werden (und daher muss ich die Funktionen von einer Subshell aus aufrufen):

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

Auf diese Weise hätte ich eine nummerierte mehrzeilige Eingabeaufforderung:

> echo 'this
1   is
2   a
3   test'

Süß. Aber aufgrund der oben genannten Einschränkung funktioniert nicht.

Eine nicht funktionierende Lösung wäre, die Anzahl in eine Datei anstatt in eine Variable zu schreiben. Dies würde jedoch zu einem Konflikt zwischen mehreren gleichzeitig ausgeführten Sitzungen führen. Ich könnte natürlich die Prozess-ID der Shell an den Dateinamen anhängen. Aber ich hoffe, es gibt eine bessere Lösung, die mein System nicht mit vielen Dateien überfrachtet.


WRT-Kollisionen mit einem Datei-Stash sehen man 1 mktemp.
Goldlöckchen

Du solltest meine Bearbeitung sehen - ich denke, es wird dir gefallen.
mikeserv

Antworten:


14

Bildbeschreibung hier eingeben

Um die gleiche Ausgabe zu erhalten, die Sie in Ihrer Frage notiert haben, ist nur Folgendes erforderlich:

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

Sie brauchen sich nicht zu verziehen. Diese beiden Zeilen funktionieren in jeder Shell, die eine POSIX-Kompatibilität vorgibt.

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

Aber mir hat das gefallen. Und ich wollte die Grundlagen dafür demonstrieren, was diese Arbeit ein bisschen besser macht. Also habe ich das ein wenig bearbeitet. Ich habe es /tmperstmal reingesteckt, aber ich denke, ich werde es auch für mich behalten. Es ist hier:

cat /tmp/prompt

PROMPT SCRIPT:

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

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

Hinweis: Nachdem ich kürzlich von Yash erfahren habe , habe ich es gestern gebaut. Aus irgendeinem Grund gibt es nicht das erste Byte jedes Arguments mit der %cZeichenfolge aus - obwohl die Dokumente spezifisch für Wide-Char-Erweiterungen für dieses Format waren und es möglicherweise damit zusammenhängt -, aber es ist in Ordnung mit%.1s

Das ist die ganze Sache. Dort oben gehen zwei Dinge vor. Und so sieht es aus:

/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

Jedes Mal, wenn $PS1es ausgewertet wird, wird es analysiert und gedruckt $PWD, um es der Eingabeaufforderung hinzuzufügen. Aber ich mag es nicht, wenn $PWDmein Bildschirm voll ist, deshalb möchte ich nur den ersten Buchstaben jedes Breadcrumbs im aktuellen Pfad bis zum aktuellen Verzeichnis, das ich gerne vollständig sehen würde. So was:

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

Hier gibt es ein paar Schritte:

IFS=/

Wir müssen den Strom aufteilen, $PWDund der zuverlässigste Weg, dies zu tun, ist $IFSSplit on /. Danach brauchen Sie sich überhaupt nicht mehr darum zu kümmern - alle Aufteilungen von hier an werden durch das Positionsparameter- $@Array der Shell im nächsten Befehl wie folgt definiert :

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

Das ist ein bisschen knifflig, aber die Hauptsache ist, dass wir uns $PWDauf /Symbole aufteilen. Ich verwende auch die Parametererweiterung, um $lastnach jedem Wert, der zwischen dem /Schrägstrich ganz links und ganz rechts liegt , alles zuzuweisen . Auf diese Weise weiß ich, dass wenn ich gerade dabei bin /und nur einen habe, /dann $lastimmer noch das Ganze gleich ist $PWDund $1leer sein wird. Das ist wichtig. Ich entferne auch $lastdas hintere Ende von, $PWDbevor ich es zuordne $@.

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

Also hier - solange ${1+is set}wir printfdas erste %cZeichen der Argumente unserer Shell sind - das wir gerade auf jedes Verzeichnis in unserem aktuellen $PWD- abzüglich des obersten Verzeichnisses - aufgeteilt haben /. Wir drucken also im Wesentlichen nur das erste Zeichen jedes Verzeichnisses $PWDaußer dem obersten. Es ist jedoch wichtig zu wissen, dass dies nur dann geschieht, wenn $1es überhaupt gesetzt wird, was nicht bei root /oder bei einem, der von /solchen wie in entfernt wird, der Fall ist /etc.

printf "$last > "

$lastist die Variable, die ich gerade unserem Hauptverzeichnis zugewiesen habe. Das ist also unser oberstes Verzeichnis. Es wird gedruckt, ob die letzte Anweisung dies tat oder nicht. Und es braucht ein ordentliches kleines >Maß.

ABER WAS IST MIT DER ERHÖHUNG?

Und dann ist da noch die Sache mit der $PS2Bedingung. Ich habe bereits früher gezeigt, wie dies gemacht werden kann. Dies ist grundsätzlich eine Frage des Umfangs. Aber es printf \bsteckt noch ein bisschen mehr dahinter, es sei denn, Sie möchten eine Reihe von Ackspaces erstellen und dann versuchen, die Anzahl ihrer Charaktere auszugleichen ... ugh. Also mache ich das:

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

Auch hier ${parameter##expansion}spart der Tag. Hier ist es allerdings etwas seltsam - wir setzen die Variable tatsächlich, während wir sie selbst entfernen. Wir verwenden seinen neuen Wert - Set Mid-Strip - als Glob, von dem wir entfernen. Siehst du? Wir ##*streifen alles vom Kopf unserer Inkrementvariablen bis zum letzten Zeichen ab, von dem alles sein kann [$((PS2c=0))-9]. Auf diese Weise wird garantiert, dass der Wert nicht ausgegeben wird, und wir weisen ihn dennoch zu. Es ist ziemlich cool - das habe ich noch nie gemacht. POSIX garantiert uns aber auch, dass dies die portabelste Art ist, dies zu tun.

Dank POSIX ${parameter} $((expansion))bleiben diese Definitionen in der aktuellen Shell, ohne dass wir sie in einer separaten Subshell festlegen müssen, unabhängig davon, wo wir sie auswerten. Und deshalb funktioniert es in dashund shgenauso gut wie in bashund zsh. Wir verwenden keine Shell / Terminal-abhängigen Escapes und lassen die Variablen selbst testen. Das macht portablen Code schnell.

Der Rest ist ziemlich einfach - erhöhen Sie einfach unseren Zähler für jedes Mal $PS2, wenn $PS1er ausgewertet wird, bis er wieder zurückgesetzt wird. So was:

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

So jetzt kann ich:

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

Es funktioniert genauso in bashoder 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

Wie oben erwähnt, besteht das Hauptproblem darin, dass Sie sich überlegen müssen, wo Sie Ihre Berechnung durchführen. Sie erhalten den Status nicht in der übergeordneten Shell - also berechnen Sie dort nicht. Sie erhalten den Status in der Subshell - also rechnen Sie dort. Aber Sie machen die Definition in der übergeordneten Shell.

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 Wir drehen uns im Kreis. Ich weiß das alles. Aber wie verwende ich das in meiner Definition von PS2? Dies ist der schwierige Teil. Ich glaube nicht, dass Ihre Lösung hier angewendet werden kann. Wenn Sie anders denken, zeigen Sie mir bitte, wie.
Konrad Rudolph

1
@mikeserv Nein, das hat nichts zu tun, sorry. Siehe meine Frage für Details. PS1und PS2sind spezielle Variablen in der Shell, die als Eingabeaufforderung ausgegeben werden (versuchen Sie es, indem Sie PS1in einem neuen Shell-Fenster einen anderen Wert festlegen). Sie werden daher ganz anders als Ihr Code verwendet. Hier einige weitere Informationen zu ihrer Verwendung: linuxconfig.org/bash-prompt-basics
Konrad Rudolph

1
@KonradRudolph Was hindert Sie daran, sie zweimal zu definieren? Welches ist, was meine ursprüngliche Sache getan hat ... Ich muss auf Ihre Antwort schauen ... Dies wird die ganze Zeit getan.
mikeserv

1
@mikeserv Geben Sie echo 'thisan einer Eingabeaufforderung Folgendes ein und erläutern Sie, wie Sie den Wert von aktualisieren, PS2bevor Sie das schließende einfache Anführungszeichen eingeben.
Chepner

1
Okay, diese Antwort ist jetzt offiziell erstaunlich. Ich mag auch die Semmelbrösel, obwohl ich sie nicht übernehmen werde, da ich den vollständigen Pfad sowieso in einer separaten Zeile drucke
Konrad Rudolph

8

Mit diesem Ansatz (Funktion wird in einer Subshell ausgeführt) können Sie den Status des Master-Shell-Prozesses nicht aktualisieren, ohne Verzerrungen zu durchlaufen. Sorgen Sie stattdessen dafür, dass die Funktion im Master-Prozess ausgeführt wird.

Der Wert der PROMPT_COMMANDVariablen wird als Befehl interpretiert, der vor dem Drucken der PS1Eingabeaufforderung ausgeführt wird.

Denn PS2es gibt nichts Vergleichbares. Sie können jedoch stattdessen einen Trick verwenden: Da Sie lediglich eine arithmetische Operation ausführen möchten, können Sie eine arithmetische Erweiterung verwenden, die keine Unterschale umfasst.

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

Das Ergebnis der arithmetischen Berechnung endet in der Eingabeaufforderung. Wenn Sie es ausblenden möchten, können Sie es als Array-Index übergeben, der nicht vorhanden ist.

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

4

Es ist ein bisschen I / O-intensiv, aber Sie müssen eine temporäre Datei verwenden, um den Wert der Zählung zu halten.

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

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

Wenn Sie befürchten, eine separate Datei pro Shell-Sitzung zu benötigen (was als unbedeutend erscheint; werden Sie wirklich mehrzeilige Befehle in zwei verschiedenen Shells gleichzeitig eingeben?), Sollten Sie mktempfür jede eine neue Datei erstellen verwenden.

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 Die E / A ist wahrscheinlich nicht sehr wichtig, da die Datei, wenn sie klein ist und häufig zugegriffen wird, zwischengespeichert wird, dh im Wesentlichen als gemeinsamer Speicher fungiert.
Goldlöckchen

1

Auf diese Weise können Sie keine Shell-Variable verwenden, und Sie wissen bereits, warum. Eine Subshell erbt Variablen genauso wie ein Prozess seine Umgebung: Alle vorgenommenen Änderungen gelten nur für sie und ihre untergeordneten Elemente und nicht für einen Vorgängerprozess.

Laut anderen Antworten ist es am einfachsten, diese Daten in einer Datei zu speichern.

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

Etc.


Natürlich können Sie auf diese Weise eine Variable setzen. Sie benötigen keine temporäre Datei. Sie setzen die Variable in der Subshell und drucken ihren Wert in die übergeordnete Shell, in der Sie diesen Wert aufnehmen. Sie erhalten den gesamten Status, den Sie benötigen, um den Wert in der Subshell zu berechnen.
mikeserv

1
@mikeserv Das ist nicht dasselbe, weshalb das OP gesagt hat, eine solche Lösung werde nicht funktionieren (obwohl dies in der Frage klarer hätte gemacht werden sollen). Sie beziehen sich auf die Übergabe eines Werts an einen anderen Prozess über IPC , damit dieser Wert einem beliebigen Prozess zugewiesen werden kann. Das OP wollte / musste den Wert einer globalen Variablen beeinflussen, die von einer Reihe von Prozessen gemeinsam genutzt wird, und Sie können dies nicht über die Umgebung tun. es ist nicht sehr nützlich für IPC.
Goldlöckchen

Mann, entweder habe ich völlig falsch verstanden, was hier gebraucht wird, oder jeder andere hat. Es scheint mir wirklich einfach zu sein. Siehst du meine Bearbeitung? Was stimmt damit nicht?
mikeserv

@mikeserv Ich glaube nicht, dass du missverstanden hast und um fair zu sein, was du hast, ist eine Form von IPC und könnte funktionieren. Es ist mir nicht klar, warum es Konrad nicht gefällt, aber wenn es nicht flexibel genug ist, ist der Dateiversteck ziemlich einfach (und auch Möglichkeiten, Kollisionen zu vermeiden, z mktemp. B. ).
Goldlöckchen

2
@mikeserv Die beabsichtigte Funktion wird aufgerufen, wenn der Wert von PS2von der Shell erweitert wird. Zu diesem Zeitpunkt haben Sie keine Möglichkeit, den Wert einer Variablen in der übergeordneten Shell zu aktualisieren.
Chepner

0

Als Referenz hier meine Lösung mit temporären Dateien, die pro Shell-Prozess eindeutig sind und so schnell wie möglich gelöscht werden (um Unordnung zu vermeiden, wie in der Frage angedeutet):

# 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"
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.