To wymaga basha 4.1, jeśli używasz {fd}
lub local -n
.
Reszta powinna działać w bash 3.x mam nadzieję. Nie jestem do końca pewien z powodu printf %q
- może to być funkcja bash 4.
Podsumowanie
Twój przykład można zmodyfikować w następujący sposób, aby zarchiwizować pożądany efekt:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
drukuje zgodnie z życzeniem:
hello
4
Zwróć uwagę, że to rozwiązanie:
- Działa
e=1000
też dla.
- Przetwory,
$?
jeśli potrzebujesz$?
Jedyne złe efekty uboczne to:
- Potrzebuje nowoczesnego
bash
.
- Widły dość częściej.
- Potrzebuje adnotacji (nazwanej tak jak twoja funkcja, z dodanym
_
)
- Poświęca deskryptor pliku 3.
- Jeśli potrzebujesz, możesz zmienić go na inny FD.
- W
_capture
prostu wymienić wszystkie wystąpień z 3
innym (wyższym) numer.
Poniższe (które jest dość długie, przepraszam za to), mam nadzieję, wyjaśnia, jak dostosować ten przepis również do innych skryptów.
Problem
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
wyjścia
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
podczas gdy pożądany wynik jest
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Przyczyna problemu
Zmienne powłoki (lub ogólnie mówiąc, środowisko) są przekazywane z procesów rodzicielskich do procesów potomnych, ale nie odwrotnie.
Jeśli przechwytujesz dane wyjściowe, zwykle jest to uruchamiane w podpowłoce, więc przekazywanie zmiennych jest trudne.
Niektórzy nawet mówią, że nie da się tego naprawić. To jest złe, ale od dawna znany jest trudny do rozwiązania problem.
Istnieje kilka sposobów najlepszego rozwiązania tego problemu, zależy to od Twoich potrzeb.
Oto przewodnik krok po kroku, jak to zrobić.
Przekazywanie zmiennych do powłoki rodzicielskiej
Istnieje sposób przekazania zmiennych do powłoki rodzicielskiej. Jest to jednak niebezpieczna ścieżka, ponieważ ta używa eval
. Jeśli zostanie to zrobione niewłaściwie, ryzykujesz wiele złych rzeczy. Ale jeśli zostanie to zrobione poprawnie, jest to całkowicie bezpieczne, pod warunkiem, że nie ma błędu bash
.
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
wydruki
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Pamiętaj, że działa to również w przypadku niebezpiecznych rzeczy:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
wydruki
; /bin/echo *
Wynika to z printf '%q'
tego, że cytuje wszystko w taki sposób, że można bezpiecznie ponownie użyć go w kontekście powłoki.
Ale to jest ból w ...
To nie tylko wygląda brzydko, ale także jest dużo do pisania, więc jest podatne na błędy. Tylko jeden błąd i jesteś skazany, prawda?
Cóż, jesteśmy na poziomie powłoki, więc możesz to poprawić. Pomyśl tylko o interfejsie, który chcesz zobaczyć, a następnie możesz go zaimplementować.
Rozszerz, jak powłoka przetwarza rzeczy
Cofnijmy się o krok i zastanówmy się nad jakimś API, które pozwoli nam w łatwy sposób wyrazić to, co chcemy robić.
Cóż, co chcemy zrobić z d()
funkcją?
Chcemy przechwycić dane wyjściowe do zmiennej. OK, w takim razie zaimplementujmy API właśnie do tego:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Teraz zamiast pisać
d1=$(d)
możemy pisać
capture d1 d
Cóż, wygląda na to, że niewiele się zmieniliśmy, ponieważ ponownie zmienne nie są przekazywane z powrotem d
do powłoki nadrzędnej i musimy wpisać trochę więcej.
Jednak teraz możemy rzucić na niego pełną moc powłoki, ponieważ jest ładnie opakowana w funkcję.
Pomyśl o łatwym do ponownego wykorzystania interfejsie
Po drugie, chcemy być SUCHY (nie powtarzaj się). Więc definitywnie nie chcemy pisać czegoś takiego
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
x
Tutaj jest nie tylko zbędne, jest podatny na błędy zawsze repeate we właściwym kontekście. A jeśli użyjesz go 1000 razy w skrypcie, a następnie dodasz zmienną? Zdecydowanie nie chcesz zmieniać wszystkich 1000 lokalizacji, do których dochodzi połączenie d
.
Więc zostaw to x
daleko, żebyśmy mogli napisać:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
wyjścia
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
To już wygląda bardzo dobrze. (Ale nadal jest to, local -n
które nie działa w oder common bash
3.x)
Unikaj zmian d()
Ostatnie rozwiązanie ma kilka dużych wad:
d()
musi zostać zmieniony
xcapture
Aby przekazać dane wyjściowe, musi użyć pewnych wewnętrznych szczegółów .
- Zwróć uwagę, że to cienie (wypala) jedną nazwaną zmienną
output
, więc nigdy nie możemy jej przekazać.
- Musi współpracować
_passback
Czy też możemy się tego pozbyć?
Oczywiście możemy! Jesteśmy w skorupie, więc jest wszystko, czego potrzebujemy, aby to zrobić.
Jeśli przyjrzysz się bliżej wezwaniu eval
, zobaczysz, że mamy 100% kontrolę w tej lokalizacji. „Wewnątrz” eval
znajdujemy się w podpowłoce, więc możemy robić wszystko, co chcemy, bez obawy, że zrobimy coś złego w powłoce rodzicielskiej.
Tak, fajnie, więc dodajmy kolejne opakowanie, teraz bezpośrednio w eval
:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
wydruki
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Jednak to znowu ma pewną poważną wadę:
- Te
!DO NOT USE!
markery są tam, bo jest to bardzo zły stan wyścig w tym, czego nie można zobaczyć w prosty sposób:
- To
>(printf ..)
jest praca w tle. Może więc nadal działać, gdy _passback x
jest uruchomiony.
- Możesz to zobaczyć samodzielnie, dodając
sleep 1;
przed printf
lub _passback
.
_xcapture a d; echo
następnie odpowiednio wyprowadza x
lub a
pierwszy.
- Nie
_passback x
należy go częścią _xcapture
, ponieważ utrudnia to ponowne użycie tego przepisu.
- Mamy tu też nieużywany rozwidlenie (the
$(cat)
), ale ponieważ jest to rozwiązanie, !DO NOT USE!
wybrałem najkrótszą trasę.
Jednak pokazuje to, że możemy to zrobić bez modyfikacji d()
(i bez local -n
)!
Zwróć uwagę, że nie jest to wcale konieczne _xcapture
, ponieważ mogliśmy wszystko zapisać w pliku eval
.
Jednak robienie tego zwykle nie jest zbyt czytelne. A jeśli wrócisz do swojego scenariusza za kilka lat, prawdopodobnie będziesz chciał móc go ponownie przeczytać bez większych problemów.
Napraw wyścig
Teraz naprawmy stan wyścigu.
Sztuczka może polegać na tym, aby poczekać, aż printf
zamknie się STDOUT, a następnie wyprowadzić x
.
Istnieje wiele sposobów archiwizacji tego:
- Nie można używać rur osłonowych, ponieważ rury przebiegają w różnych procesach.
- Można używać plików tymczasowych,
- lub coś w rodzaju pliku blokady lub kolejki FIFO. Pozwala to czekać na blokadę lub FIFO,
- lub różnymi kanałami, aby wyprowadzić informacje, a następnie zestawić dane wyjściowe w odpowiedniej kolejności.
Podążanie za ostatnią ścieżką mogłoby wyglądać (zauważ, że robi to printf
ostatnią, ponieważ działa to lepiej tutaj):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
wyjścia
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Dlaczego jest to poprawne?
_passback x
bezpośrednio rozmawia z STDOUT.
- Jednakże, ponieważ STDOUT musi być przechwycone w poleceniu wewnętrznym, najpierw "zapisujemy" go do FD3 (możesz oczywiście użyć innych) z '3> & 1', a następnie używamy go ponownie
>&3
.
- Do
$("${@:2}" 3<&-; _passback x >&3)
wykończenia po _passback
, kiedy podpowłoki zamyka standardowe wyjście.
- Więc
printf
nie może się to zdarzyć przed _passback
, niezależnie od tego, jak długo _passback
to potrwa.
- Zauważ, że
printf
polecenie nie jest wykonywane przed złożeniem całego wiersza polecenia, więc nie możemy zobaczyć artefaktów printf
, niezależnie od tego , jak printf
jest zaimplementowane.
Dlatego najpierw _passback
wykonuje, a następnie printf
.
To rozwiązuje wyścig, poświęcając jeden ustalony deskryptor pliku 3. Możesz oczywiście wybrać inny deskryptor pliku w przypadku, gdy FD3 nie jest wolny w twoim skrypcie.
Proszę również zwrócić uwagę na to, 3<&-
co chroni FD3 przed przekazaniem do funkcji.
Uczyń to bardziej ogólnym
_capture
zawiera części, do których należą d()
, co jest złe z punktu widzenia możliwości ponownego użycia. Jak to rozwiązać?
Cóż, zrób to w desperacki sposób, wprowadzając jeszcze jedną rzecz, dodatkową funkcję, która musi zwrócić właściwe rzeczy, której nazwa pochodzi od pierwotnej funkcji z _
dołączoną.
Ta funkcja jest wywoływana po funkcji rzeczywistej i może wzmacniać rzeczy. W ten sposób można to odczytać jako adnotację, dzięki czemu jest bardzo czytelny:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
nadal drukuje
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Zezwól na dostęp do kodu zwrotnego
Brakuje tylko jednego bitu:
v=$(fn)
ustawia $?
to, co fn
wróciło. Więc prawdopodobnie też tego chcesz. Wymaga jednak większych poprawek:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
wydruki
23 42 69 FAIL
Jest jeszcze wiele do zrobienia
_passback()
można wyeliminować passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
można wyeliminować za pomocą capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
Rozwiązanie zanieczyszcza deskryptor pliku (tutaj 3), używając go wewnętrznie. Musisz o tym pamiętać, jeśli zdarzy Ci się zdać FD.
Zauważ, że bash
4.1 i nowsze muszą {fd}
używać nieużywanego FD.
(Być może dodam tutaj rozwiązanie, kiedy się pojawię.)
Zauważ, że dlatego używam oddzielnych funkcji, takich jak _capture
, ponieważ upchnięcie tego wszystkiego w jedną linię jest możliwe, ale sprawia, że coraz trudniej jest przeczytać i zrozumieć
Być może chcesz również przechwycić STDERR wywoływanej funkcji. Lub chcesz nawet przekazywać i przekazywać więcej niż jeden deskryptor pliku zi do zmiennych.
Nie mam jeszcze rozwiązania, jednak tutaj jest sposób na złapanie więcej niż jednego FD , więc prawdopodobnie możemy również w ten sposób przekazać zmienne.
Nie zapomnij również:
To musi wywołać funkcję powłoki, a nie polecenie zewnętrzne.
Nie ma łatwego sposobu przekazywania zmiennych środowiskowych z poleceń zewnętrznych. (Z LD_PRELOAD=
tym jednak powinno być!) Ale to jest coś zupełnie innego.
Ostatnie słowa
To nie jedyne możliwe rozwiązanie. To jeden przykład rozwiązania.
Jak zawsze masz wiele sposobów wyrażania rzeczy w powłoce. Więc nie krępuj się ulepszyć i znaleźć coś lepszego.
Przedstawione tutaj rozwiązanie jest dalekie od doskonałości:
- To prawie nie było w ogóle testowane, więc proszę wybacz literówki.
- Jest wiele do zrobienia, patrz powyżej.
- Używa wielu funkcji od nowoczesnych
bash
, więc prawdopodobnie trudno jest go przenieść na inne powłoki.
- I mogą być jakieś dziwactwa, o których nie myślałem.
Jednak myślę, że jest dość łatwy w użyciu:
- Dodaj tylko 4 wiersze „biblioteki”.
- Dodaj tylko jedną linię „adnotacji” dla swojej funkcji powłoki.
- Poświęca tymczasowo tylko jeden deskryptor pliku.
- Każdy krok powinien być łatwy do zrozumienia nawet po latach.