Serializuj zmienne powłoki w bash lub zsh


12

Czy jest jakiś sposób na serializację zmiennej powłoki? Załóżmy, że mam zmienną $VARi chcę móc zapisać ją w pliku lub czymkolwiek, a następnie odczytać ją później, aby odzyskać tę samą wartość?

Czy istnieje przenośny sposób na zrobienie tego? (Nie sądzę)

Czy istnieje sposób na zrobienie tego w bash lub zsh?


2
Uwaga: wersja mojej odpowiedzi, którą zaakceptowałeś innego dnia, miała poważny problem, który może się popsuć w niektórych scenariuszach. Ponownie napisałem go, aby zawierał poprawki (i dodawał funkcje) i powinieneś naprawdę przeczytać go od nowa i przenieść swój kod, aby użyć poprawionej wersji.
Caleb

^ Kolejny ^ przykład doskonałego obywatelstwa @ Caleb.
mikeserv

Odpowiedzi:


14

Ostrzeżenie: korzystając z któregokolwiek z tych rozwiązań, musisz mieć świadomość, że ufasz integralności plików danych, aby były bezpieczne, ponieważ zostaną one wykonane jako kod powłoki w skrypcie. Zabezpieczenie ich jest najważniejsze dla bezpieczeństwa twojego skryptu!

Prosta implementacja wbudowana do szeregowania jednej lub więcej zmiennych

Tak, zarówno w bash, jak i zsh można serializować zawartość zmiennej w sposób łatwy do odczytania za pomocą typesetwbudowanego -pargumentu. Format wyjściowy jest taki, że możesz po prostu sourcewyjść, aby odzyskać swoje rzeczy.

 # You have variable(s) $FOO and $BAR already with your stuff
 typeset -p FOO BAR > ./serialized_data.sh

Możesz odzyskać swoje rzeczy w ten sposób później w skrypcie lub w innym skrypcie:

# Load up the serialized data back into the current shell
source serialized_data.sh

Będzie to działać w przypadku bash, zsh i ksh, w tym przekazywania danych między różnymi powłokami. Bash przetłumaczy to na swoją wbudowaną declarefunkcję, podczas gdy zsh implementuje to za pomocą, typesetale ponieważ bash ma alias, aby działał tak czy inaczej, ponieważ używamy typesettutaj dla kompatybilności z ksh.

Bardziej złożone uogólnione wdrożenie za pomocą funkcji

Powyższa implementacja jest naprawdę prosta, ale jeśli często ją wywołujesz, możesz chcieć udostępnić sobie funkcję narzędziową, aby to ułatwić. Dodatkowo, jeśli kiedykolwiek spróbujesz zawrzeć powyższe funkcje niestandardowe, napotkasz problemy ze zmiennym zasięgiem. Ta wersja powinna wyeliminować te problemy.

Uwaga dla wszystkich z nich, w celu utrzymania bash / zsh przekrój kompatybilność będziemy mocowania zarówno przypadki typeset, a declarewięc kod powinien działać w jednej lub obu muszli. Dodaje to trochę bałaganu, który można by wyeliminować, gdybyś robił to tylko dla jednej powłoki.

Głównym problemem związanym z używaniem funkcji do tego (lub włączaniem kodu do innych funkcji) jest to, że typesetfunkcja generuje kod, który po przejściu do skryptu z wnętrza funkcji domyślnie tworzy zmienną lokalną, a nie globalną.

Można to naprawić za pomocą jednego z kilku hacków. Pierwszą próbą rozwiązania tego problemu było przeanalizowanie wyniku procesu serializacji w sedcelu dodania -gflagi, aby utworzony kod definiował zmienną globalną po ponownym przejściu.

serialize() {
    typeset -p "$1" | sed -E '0,/^(typeset|declare)/{s/ / -g /}' > "./serialized_$1.sh"
}
deserialize() {
    source "./serialized_$1.sh"
}

Zauważ, że funky sedwyrażenie ma pasować tylko do pierwszego wystąpienia „składu” lub „zadeklarować” i dodać -gjako pierwszy argument. Konieczne jest dopasowanie tylko pierwszego wystąpienia, ponieważ, jak słusznie zauważył Stéphane Chazelas w komentarzach, w przeciwnym razie dopasuje również przypadki, w których szeregowany ciąg zawiera dosłowne znaki nowej linii, a po nim słowo deklaruj lub skład.

