Umgebungsvariablen werden nicht festgelegt, wenn meine Funktion in einer Pipeline aufgerufen wird

10

Ich habe die folgende rekursive Funktion zum Festlegen von Umgebungsvariablen:

function par_set {
  PAR=$1
  VAL=$2
  if [ "" != "$1" ]
  then
    export ${PAR}=${VAL}
    echo ${PAR}=${VAL}
    shift
    shift
    par_set $*
  fi
}

Wenn ich es selbst aufrufe, setzt es sowohl die Variable als auch das Echo auf stdout:

$ par_set FN WORKS
FN=WORKS
$ echo "FN = "$FN
FN = WORKS

Das Umleiten von stdout in eine Datei funktioniert auch:

$ par_set REDIR WORKS > out
cat out
REDIR=WORKS
$ echo "REDIR = "$REDIR
REDIR = WORKS

Wenn ich jedoch stdout an einen anderen Befehl weitergebe, wird die Variable nicht gesetzt:

$ par_set PIPE FAILS |sed -e's/FAILS/BARFS/'
PIPE=BARFS
$ echo "PIPE = "$PIPE
PIPE =

Warum verhindert die Pipe, dass die Funktion die Variable exportiert? Gibt es eine Möglichkeit, dies zu beheben, ohne auf temporäre Dateien oder Named Pipes zurückzugreifen?

Gelöst:

Arbeitscode dank Gilles:

par_set $(echo $*|tr '=' ' ') > >(sed -e's/^/  /' >> ${LOG})

Dadurch kann das Skript folgendermaßen aufgerufen werden:

$ . ./script.sh PROCESS_SUB ROCKS PIPELINES=NOGOOD
$ echo $PROCESS_SUB
ROCKS
$ echo $PIPELINES
NOGOOD
$ cat log
7:20140606155622162731431:script.sh:29581:Parse Command Line parameters.  Params must be in matched pairs separated by one or more '=' or ' '.
  PROCESS_SUB=ROCKS
  PIPELINES=NOGOOD

Projekt gehostet auf bitbucket https://bitbucket.org/adalby/monitor-bash, wenn Sie an vollständigem Code interessiert sind.

Andrew
quelle

Antworten:

8

Jeder Teil einer Rohrleitung (dh jede Seite des Rohres) läuft in einem separaten Prozess (a Subshell aufgerufen, wenn eine Schale gabelt ein Subprozess Teil des Skripts auszuführen). In par_set PIPE FAILS |sed -e's/FAILS/BARFS/'wird die PIPEVariable in dem Unterprozess festgelegt, der die linke Seite der Pipe ausführt. Diese Änderung spiegelt sich nicht im übergeordneten Prozess wider (Umgebungsvariablen werden nicht zwischen Prozessen übertragen, sondern nur von Unterprozessen geerbt.

Die linke Seite eines Rohrs verläuft immer in einer Unterschale. Einige Shells (ATT ksh, zsh) laufen in den übergeordneten Shells auf der rechten Seite. Die meisten führen auch die rechte Seite in einer Unterschale aus.

Wenn Sie sowohl die Ausgabe eines Teils des Skripts umleiten als auch diesen Teil in der übergeordneten Shell in ksh / bash / zsh ausführen möchten, können Sie die Prozessersetzung verwenden .

par_set PROCESS SUBSTITUTION > >(sed s/ION/ED/)

Mit jeder POSIX-Shell können Sie die Ausgabe in eine Named Pipe umleiten.

mkfifo f
<f grep NAMED= &
par_set NAMED PIPE >f

Oh, und Sie vermissen Anführungszeichen für Variablensubstitutionen , Ihr Code bricht bei Dingen wie par_set name 'value with spaces' star '*'.

export "${PAR}=${VAL}"

par_set "$@"
Gilles 'SO - hör auf böse zu sein'
quelle
Prozessersetzung für den Gewinn! Ich wusste, dass ich eine Named Pipe- oder Temp-Datei verwenden könnte, aber diese sind hässlich, haben eine schlechte Parallelität und hinterlassen ein Durcheinander, wenn das Skript stirbt (Trap hilft bei der letzten). Das Weltraum-Ding ist beabsichtigt. Konventionell sind Variablen, die in der Befehlszeile übergeben werden, in Name / Wert-Paaren und durch '=' und / oder '' getrennt.
Andrew
3

Dies funktioniert nicht, da jede Seite der Pipe in einer Unterschale ausgeführt bashwird und die in einer Unterschale festgelegten Variablen lokal für diese Unterschale sind.

Aktualisieren:

Es sieht so aus, als ob es einfach ist, Variablen vom übergeordneten Element an die untergeordnete Shell zu übergeben, aber es ist wirklich schwierig, dies andersherum zu tun. Einige Problemumgehungen werden als Pipes, temporäre Dateien, Schreiben in stdout und Lesen im übergeordneten Element usw. bezeichnet.

Einige Referenzen:

http://mywiki.wooledge.org/BashFAQ/024
/programming//q/15541321/3565972
/programming//a/15383353/3565972
http://forums.opensuse.org/showthread .php / 458979-How-Export-Variable-in-Subshell-Back-Out-to-Parent

Savanto
quelle
Ich dachte, das wäre eine Möglichkeit, aber die Funktion sollte in der aktuellen Shell ausgeführt werden. Ich habe getestet, indem ich der Funktion ein "echo $$" hinzugefügt habe. par_set STILL FAILS | sed -e "s / ^ / sedpid = $$, fnpid = /" gibt sedpid = 15957, fnpid = 15957 aus.
Andrew
@ Andrew Siehe diesen stackoverflow.com/a/20726041/3565972 . Anscheinend $$ist das gleiche für Eltern- und Kinderschalen. Sie können verwenden $BASHPID, um die Subshell-PID zu erhalten. Wenn ich echo $$ $BASHPIDinnerhalb par_setich verschiedene pids bekommen.
Savanto
@ Andrew versucht immer noch, eine Problemumgehung zu finden, schlägt aber fehl! =)
Savanto
@ Savanto-Danke. Ich wusste das nicht über $$ vs $ BASHPID oder die Pipe Forcing Subshells.
Andrew
0

