Jak zmodyfikować zmienną globalną w funkcji w bash?

105

Pracuję z tym:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

Mam taki skrypt jak poniżej:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

Który zwraca:

hello
4

Ale jeśli przypiszę wynik funkcji do zmiennej, zmienna globalna enie jest modyfikowana:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

ret=$(test1)

echo "$ret"
echo "$e"

Zwroty:

hello
2

Słyszałem o użyciu eval w tym przypadku, więc zrobiłem to w test1:

eval 'e=4'

Ale ten sam wynik.

Czy możesz mi wyjaśnić, dlaczego nie jest modyfikowany? Jak mogę zapisać echo test1funkcji w reti zmodyfikować również zmienną globalną?

harrison4
źródło
Czy chcesz wrócić witam? Możesz po prostu powtórzyć echo $ e, aby powrócił. Albo powtórzyć wszystko, co chcesz, a następnie przeanalizować wynik?

Odpowiedzi:

98

Kiedy używasz podstawiania poleceń (tj. $(...)Konstrukcji), tworzysz podpowłokę. Podpowłoki dziedziczą zmienne ze swoich powłok nadrzędnych, ale działa to tylko w jeden sposób - podpowłoka nie może modyfikować środowiska swojej powłoki nadrzędnej. Twoja zmienna ejest umieszczona w podpowłoce, ale nie w powłoce nadrzędnej. Istnieją dwa sposoby przekazywania wartości z podpowłoki do jej elementu nadrzędnego. Najpierw możesz wyprowadzić coś na standardowe wyjście, a następnie przechwycić to za pomocą podstawienia polecenia:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Daje:

Hello

W przypadku wartości liczbowej od 0 do 255 można użyć returndo przekazania liczby jako statusu wyjścia:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Daje:

Hello - num is 4
Josh Jolly
źródło
Dzięki za punkt, ale muszę zwrócić tablicę ciągów, aw ramach funkcji muszę dodać elementy do dwóch globalnych tablic ciągów.
harrison4
3
Zdajesz sobie sprawę, że jeśli po prostu uruchomisz funkcję bez przypisywania jej do zmiennej, wszystkie zawarte w niej zmienne globalne zostaną zaktualizowane. Zamiast zwracać tablicę ciągów, dlaczego nie zaktualizować tablicy ciągów w funkcji, a następnie przypisać ją do innej zmiennej po zakończeniu funkcji?
@JohnDoe: Nie możesz zwrócić „tablicy ciągów” z funkcji. Wszystko, co możesz zrobić, to wydrukować ciąg. Możesz jednak zrobić coś takiego:setarray() { declare -ag "$1=(a b c)"; }
rici
34

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=1000też 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 _captureprostu wymienić wszystkie wystąpień z 3innym (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 ddo 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

xTutaj 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 xdaleko, ż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 -nktóre nie działa w oder common bash3.x)

Unikaj zmian d()

Ostatnie rozwiązanie ma kilka dużych wad:

  • d() musi zostać zmieniony
  • xcaptureAby 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” evalznajdujemy 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 xjest uruchomiony.
    • Możesz to zobaczyć samodzielnie, dodając sleep 1;przed printflub _passback. _xcapture a d; echonastępnie odpowiednio wyprowadza xlub apierwszy.
  • Nie _passback xnależ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ż printfzamknie 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 printfostatnią, 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 printfnie może się to zdarzyć przed _passback, niezależnie od tego, jak długo _passbackto potrwa.
  • Zauważ, że printfpolecenie 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 printfjest zaimplementowane.

Dlatego najpierw _passbackwykonuje, 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

_capturezawiera 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 fnwró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 bash4.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.
Tino
źródło
2
jesteś niesamowity
Eliran Malka
14

Może możesz użyć pliku, zapisać do pliku wewnątrz funkcji, czytać z pliku po nim. Zmieniłem ena tablicę. W tym przykładzie puste miejsca są używane jako separatory podczas odczytu tablicy.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Wynik:

hi
first second third
first
second
third
Ashkan
źródło
13

Co robisz, wykonujesz test1

$(test1)

w powłoce podrzędnej (powłoce potomnej), a powłoki potomne nie mogą niczego modyfikować w rodzica .

Możesz go znaleźć w instrukcji basha

Proszę sprawdzić: wyniki są wyświetlane w podpowłoce tutaj

PradyJord
źródło
7

Miałem podobny problem, gdy chciałem automatycznie usunąć utworzone przez siebie pliki tymczasowe. Rozwiązanie, które wymyśliłem, nie polegało na użyciu podstawiania poleceń, ale raczej na przekazaniu do funkcji nazwy zmiennej, która powinna przyjąć ostateczny wynik. Na przykład

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

Tak więc w twoim przypadku byłoby to:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Działa i nie ma ograniczeń dotyczących „zwracanej wartości”.

Elmar Zander
źródło
1

Dzieje się tak, ponieważ podstawianie poleceń jest wykonywane w podpowłoce, więc podczas gdy podpowłoka dziedziczy zmienne, zmiany w nich są tracone po zakończeniu podpowłoki.

Odniesienie :

Zastępowanie poleceń, polecenia zgrupowane w nawiasach i polecenia asynchroniczne są wywoływane w środowisku podpowłoki, które jest duplikatem środowiska powłoki

Jakiś koleś programista
źródło
@JohnDoe Nie jestem pewien, czy to możliwe. Być może będziesz musiał przemyśleć swój projekt scenariusza.
Jakiś programista,
Och, ale muszę przypisać globalną tablicę w funkcji, jeśli nie, musiałbym powtórzyć dużo kodu (powtórzyć kod funkcji -30 wierszy- 15 razy -jeden na wywołanie-). Nie ma innego wyjścia, prawda?
harrison4
1

Rozwiązaniem tego problemu, bez konieczności wprowadzania złożonych funkcji i znacznej modyfikacji oryginalnej, jest przechowywanie wartości w pliku tymczasowym i odczyt / zapis w razie potrzeby.

To podejście bardzo mi pomogło, gdy musiałem kpić z funkcji basha wywoływanej wielokrotnie w przypadku testowym nietoperzy.

Na przykład możesz mieć:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

Wadą jest to, że możesz potrzebować wielu plików tymczasowych dla różnych zmiennych. Może być również konieczne wydanie syncpolecenia utrwalenia zawartości dysku między operacjami zapisu i odczytu.

Fabio
źródło
-1

Zawsze możesz użyć aliasu:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Dino Dini
źródło