Oprócz poprawienia mojego początkowego faux pasowania , Stéphane zasugerował również mniej kruchy sposób zhakowania tego, który nie tylko rozwiązuje problemy z parsowaniem łańcuchów, ale może być przydatnym hakiem, aby dodać dodatkową funkcjonalność za pomocą funkcji otoki w celu przedefiniowania działań pobierane przy ponownym pozyskiwaniu danych. Zakłada się, że nie grasz w żadną inną grę za pomocą poleceń deklarowania lub składu, ale tę technikę łatwiej byłoby wdrożyć w sytuacji, w której włączałeś tę funkcję jako część innej własnej lub nie kontrolowałeś zapisywanych danych i tego, czy -gdodano flagę. Coś podobnego można również zrobić z aliasami, zobacz implementację Gillesa .

Aby wynik był jeszcze bardziej użyteczny, możemy iterować wiele zmiennych przekazywanych do naszych funkcji, zakładając, że każde słowo w tablicy argumentów jest nazwą zmiennej. Wynik staje się mniej więcej taki:

serialize() {
    for var in $@; do
        typeset -p "$var" > "./serialized_$var.sh"
    done
}

deserialize() {
    declare() { builtin declare -g "$@"; }
    typeset() { builtin typeset -g "$@"; }
    for var in $@; do
        source "./serialized_$var.sh"
    done
    unset -f declare typeset
}

W przypadku obu rozwiązań użycie wyglądałoby tak:

# Load some test data into variables
FOO=(an array or something)
BAR=$(uptime)

# Save it out to our serialized data files
serialize FOO BAR

# For testing purposes unset the variables to we know if it worked
unset FOO BAR

# Load  the data back in from out data files
deserialize FOO BAR

echo "FOO: $FOO\nBAR: $BAR"

declarejest bashodpowiednikiem ksh„s” typeset. bash, zshrównież wsparcie, typesetwięc w tym względzie typesetjest bardziej przenośne. export -pjest POSIX, ale nie przyjmuje żadnego argumentu, a jego wynik zależy od powłoki (chociaż jest dobrze określony dla powłok POSIX, więc na przykład, gdy wywoływane jest bash lub ksh sh). Pamiętaj, aby podać swoje zmienne; użycie operatora split + glob tutaj nie ma sensu.
Stéphane Chazelas,

Uwaga: -Emożna go znaleźć tylko w niektórych BSD sed. Zmienne wartości mogą zawierać znaki nowego wiersza, więc sed 's/^.../.../'nie gwarantuje się, że będą działać poprawnie.
Stéphane Chazelas,

Właśnie tego szukałem! Chciałem w wygodny sposób przesuwać zmienne tam iz powrotem między powłokami.
fwenom

Miałem na myśli: a=$'foo\ndeclare bar' bash -c 'declare -p a'do instalacji wyświetli wiersz zaczynający się od declare. Prawdopodobnie lepiej to zrobić declare() { builtin declare -g "$@"; }przed zadzwonieniem source(a potem
rozbrojeniem

2
@Gilles, aliasy nie działałyby wewnątrz funkcji (należy je zdefiniować w momencie definicji funkcji), a przy bash oznaczałoby to, że musisz zrobić, shopt -s expandaliasgdy nie jest interaktywny. Za pomocą funkcji można również ulepszyć declareopakowanie, aby przywracało tylko określone zmienne.
Stéphane Chazelas

3

Użyj przekierowania, podstawiania poleceń i rozszerzania parametrów. Aby zachować białe znaki i znaki specjalne, potrzebne są podwójne cudzysłowy. Końcowe xzapisuje końcowe znaki nowej linii, które w innym przypadku zostałyby usunięte w podstawieniu polecenia.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}

Prawdopodobnie chce zapisać nazwę zmiennej również w pliku.
user80551

2

Serializuj wszystko - POSIX