Sie weisen auf die Subshells hin, die mit etwas Finagling in der Shell außerhalb einer Pipeline umgangen werden können, aber der schwierigere Teil des Problems hat mit der Parallelität der Pipeline zu tun .

Alle Prozessmitglieder der Pipeline starten gleichzeitig. Daher ist das Problem möglicherweise leichter zu verstehen, wenn Sie es folgendermaßen betrachten:

{ sleep 1 ; echo $((f=1+2)) >&2 ; } | echo $((f))
###OUTPUT
0
...
3

Die Pipeline-Prozesse können die Variablenwerte nicht erben, da sie bereits deaktiviert sind und ausgeführt werden, bevor die Variable jemals festgelegt wird.

Ich kann nicht wirklich verstehen, worum es bei Ihrer Funktion geht - welchen Zweck erfüllt sie, der exportnoch nicht erfüllt ist? Oder auch nur var=val? Hier ist zum Beispiel wieder fast dieselbe Pipeline:

pipeline() { 
    { sleep 1
      echo "f=$((f=f+1+2))" >&3
    } | echo $((f)) >&2
} 3>&1

f=4 pipeline

###OUTPUT

4
...
f=7

Und mit export:

export $(f=4 pipeline) ; pipeline

###OUTPUT:

4
7
...
f=10

So könnte dein Ding funktionieren wie:

par_set $(echo PIPE FAILS | 
    sed 's/FAIL/WORK/;/./w /path/to/log')

Dies würde sich bei der sedAusgabe einer Datei anmelden und diese als Shell-Split an Ihre Funktion liefern "$@".

Oder alternativ:

$ export $(echo PIPE FAILS | sed 's/ FAIL/=WORK/')
$ par_set $PIPE TWICE_REMOVED
$ echo "WORKS = "$WORKS
WORKS = TWICE_REMOVED

Wenn ich Ihre Funktion schreiben würde, würde sie wahrscheinlich so aussehen:

_env_eval() { 
    while ${2+:} false ; do
       ${1:+export} ${1%%["${IFS}"]*}="${2}" || :
       shift 2
    done
}
mikeserv
quelle
Das ist wahr, aber irrelevant: Andrew versucht, die Variablen nach dem Ende der Pipeline zu verwenden, nicht auf der anderen Seite der Pipe.
Gilles 'SO - hör auf böse zu sein'
@ Gilles - nun, das kann er. |pipeline | sh
Mikesserv
@ Gilles - eigentlich brauchst du gar nicht sh. Er benutzt schon export.
Mikesserv
Übergeordnetes Ziel sind Systemüberwachungsskripte. Möchte sie interaktiv, aus anderen Skripten oder aus cron aufrufen können. Sie möchten Parameter übergeben können, indem Sie env vars festlegen, die Befehlszeile oder die Konfigurationsdatei übergeben. Es gibt eine Funktion, um Eingaben von einer oder mehreren Quellen zu übernehmen und die Umgebung korrekt einzustellen. Ich möchte jedoch auch in der Lage sein, die Ausgabe optional zu protokollieren (nachdem ich sed zum Format durchlaufen habe), daher muss eine Pipe erstellt werden.
Andrew
1
@ mikeserv- Danke für alle Kommentare. Ich habe Gilles Vorschlag einer Prozessersetzung befolgt, aber wenn ich für meinen Code argumentieren muss, bin ich ein besserer Programmierer.
Andrew