it-swarm-eu.dev

Získejte stav ukončení procesu, který je přenesen do jiného

Mám dva procesy foo a bar, spojené s trubkou:

$ foo | bar

bar vždy opouští 0; Mám zájem o výstupní kód foo. Existuje nějaký způsob, jak se k tomu dostat?

314
Michael Mrozek

Pokud používáte bash, můžete pomocí proměnné pole PIPESTATUS získat stav ukončení každého prvku potrubí.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Pokud používáte zsh, pole se nazývá pipestatus (případové záležitosti!) A indexy pole začínají jedním:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

Chcete-li je zkombinovat do funkce způsobem, který neztratí hodnoty:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Spustit výše v bash nebo zsh a dostanete stejné výsledky; pouze jeden z retval_bash a retval_zsh bude nastaveno. Druhý bude prázdný. To by umožnilo ukončení funkce return $retval_bash $retval_zsh (všimněte si nedostatku nabídek!).

277
camh

Existují 3 běžné způsoby, jak toho dosáhnout:

Pipefail

Prvním způsobem je nastavení volby pipefail (ksh, zsh nebo bash). Toto je nejjednodušší a to, co dělá, je v podstatě nastavit stav ukončení $? k výstupnímu kódu posledního programu pro ukončení nenulové (nebo nulové, pokud byly všechny úspěšně ukončeny).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash má také maticovou proměnnou nazvanou $PIPESTATUS ($pipestatus in zsh), který obsahuje stav ukončení všech programů v posledním potrubí.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

Příklad 3. příkazu můžete použít k získání konkrétní hodnoty v potrubí, které potřebujete.

Samostatné popravy

Toto je nejsnadnější řešení. Spusťte každý příkaz zvlášť a zachyťte stav

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
253
Patrick

Toto řešení funguje bez použití bash specifických funkcí nebo dočasných souborů. Bonus: na konci je stav ukončení vlastně stavem ukončení a nikoli řetězcem v souboru.

Situace:

someprog | filter

chcete stav ukončení z someprog a výstup z filter.

Zde je moje řešení:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

výsledek tohoto konstruktu je stdout od filter jako stdout konstruktu a výstupní stav z someprog jako stav ukončení konstruktu.


tento konstrukt také pracuje s jednoduchým seskupením příkazů {...} namísto subshells (...). subshells mají některé důsledky, mimo jiné náklady na výkon, které zde nepotřebujeme. přečtěte si příručku pro jemné bash pro více informací: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

Bohužel bash gramatika vyžaduje mezery a středníky pro složené závorky, aby se konstrukce stala mnohem prostornější.

Ke zbytku tohoto textu budu používat variantu subshell.


Příklad someprog a filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Příklad výstupu:

filtered line1
filtered line2
filtered line3
42

Poznámka: Podřízený proces zdědí popisovače otevřeného souboru od nadřazeného. To znamená, že someprog zdědí otevřený deskriptor souboru 3 a 4. Pokud someprog zapíše do deskriptoru souboru 3, stane se to stavem ukončení. Skutečný stav ukončení bude ignorován, protože read přečte pouze jednou.

Pokud se obáváte, že váš someprog může zapisovat do deskriptoru souboru 3 nebo 4, je nejlepší zavřít popisovače souborů před voláním someprog.

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

The exec 3>&- 4>&- before someprog uzavře deskriptor souboru před provedením someprog, takže pro someprog tyto deskriptory prostě neexistují.

Lze to napsat také takto: someprog 3>&- 4>&-


Vysvětlení konstrukce krok za krokem:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