W dowolnej powłoce POSIX można serializować wszystkie zmienne środowiskowe za pomocą export -p. Nie obejmuje to nieeksportowanych zmiennych powłoki. Dane wyjściowe są poprawnie cytowane, dzięki czemu można odczytać je z powrotem w tej samej powłoce i uzyskać dokładnie te same wartości zmiennych. Dane wyjściowe mogą nie być czytelne w innej powłoce, na przykład ksh używa $'…'składni innej niż POSIX .

save_environment () {
  export -p >my_environment
}
restore_environment () {
  . ./my_environment
}

Serializuj niektóre lub wszystkie - ksh, bash, zsh

Ksh (zarówno pdksh / mksh, jak i ATT ksh), bash i zsh zapewniają lepszą funkcjonalność dzięki typesetwbudowanemu. typeset -pwypisuje wszystkie zdefiniowane zmienne i ich wartości (zsh pomija wartości zmiennych, które zostały ukryte typeset -H). Dane wyjściowe zawierają odpowiednią deklarację, dzięki czemu zmienne środowiskowe są eksportowane podczas odczytu (ale jeśli zmienna jest już eksportowana podczas odczytu, nie zostanie wyeksportowana), dzięki czemu tablice są odczytywane jako tablice itp. Tutaj również dane wyjściowe jest poprawnie cytowany, ale gwarantuje się, że będzie czytelny tylko w tej samej powłoce. Możesz przekazać zestaw zmiennych do serializacji w wierszu poleceń; jeśli nie podasz żadnej zmiennej, wszystkie są serializowane.

save_some_variables () {
  typeset -p VAR OTHER_VAR >some_vars
}

W bash i zsh przywracania nie można wykonać z funkcji, ponieważ typesetinstrukcje wewnątrz funkcji są ograniczone do tej funkcji. Musisz uruchomić . ./some_varsw kontekście, w którym chcesz użyć wartości zmiennych, zwracając uwagę, aby zmienne, które były globalne podczas eksportu, zostały ponownie zadeklarowane jako globalne. Jeśli chcesz odczytać wartości w funkcji i wyeksportować je, możesz zadeklarować tymczasowy alias lub funkcję. W Zsh:

restore_and_make_all_global () {
  alias typeset='typeset -g'
  . ./some_vars
  unalias typeset
}

W bash (który używa declarezamiast typeset):

restore_and_make_all_global () {
  alias declare='declare -g'
  shopt -s expand_aliases
  . ./some_vars
  unalias declare
}

W ksh typesetdeklaruje zmienne lokalne w funkcjach zdefiniowanych za pomocą function function_name { … }i zmienne globalne w funkcjach zdefiniowanych za pomocą function_name () { … }.

Serializuj niektóre - POSIX

Jeśli chcesz mieć większą kontrolę, możesz ręcznie wyeksportować zawartość zmiennej. Aby wydrukować zawartość zmiennej dokładnie do pliku, użyj printfwbudowanego ( echoma kilka specjalnych przypadków, takich jak echo -nniektóre powłoki i dodaje nowy wiersz):

printf %s "$VAR" >VAR.content

Możesz to przeczytać ponownie za $(cat VAR.content)wyjątkiem tego, że podstawienie polecenia usuwa końcowe znaki nowej linii. Aby uniknąć tego zmarszczki, zadbaj o to, aby wydruk nigdy nie kończył się nową linią.

VAR=$(cat VAR.content && echo a)
if [ $? -ne 0 ]; then echo 1>&2 "Error reading back VAR"; exit 2; fi
VAR=${VAR%?}

Jeśli chcesz wydrukować wiele zmiennych, możesz zacytować je pojedynczymi cudzysłowami i zastąpić wszystkie osadzone pojedyncze cudzysłowy '\''. Tę formę cytowania można odczytać z powrotem w dowolnej powłoce w stylu Bourne / POSIX. Poniższy fragment kodu działa w dowolnej powłoce POSIX. Działa tylko w przypadku zmiennych łańcuchowych (i zmiennych numerycznych w powłokach, które je mają, chociaż będą odczytywane jako ciągi znaków), nie próbuje zajmować się zmiennymi tablicowymi w powłokach, które je zawierają.

serialize_variables () {
  for __serialize_variables_x do
    eval "printf $__serialize_variables_x=\\'%s\\'\\\\n \"\$${__serialize_variables_x}\"" |
    sed -e "s/'/'\\\\''/g" -e '1 s/=.../=/' -e '$ s/...$//'
  done
}

Oto inne podejście, które nie rozwidla podprocesu, ale jest trudniejsze w manipulowaniu łańcuchem.

serialize_variables () {
  for __serialize_variables_var do
    eval "__serialize_variables_tail=\${$__serialize_variables_var}"
    while __serialize_variables_quoted="$__serialize_variables_quoted${__serialize_variables_tail%%\'*}"
          [ "${__serialize_variables_tail%%\'*}" != "$__serialize_variables_tail" ]; do
      __serialize_variables_tail="${__serialize_variables_tail#*\'}"
      __serialize_variables_quoted="${__serialize_variables_quoted}'\\''"
    done
    printf "$__serialize_variables_var='%s'\n" "$__serialize_variables_quoted"
  done
}

Zauważ, że w powłokach, które dopuszczają zmienne tylko do odczytu, pojawi się błąd, jeśli spróbujesz odczytać zmienną tylko do odczytu.


Daje to zmienne takie jak $PWDi $_- proszę zobaczyć własne komentarze poniżej.
mikeserv

@Caleb Co powiesz na utworzenie typesetaliasu typeset -g?
Gilles „SO- przestań być zły”

@Gilles Pomyślałem o tym po tym, jak Stephanie zasugerowała metodę funkcji, ale nie byłem pewien, jak przenośnie ustawić niezbędne opcje rozwijania aliasów w powłokach. Może mógłbyś podać to w swojej odpowiedzi jako realną alternatywę dla funkcji, którą zawarłem.
Caleb

0

Wiele dzięki @ stéphane-chazelas, który wskazał wszystkie problemy z moimi poprzednimi próbami, wydaje się, że teraz działa to na serializację tablicy na standardowe wyjście lub na zmienną.

Ta technika nie analizuje danych wejściowych w powłoce (w przeciwieństwie do declare -a/ declare -p), a zatem jest bezpieczna przed złośliwym wstawianiem metaznaków w serializowanym tekście.

Uwaga: znaki nowej linii nie są poprzedzane znakami ucieczki, ponieważ readusuwa \<newlines>parę znaków, dlatego -d ...należy ją przekazać do odczytu, a następnie znaki nowej linii bez zmian są zachowywane.

Wszystko to jest zarządzane w unserialisefunkcji.

Wykorzystywane są dwie magiczne postacie, separator pól i separator rekordów (dzięki czemu wiele tablic może być szeregowanych do tego samego strumienia).

Znaki te można zdefiniować jako FSi RSżaden z nich nie może być zdefiniowany jako newlineznak, ponieważ znak nowej linii jest usuwany przez read.

Znak ucieczki musi być \odwrotnym ukośnikiem, ponieważ używa się go, readaby uniknąć rozpoznania postaci jako IFSznaku.

serialisebędzie serialise "$@"na standardowe wyjście, serialise_tobędzie serialise do varable w nazwie$1

serialise() {
  set -- "${@//\\/\\\\}" # \
  set -- "${@//${FS:-;}/\\${FS:-;}}" # ; - our field separator
  set -- "${@//${RS:-:}/\\${RS:-:}}" # ; - our record separator
  local IFS="${FS:-;}"
  printf ${SERIALIZE_TARGET:+-v"$SERIALIZE_TARGET"} "%s" "$*${RS:-:}"
}
serialise_to() {
  SERIALIZE_TARGET="$1" serialise "${@:2}"
}
unserialise() {
  local IFS="${FS:-;}"
  if test -n "$2"
  then read -d "${RS:-:}" -a "$1" <<<"${*:2}"
  else read -d "${RS:-:}" -a "$1"
  fi
}

i odserializować za pomocą:

unserialise data # read from stdin

lub

unserialise data "$serialised_data" # from args

na przykład

$ serialise "Now is the time" "For all good men" "To drink \$drink" "At the \`party\`" $'Party\tParty\tParty'
Now is the time;For all good men;To drink $drink;At the `party`;Party   Party   Party:

(bez końcowego znaku nowej linii)

przeczytaj to:

$ serialise_to s "Now is the time" "For all good men" "To drink \$drink" "At the \`party\`" $'Party\tParty\tParty'
$ unserialise array "$s"
$ echo "${array[@]/#/$'\n'}"

Now is the time 
For all good men 
To drink $drink 
At the `party` 
Party   Party   Party

lub

unserialise array # read from stdin

Bash readrespektuje znak zmiany znaczenia \(chyba że podasz flagę -r), aby usunąć specjalne znaczenie znaków, takie jak separacja pól wejściowych lub rozdzielanie linii.

Jeśli chcesz serializować tablicę zamiast zwykłej listy argumentów, po prostu przekaż tablicę jako listę argumentów:

serialise_array "${my_array[@]}"

Możesz używać unserialisew pętli tak, jak byś to zrobił, readponieważ jest to po prostu zawinięty odczyt - ale pamiętaj, że strumień nie jest oddzielony znakiem nowej linii:

while unserialise array
do ...
done

To nie działa jeśli elementy zawierają niedrukowalne (w bieżącej lokalizacji) lub znaków sterujących, takich jak TAB lub przełamane jak wtedy bashi zshuczynić je jako $'\xxx'. Spróbuj z bash -c $'printf "%q\n" "\t"'lubbash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas

cholerny tootin, masz rację! Zmodyfikuję moją odpowiedź, aby nie używać iteracji printf% q, ale $ {@ // .. / ..}, aby zamiast tego uniknąć białych
znaków

To rozwiązanie zależy od $IFSniezmodyfikowania, a teraz nie przywraca poprawnie pustych elementów tablicy. W rzeczywistości rozsądniej byłoby użyć innej wartości IFS i użyć, -d ''aby uniknąć konieczności ucieczki od nowej linii. Na przykład użyj :jako separatora pól i unikaj tego oraz ukośnika odwrotnego i użyj IFS=: read -ad '' arraydo importowania.
Stéphane Chazelas

Tak ... Zapomniałem o specjalnym traktowaniu zawalającej się białej przestrzeni, gdy używano go jako separatora pól w czytaniu. Cieszę się, że dzisiaj jesteś na balu! Masz rację co do -d "", aby uniknąć ucieczki \ n, ale w moim przypadku chciałem przeczytać strumień serializacji - dostosuję jednak odpowiedź. Dzięki!
Sam Liddicott

Ucieczka od nowej linii nie pozwala na jej zachowanie, sprawia, że ​​raz odejdzie read. backslash-newline for readjest sposobem na kontynuację linii logicznej do innej linii fizycznej. Edycja: ah Widzę, że wspominałeś już o problemie z nową linią.
Stéphane Chazelas

0

Możesz użyć base64:

$ VAR="1/ 
,x"
$ echo "$VAR" | base64 > f
$ VAR=$(cat f | base64 -d)
$ echo "${VAR}X"
1/ 
,xX

-2
printf 'VAR=$(cat <<\'$$VAR$$'\n%s\n'$$VAR$$'\n)' "$VAR" >./VAR.file

Innym sposobem na to jest zapewnienie obsługi wszystkich 'takich cytatów:

sed '"s/'"'/&"&"&/g;H;1h;$!d;g;'"s/.*/VAR='&'/" <<$$VAR$$ >./VAR.file
$VAR
$$VAR$$

Lub z export:

env - "VAR=$VAR" sh -c 'export -p' >./VAR.file 

Pierwsza i druga opcja działają w dowolnej powłoce POSIX, przy założeniu, że wartość zmiennej nie zawiera łańcucha:

"\n${CURRENT_SHELLS_PID}VAR${CURRENT_SHELLS_PID}\n" 

Trzecia opcja powinna działać dla dowolnej powłoki POSIX, ale może próbować zdefiniować inne zmienne, takie jak _lub PWD. Prawda jest jednak taka, że ​​jedyne zmienne, które może próbować zdefiniować, są ustawiane i utrzymywane przez samą powłokę - a więc nawet jeśli importujesz exportwartość dla dowolnej z nich - $PWDna przykład - powłoka po prostu zresetuje je do i tak poprawna wartość natychmiast - spróbuj zrobić PWD=any_valuei przekonaj się sam.