Zdola nahoru:

  1. Subshell je vytvořen s deskriptorem souboru 4 přesměrovaným na stdout. To znamená, že cokoli je vytištěno do deskriptoru souboru 4 v subshell, skončí jako stdout celého konstruktu.
  2. Potrubí je vytvořeno a příkazy vlevo (#part3) a vpravo (#part2) jsou provedeny. exit $xs je také posledním příkazem roury a to znamená, že řetězec ze stdin bude stavem ukončení celého konstruktu.
  3. Subshell je vytvořen s deskriptorem souboru 3 přesměrovaným na stdout. To znamená, že cokoli bude vytištěno do deskriptoru souboru 3 v tomto podshellu, skončí v #part2 a zase bude stav ukončení celého konstruktu.
  4. Potrubí je vytvořeno a příkazy vlevo (#part5 a #part6) a vpravo (filter >&4) jsou provedeny. Výstup filter je přesměrován do deskriptoru souboru 4. V #part1 popisovač souboru 4 byl přesměrován na stdout. To znamená, že výstup filter je stdout celého konstruktu.
  5. Ukončete stav z #part6 je vytištěno do deskriptoru souboru 3. V #part3 popisovač souboru 3 byl přesměrován na #part2. To znamená, že stav ukončení z #part6 bude konečný stav ukončení celého konstruktu.
  6. someprog je proveden. Stav ukončení je převzat v #part5. Stdout je odebrán potrubím v #part4 a přeposlána na filter. Výstup z filter bude zase dosahovat stdout, jak je vysvětleno v #part4
58
lesmana

I když to není přesně to, co jste požádali, můžete použít

#!/bin/bash -o pipefail

takže vaše potrubí vrátí poslední nenulový návrat.

může být o něco méně kódování

Upravit: Příklad

[[email protected] ~]# false | true
[[email protected] ~]# echo $?
0
[[email protected] ~]# set -o pipefail
[[email protected] ~]# false | true
[[email protected] ~]# echo $?
1
37
Chris

Co dělat, když je to možné, je vložit výstupní kód z foo do bar. Pokud například vím, že foo nikdy nevytvoří řádek s pouhými číslicemi, mohu jen ukončit výstupní kód:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Nebo pokud vím, že výstup z foo nikdy neobsahuje řádek s právě .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

To lze vždy provést, pokud existuje nějaký způsob, jak přimět bar, aby fungoval na všech kromě posledního řádku a předal poslední řádek jako svůj výstupní kód.

Pokud bar je komplexní potrubí, jehož výstup nepotřebujete, můžete jeho část obejít vytištěním výstupního kódu na jiném deskriptoru souboru.

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

Po tomto $exit_codes Je obvykle foo:X bar:Y, Ale může to být bar:Y foo:X, Pokud bar skončí před přečtením všech jeho vstupů nebo pokud máte smůlu. Myslím, že zápisy do kanálů až 512 bytů jsou atomové ve všech pobočkách, takže části foo:$? A bar:$? Nebudou smíchány, dokud nebudou řetězce značek pod 507 bajtů.

Pokud potřebujete zachytit výstup z bar, je to obtížné. Výše uvedené techniky můžete kombinovat uspořádáním tak, aby výstup bar nikdy neobsahoval řádek, který vypadá jako indikace výstupního kódu, ale bude se chovat fiddly.

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

A samozřejmě existuje jednoduchá možnost pomocí dočasného soubor k uložení stavu. Jednoduché, ale ne , které jednoduché ve výrobě:

  • Pokud existuje více skriptů spuštěných současně nebo pokud stejný skript používá tuto metodu na několika místech, musíte se ujistit, že používají různé dočasné názvy souborů.
  • Vytvoření dočasného souboru bezpečně ve sdíleném adresáři je obtížné. Často je /tmp Jediným místem, kde skript dokáže zapisovat soubory. Použijte mktemp , což není POSIX, ale v současné době je k dispozici ve všech závažných odvětvích.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

Počínaje potrubím:

foo | bar | baz

Toto je obecné řešení používající pouze POSIX Shell a žádné dočasné soubory:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses obsahuje stavové kódy všech neúspěšných procesů, v náhodném pořadí, s indexy, které říkají, který příkaz vydal každý stav.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Všimněte si uvozovek kolem $error_statuses v mých testech; bez nich grep nemůže rozlišovat, protože nové řádky jsou donuceny do mezer.

17
Jander

Chtěl jsem tedy přispět takovou odpovědí jako lesmana, ale myslím, že moje je možná trochu jednodušší a o něco výhodnější řešení čistě Bourne-Shell:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Myslím, že je to nejlépe vysvětleno zevnitř ven - command1 provede a vytiskne svůj pravidelný výstup na stdout (deskriptor souboru 1), poté, co je hotovo, printf provede a vytiskne výstupní kód Command1 na svém stdout, ale ten stdout je přesměrován na popisovač souboru 3.

Zatímco příkaz1 je spuštěn, jeho stdout je přiváděn na příkaz2 (výstup printf nikdy nedovolí, aby příkaz2, protože jej odesíláme do deskriptoru souboru namísto 1, což je to, co čte dýmka). Potom přesměrováváme výstup Command2 na deskriptor souboru 4, takže také zůstane mimo deskriptor 1 souboru - protože chceme, aby popisovač souboru 1 byl o něco později zdarma, protože přivedeme výstup printf na deskriptoru souboru 3 zpět dolů do deskriptoru souboru. 1 - protože to je to, co příkaz substituce (backticks) zachytí a to se dostane do proměnné.

Poslední kousek magie je ten první exec 4>&1 udělali jsme jako samostatný příkaz - otevře popisovač souboru 4 jako kopii externího Shell's stdout. Substituce příkazů zachytí vše, co je psáno standardně, z pohledu příkazů uvnitř - ale protože výstup command2 jde do deskriptoru souboru 4, pokud jde o substituci příkazu, substituce příkazu ji nezachytí - jakmile se dostane „ven“ ze substituce příkazu, efektivně stále jde do deskriptoru souboru skriptu 1.

(exec 4>&1 musí být samostatným příkazem, protože mnoho běžných skořápek se mu nelíbí, když se pokoušíte zapsat do deskriptoru souboru uvnitř substituce příkazu, který je otevřen v "externím" příkazu, který používá substituci. Je to nejjednodušší způsob, jak to udělat.)

Můžete se na to dívat méně technickým a hravějším způsobem, jako by se výstupy příkazů navzájem přeskočily: command1 pipe to command2, pak výstup printf přeskočí nad příkazem 2, aby jej příkaz2 nezachytil, a pak výstup příkazu 2 přeskakuje ze substituce příkazů stejně jako printf přistane právě včas, aby se zachytil substitucí tak, že skončí v proměnné, a výstup příkazu2 pokračuje svou veselou cestou zapisováním na standardní výstup, stejně jako v normální trubce.

Také, jak tomu rozumím, $? bude stále obsahovat návratový kód druhého příkazu v potrubí, protože přiřazení proměnných, substituce příkazů a složené příkazy jsou efektivně průhledné pro návratový kód příkazu uvnitř nich, takže návratový stav příkazu2 by se měl šířit ven. - toto, a nemusím definovat další funkci, je důvod, proč si myslím, že by to mohlo být o něco lepší řešení, než jaké navrhuje Lesmana.

Podle zmínky lesmana je možné, že příkaz1 skončí v určitém okamžiku pomocí deskriptorů souborů 3 nebo 4, aby byl robustnější, udělali byste:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Všimněte si, že v mém příkladu používám složené příkazy, ale subshells (pomocí ( ) namísto { } bude také fungovat, i když možná bude méně efektivní.)

Příkazy zdědí popisovače souborů od procesu, který je spustí, takže celý druhý řádek zdědí popisovač souboru čtyři a složený příkaz následovaný 3>&1 zdědí popisovač souboru tři. Takže 4>&- zajišťuje, že vnitřní složený příkaz nezdědí popisovač souboru čtyři a 3>&- nezdědí popisovač souboru tři, takže příkaz1 získá „čistší“, standardnější prostředí. Můžete také přesunout vnitřní 4>&- vedle 3>&-, ale zjistím, proč nejen omezit jeho rozsah co nejvíce.

Nejsem si jistý, jak často věci používají popisovač souborů tři a čtyři přímo - myslím, že většina časových programů používá syscalls, které vracejí nepoužívané popisovače souborů, ale někdy kód píše přímo do popisovače 3 souborů, hádat (dovedl bych si představit program, který kontroluje deskriptor souboru, aby zjistil, zda je otevřený, a používal jej, pokud je, nebo se choval jinak, pokud není). Je tedy pravděpodobně nejlepší mít na paměti a použít je pro obecné účely.

12
mtraceur

Pokud máte nainstalovaný balíček moreutils , můžete použít nástroj mispipe , který přesně dělá, co jste požádali.

11
Emanuele Aina

řešení lesmana výše může být také provedeno bez režie spouštění vnořených podprocesů pomocí { .. } místo toho (pamatujeme, že tato forma seskupených příkazů musí vždy končit středníky). Něco takového:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

Zkontroloval jsem tento konstrukt s pomlčkou verze 0.5.5 a bash verze 3.2.25 a 4.2.42, takže i když některé skořápky nepodporují { .. } seskupení, stále je kompatibilní s POSIX.

7
pkeller

Následující je míněno jako doplněk k odpovědi @ Patrik v případě, že nejste schopni použít jedno z běžných řešení.

Tato odpověď předpokládá následující:

  • Máte Shell, který neví o $PIPESTATUS Ani set -o pipefail
  • Chcete použít potrubí pro paralelní spuštění, takže žádné dočasné soubory.
  • Nechcete-li mít další nepořádek, pokud přerušíte skript, pravděpodobně náhlým výpadkem napájení.
  • Toto řešení by mělo být relativně snadno sledovatelné a čitelné.
  • Nechcete zavádět další dílčí skořepiny.
  • Nemůžete si pohrávat s existujícími deskriptory souborů, takže se nesmíte dotýkat stdin/out/err (můžete však dočasně představit nový).

Další předpoklady. Můžete se všeho zbavit, ale tento recept příliš ohromuje recept, takže zde není zahrnut:

  • Vše, co chcete vědět, je, že všechny příkazy v PIPE mají výstupní kód 0.
  • Nepotřebujete další informace o postranním pásmu.
  • Vaše prostředí čeká na vrácení všech příkazů potrubí.

Před: foo | bar | baz, Ale vrací pouze výstupní kód posledního příkazu (baz)

Chtěl: $? Nesmí být 0 (True), pokud některý z příkazů v kanálu selhal.

Po:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Vysvětlil:

  • Vytvoří se tempfile pomocí mktemp. To obvykle okamžitě vytvoří soubor v /tmp
  • Tento tempfile je poté přesměrován na FD 9 pro zápis a FD 8 pro čtení
  • Pak je tempfile okamžitě smazán. Zůstává však otevřená, dokud obě FD nezmizí.
  • Nyní je potrubí spuštěno. Každý krok přidá pouze do FD 9, pokud došlo k chybě.
  • wait je potřeba pro ksh, protože ksh else nečeká na dokončení všech příkazů kanálu. Vezměte však na vědomí, že pokud jsou přítomny některé úkoly na pozadí, jsou nežádoucí vedlejší účinky, takže jsem ve výchozím nastavení okomentoval. Pokud čekání neuškodí, můžete to komentovat.
  • Poté se načte obsah souboru. Pokud je prázdný (protože všechny fungovaly) read vrátí false, takže true označuje chybu

To lze použít jako náhradu pluginu pro jediný příkaz a vyžaduje pouze následující:

  • Nepoužívané FD 9 a 8
  • Jedna proměnná prostředí, která pojme název tempfile
  • A tento recept lze přizpůsobit téměř každému Shell, který umožňuje přesměrování IO přesměrování)
  • Je to také docela agnostická platforma a nepotřebuje věci jako /proc/fd/N

Hmyz:

Tento skript obsahuje chybu v případě, že /tmp Dojde nedostatek místa. Pokud potřebujete ochranu před tímto umělým případem, můžete to udělat následovně, má však tu nevýhodu, že počet 0 V 000 Závisí na počtu příkazů v potrubí , takže je o něco složitější:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Poznámky k přenositelnosti:

  • ksh a podobné skořepiny, které čekají pouze na poslední příkaz potrubí, musí být wait uncommented

  • Poslední příklad používá printf "%1s" "$?" Místo echo -n "$?", Protože je přenosnější. Ne každá platforma interpretuje -n Správně.

  • printf "$?" By to také udělal, ale printf "%1s" Zachytí některé rohové případy, pokud spustíte skript na nějaké opravdu zlomené platformě. (Přečtěte si: pokud náhodou programujete v paranoia_mode=extreme.)

  • FD 8 a FD 9 mohou být vyšší na platformách, které podporují více číslic. AFAIR v souladu s POSIXem Shell potřebuje pouze podporu jedné číslice.

  • Testováno s Debianem 8.2 sh, bash, ksh, ash, sash a dokonce csh

5
Tino

Toto je přenosné, tj. Pracuje s jakýmkoli jiným shellem kompatibilním s POSIX, nevyžaduje, aby byl aktuální adresář zapisovatelný a umožňuje více skriptů, které používají stejný trik, aby byly spuštěny současně.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Upravit: Zde je silnější verze, která následuje Gillesovy komentáře:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: a tady je mírně lehčí varianta následující dubiousjim komentář:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
4
jlliagre

S trochou opatrnosti by to mělo fungovat:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
3
alex

Následující blok „if“ bude spuštěn, pouze pokud bude úspěšný příkaz „command“:

if command; then
   # ...
fi

Konkrétně můžete spustit něco takového:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

Který bude spuštěn haconf -makerw a uložte jeho stdout a stderr do "$ haconf_out". Pokud je vrácená hodnota z haconf pravdivá, bude proveden blok 'if' a grep bude číst "$ haconf_out", snaží se ji porovnat s "Cluster již zapisovatelným".

Všimněte si, že se potrubí automaticky čistí; s přesměrováním budete muset být opatrní, abyste po dokončení odstranili „$ haconf_out“.

Ne tak elegantní jako pipefail, ale legitimní alternativa, pokud tato funkce není na dosah.

2
Rany Albeg Wein
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
1
C.G.

(Alespoň s bash) v kombinaci s set -e lze použít subshell k výslovné emulaci pipefailu a ukončení chyby potrubí

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Pokud tedy z nějakého důvodu selže foo - zbytek programu se nespustí a skript skončí s odpovídajícím chybovým kódem. (Toto předpokládá, že foo vytiskne svou vlastní chybu, která je dostatečná k pochopení příčiny selhání)

0
noonex