A ponieważ - przynajmniej w GNU bash- wyniki debugowania są automatycznie bezpiecznie cytowane w celu ponownego wprowadzenia do powłoki, działa to niezależnie od liczby 'ciężkich cytatów w "$VAR":

 PS4= VAR=$VAR sh -cx 'VAR=$VAR' 2>./VAR.file

$VAR można później ustawić na zapisaną wartość w dowolnym skrypcie, w którym poprawna jest następująca ścieżka:

. ./VAR.file

Nie jestem pewien, co próbowałeś napisać w pierwszym poleceniu. $$jest PID działającej powłoki, czy źle napisałeś cytat i \$czy coś takiego? Można zastosować podstawowe podejście do używania dokumentu tutaj, ale jest to trudny, a nie jednowarstwowy materiał: cokolwiek wybierzesz jako znacznik końcowy, musisz wybrać coś, co nie pojawia się w ciągu.
Gilles „SO- przestań być zły”

Drugie polecenie nie działa, gdy $VARzawiera %. Trzecie polecenie nie zawsze działa z wartościami zawierającymi wiele wierszy (nawet po dodaniu oczywiście brakujących podwójnych cudzysłowów).
Gilles „SO- przestań być zły”

@Gilles - Wiem, że to pid - użyłem go jako prostego źródła ustawienia unikalnego ogranicznika. Co rozumiesz przez „nie zawsze” dokładnie? I nie rozumiem, czego brakuje podwójnym cudzysłowom - wszystkie są przypisaniami zmiennych. Podwójne cudzysłowy tylko mylą sytuację w tym kontekście.
mikeserv

@Gilles - Cofam zadanie - to argument env. Nadal jestem ciekawy, co masz na myśli mówiąc o wielu liniach - sedusuwa każdą linię do momentu napotkania VAR=do ostatniej - więc wszystkie linie $VARsą przekazywane. Czy możesz podać przykład, który go łamie?
mikeserv

Przepraszam, trzecia metoda działa (z poprawką cytowania). Dobrze, przyjmując nazwę zmiennej (tutaj VAR) nie ulega zmianie PWDlub _czy może innym, że niektóre powłoki zdefiniowania. Druga metoda wymaga bash; format wyjściowy z -vnie jest ustandaryzowany (żadne z dash, ksh93, mksh i zsh nie działają).
Gilles „SO- przestań być zły”

-2

Prawie takie same, ale nieco inne:

Ze skryptu:

#!/usr/bin/ksh 

save_var()
{

    (for ITEM in $*
    do
        LVALUE='${'${ITEM}'}'
        eval RVALUE="$LVALUE"
        echo "$ITEM=\"$RVALUE\""  
    done) >> $cfg_file
}

restore_vars()
{
    . $cfg_file
}

cfg_file=config_file
MY_VAR1="Test value 1"
MY_VAR2="Test 
value 2"

save_var MY_VAR1 MY_VAR2
MY_VAR1=""
MY_VAR2=""

restore_vars 

echo "$MY_VAR1"
echo "$MY_VAR2"

Ten czas powyżej jest testowany.


Widzę, że nie testowałeś! Podstawowa logika działa, ale to nie jest trudne. Trudnym zadaniem jest prawidłowe cytowanie rzeczy i nic z tego nie robisz. Spróbuj zmienne, których wartości zawierają znaki nowej linii, ', *, itd.
Gilles „SO- przystanek jest zła”

echo "$LVALUE=\"$RVALUE\""ma również zachowywać nowe wiersze, a wynik w pliku cfg_ powinien wyglądać następująco: MY_VAR1 = "Line1 \ nLine 2" Tak więc, kiedy eval MY_VAR1 będzie zawierał również nowe wiersze. Oczywiście możesz mieć problemy, jeśli przechowywana wartość zawiera "char. Ale można to również załatwić.
vadimbog

1
Przy okazji, po co głosować w dół na coś, co poprawnie odpowiada na pytanie tutaj zadane? Powyższe działa dla mnie bardzo dobrze i używasz wszędzie w moich skryptach?
vadimbog
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.