Serializza la variabile shell in bash o zsh


12

C'è un modo per serializzare una variabile di shell? Supponiamo di avere una variabile $VARe di poter essere in grado di salvarla su un file o qualsiasi altra cosa e poi rileggerla più tardi per ottenere lo stesso valore?

Esiste un modo portatile per farlo? (Io non la penso così)

C'è un modo per farlo in bash o zsh?


2
Attenzione: la versione della mia risposta che hai accettato l'altro giorno ha avuto un grave problema che si romperà in alcuni scenari. L'ho riscritto per includere correzioni (e aggiungere funzionalità) e dovresti davvero rileggerlo da zero e portare il tuo codice per utilizzare la versione fissa.
Caleb,

^ Un altro ^ esempio della straordinaria cittadinanza di @Caleb.
Mikeserv,

Risposte:


14

Avvertenza: con una di queste soluzioni, è necessario essere consapevoli della fiducia nell'integrità dei file di dati, poiché verranno eseguiti come codice shell nello script. Proteggerli è fondamentale per la sicurezza del tuo script!

Implementazione in linea semplice per serializzare una o più variabili

Sì, sia in bash che in zsh è possibile serializzare i contenuti di una variabile in un modo che è facile da recuperare usando il typesetbuiltin e l' -pargomento. Il formato di output è tale che puoi semplicemente sourceottenere l'output per recuperare le tue cose.

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

Puoi recuperare le tue cose in questo modo o più tardi nella tua sceneggiatura o in un'altra sceneggiatura del tutto:

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

Questo funzionerà per bash, zsh e ksh incluso il passaggio di dati tra diverse shell. Bash tradurrà questo nella sua declarefunzione incorporata mentre zsh lo implementa con typesetma poiché bash ha un alias per farlo funzionare in entrambi i modi perché usiamo typesetqui per la compatibilità di ksh.

Implementazione generalizzata più complessa tramite funzioni

L'implementazione di cui sopra è davvero semplice, ma se la chiami frequentemente potresti voler darti una funzione di utilità per renderlo più facile. Inoltre, se si tenta di includere le funzioni personalizzate sopra descritte, si verificheranno problemi con l'ambito variabile. Questa versione dovrebbe eliminare tali problemi.

Nota per tutti questi, al fine di mantenere la compatibilità incrociata bash / zsh sistemeremo entrambi i casi typesete declarequindi il codice dovrebbe funzionare in una o entrambe le shell. Questo aggiunge un po 'di massa e confusione che potrebbero essere eliminati se lo facessi solo per una shell o un'altra.

Il problema principale nell'uso di funzioni per questo (o nell'inclusione del codice in altre funzioni) è che la typesetfunzione genera codice che, quando viene ricondotto in uno script dall'interno di una funzione, viene automaticamente impostato sulla creazione di una variabile locale anziché globale.

Questo può essere risolto con uno dei numerosi hack. Il mio tentativo iniziale di risolvere questo problema è stato analizzare l'output del processo di serializzazione sedper aggiungere il -gflag, in modo che il codice creato definisca una variabile globale al momento del ritorno.

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

Nota che l' sedespressione funky deve corrispondere solo alla prima occorrenza di 'typeset' o 'declare' e aggiungerla -gcome primo argomento. È necessario corrispondere solo alla prima occorrenza perché, come ha giustamente sottolineato Stéphane Chazelas nei commenti, corrisponderà anche ai casi in cui la stringa serializzata contiene nuove righe letterali seguite dalla parola dichiarare o comporre.

Oltre a correggere il mio falso passo di analisi iniziale , Stéphane ha anche suggerito un modo meno fragile per hackerare ciò che non solo affronta i problemi con l'analisi delle stringhe ma potrebbe essere un gancio utile per aggiungere funzionalità aggiuntive utilizzando una funzione wrapper per ridefinire le azioni acquisito durante il reperimento dei dati. Ciò presuppone che non si stia giocando ad altri giochi con i comandi declare o comporre, ma questa tecnica sarebbe più semplice da implementare in una situazione in cui si includeva questa funzionalità come parte di un'altra funzione propria o non avevi il controllo dei dati scritti e se era stato -gaggiunto o meno il flag. Qualcosa di simile potrebbe essere fatto anche con gli alias, vedere la risposta di Gilles per un'implementazione.

Per rendere il risultato ancora più utile, possiamo iterare su più variabili passate alle nostre funzioni assumendo che ogni parola nell'array degli argomenti sia un nome di variabile. Il risultato diventa qualcosa del genere:

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
}

Con entrambe le soluzioni, l'utilizzo sarebbe simile al seguente:

# 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"

declareè l' bashequivalente di ksh's typeset. bash, zshanche il supporto typesetin tal senso, typesetè più portatile. export -pè POSIX, ma non accetta alcun argomento e il suo output dipende dalla shell (sebbene sia ben specificato per le shell POSIX, quindi ad esempio quando bash o ksh è chiamato come sh). Ricorda di citare le tue variabili; usare qui l'operatore split + glob non ha senso.
Stéphane Chazelas,

Si noti che si -Etrova solo in alcuni BSD sed. I valori delle variabili possono contenere caratteri di nuova riga, pertanto sed 's/^.../.../'non è garantito il corretto funzionamento.
Stéphane Chazelas,

Questo e 'esattamente quello che stavo cercando! Volevo un modo conveniente per spostare le variabili avanti e indietro tra le shell.
fwenom,

Intendevo: a=$'foo\ndeclare bar' bash -c 'declare -p a'per l'installazione verrà generata una riga che inizia con declare. Probabilmente è meglio fare declare() { builtin declare -g "$@"; }prima di chiamare source(e disinserirlo in seguito)
Stéphane Chazelas,

2
@Gilles, gli alias non funzionerebbero all'interno delle funzioni (devono essere definiti al momento della definizione della funzione), e con bash ciò significherebbe che avresti bisogno di fare un shopt -s expandaliasquando non interattivo. Con le funzioni, puoi anche migliorare il declarewrapper in modo che ripristini solo le variabili specificate.
Stéphane Chazelas,

3

Usa reindirizzamento, sostituzione dei comandi ed espansione dei parametri. Le virgolette doppie sono necessarie per conservare spazi bianchi e caratteri speciali. Il trailing xsalva le nuove righe finali che sarebbero altrimenti rimosse nella sostituzione del comando.

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

Probabilmente vuole salvare anche il nome della variabile nel file.
user80551

2

Serializza tutto - POSIX

In qualsiasi shell POSIX, è possibile serializzare tutte le variabili di ambiente con export -p. Questo non include variabili shell non esportate. L'output viene correttamente citato in modo da poterlo rileggere nella stessa shell e ottenere esattamente gli stessi valori delle variabili. L'output potrebbe non essere leggibile in un'altra shell, ad esempio ksh usa la $'…'sintassi non POSIX .

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

Serializza alcuni o tutti - ksh, bash, zsh

Ksh (sia pdksh / mksh che ATT ksh), bash e zsh forniscono una struttura migliore con l' typesetintegrato. typeset -pstampa tutte le variabili definite e i loro valori (zsh omette i valori delle variabili che sono state nascoste con typeset -H). L'output contiene un'adeguata dichiarazione in modo che le variabili di ambiente vengano esportate al momento della rilettura (ma se una variabile è già esportata al momento della rilettura, non verrà esportata), in modo che gli array vengano riletti come array, ecc. Anche qui l'output è correttamente citato ma è garantito che sia leggibile solo nella stessa shell. È possibile passare un set di variabili da serializzare sulla riga di comando; se non si passa alcuna variabile, tutte sono serializzate.

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

In bash e zsh, il ripristino non può essere eseguito da una funzione perché le typesetistruzioni all'interno di una funzione sono incluse in quella funzione. È necessario eseguire . ./some_varsnel contesto in cui si desidera utilizzare i valori delle variabili, facendo attenzione che le variabili che erano globali quando esportate verranno dichiarate globali. Se si desidera rileggere i valori all'interno di una funzione ed esportarli, è possibile dichiarare un alias o una funzione temporanea. In zsh:

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

In bash (che usa declarepiuttosto che typeset):

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

In ksh, typesetdichiara le variabili locali nelle funzioni definite con function function_name { … }e le variabili globali nelle funzioni definite con function_name () { … }.

Serializza alcuni - POSIX

Se si desidera un maggiore controllo, è possibile esportare manualmente il contenuto di una variabile. Per stampare esattamente il contenuto di una variabile in un file, usa il printfcomando incorporato ( echoha alcuni casi speciali come echo -nsu alcune shell e aggiunge una nuova riga):

printf %s "$VAR" >VAR.content

Puoi rileggerlo con $(cat VAR.content), tranne per il fatto che la sostituzione del comando rimuove le nuove righe finali. Per evitare questa ruga, fare in modo che l'output non finisca mai con una nuova riga.

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

Se si desidera stampare più variabili, è possibile citarle con virgolette singole e sostituire tutte le virgolette singole incorporate con '\''. Questa forma di quotazione può essere letta in qualsiasi shell in stile Bourne / POSIX. Il frammento seguente funziona in qualsiasi shell POSIX. Funziona solo con variabili stringa (e variabili numeriche nelle shell che le hanno, anche se verranno lette come stringhe), non cerca di gestire le variabili di matrice nelle shell che le hanno.

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
}

Ecco un altro approccio che non effettua il fork di un sottoprocesso ma è più pesante sulla manipolazione delle stringhe.

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
}

Si noti che sulle shell che consentono variabili di sola lettura, si otterrà un errore se si tenta di rileggere una variabile di sola lettura.


Questo porta in variabili come $PWDe $_- vedi sotto i tuoi commenti.
Mikeserv,

@Caleb Che ne dici di fare typesetun alias per typeset -g?
Gilles 'SO- smetti di essere malvagio' il

@Gilles Ci ho pensato dopo che Stephanie ha suggerito il metodo della funzione, ma non ero sicuro di come impostare in modo portabile le opzioni di espansione dell'alias necessarie tra le shell. Forse potresti inserirlo nella tua risposta come una valida alternativa alla funzione che ho incluso.
Caleb,

0

Molto grazie a @ Stéphane-Chazelas che indicavano tutti i problemi con i miei tentativi precedenti, adesso sembra funzionare a puntate una matrice a stdout o in una variabile.

Questa tecnica non analizza shell l'input (diversamente da declare -a/ declare -p) ed è quindi sicura contro l'inserimento dannoso di metacaratteri nel testo serializzato.

Nota: le nuove righe non vengono salvate, poiché readelimina la \<newlines>coppia di caratteri, pertanto -d ...devono essere passate per la lettura e quindi le nuove righe non salvate vengono conservate.

Tutto questo è gestito nella unserialisefunzione.

Vengono utilizzati due personaggi magici, il separatore di campo e il separatore di record (in modo che più array possano essere serializzati nello stesso flusso).

Questi caratteri possono essere definiti come FSe RSma nessuno dei due può essere definito come newlinecarattere perché una nuova riga di escape viene eliminata da read.

Il personaggio di escape deve essere \una barra rovesciata, in quanto è ciò che viene utilizzato readper evitare che il personaggio venga riconosciuto come IFSpersonaggio.

serialiseserializzerà "$@"su stdout, serialise_toserializzerà sulla variabile nominata in$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
}

e non serializzare con:

unserialise data # read from stdin

o

unserialise data "$serialised_data" # from args

per esempio

$ 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:

(senza una nuova riga finale)

rileggilo:

$ 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

o

unserialise array # read from stdin

Bash readrispetta il carattere di escape \(a meno che non passi la bandiera -r) per rimuovere il significato speciale di caratteri come per la separazione del campo di input o la delimitazione di linee.

Se si desidera serializzare un array anziché un semplice elenco di argomenti, passare semplicemente l'array come elenco di argomenti:

serialise_array "${my_array[@]}"

Puoi usarlo unserialisein un ciclo come faresti readperché è solo una lettura chiusa - ma ricorda che il flusso non è separato da una nuova riga:

while unserialise array
do ...
done

Non funziona se gli elementi contengono non stampabili (nella locale corrente) o controllano caratteri come TAB o newline come allora bashe zshli rendono come $'\xxx'. Prova con bash -c $'printf "%q\n" "\t"'obash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas il

maledizione tootin, hai ragione! Modificherò la mia risposta per non usare printf% q ma $ {@ // .. / ..} iterazioni per sfuggire invece allo spazio bianco
Sam Liddicott

Tale soluzione dipende $IFSdall'essere non modificata e ora non riesce a ripristinare correttamente gli elementi dell'array vuoti. In effetti, avrebbe più senso usare un valore diverso di IFS e usarlo -d ''per evitare di fuggire da newline. Ad esempio, utilizzare :come separatore di campo e sfuggire solo a quello e alla barra rovesciata e utilizzare IFS=: read -ad '' arrayper l'importazione.
Stéphane Chazelas,

Sì .... Ho dimenticato il trattamento speciale di collasso degli spazi bianchi quando usato come separatore di campo in lettura. Sono contento che tu sia sulla palla oggi! Hai ragione su -d "" per evitare di scappare \ n, ma nel mio caso volevo leggere un flusso di serializzazioni - adatterò comunque la risposta. Grazie!
Sam Liddicott,

L'escaping di newline non consente di conservarlo, lo fa andare via una volta read. backslash-newline per readè un modo per continuare una linea logica su un'altra linea fisica. Modifica: ah ti vedo menzionare già il problema con Newline.
Stéphane Chazelas,

0

Puoi usare 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

Un altro modo per farlo è assicurarti di gestire tutte le 'hardquotes in questo modo:

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

O con export:

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

La prima e la seconda opzione funzionano in qualsiasi shell POSIX, supponendo che il valore della variabile non contenga la stringa:

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

La terza opzione dovrebbe funzionare per qualsiasi shell POSIX ma potrebbe tentare di definire altre variabili come _o PWD. La verità è che le uniche variabili che potrebbe tentare di definire sono impostate e gestite dalla shell stessa - e quindi anche se si importa exportil valore per ognuna di esse - come $PWDad esempio - la shell semplicemente le reimposterà su il valore corretto immediatamente comunque - prova a fare PWD=any_valuee vedi di persona.

E poiché - almeno con GNU bash- l'output di debug viene automaticamente quotato in modo sicuro per il re-input nella shell, questo funziona indipendentemente dal numero di 'virgolette in "$VAR":

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

$VAR in seguito può essere impostato sul valore salvato in qualsiasi script in cui il seguente percorso è valido con:

. ./VAR.file

Non sono sicuro di cosa hai provato a scrivere nel primo comando. $$è il PID della shell in esecuzione, hai trovato la quotazione sbagliata e cattiva \$o qualcosa del genere? L'approccio di base dell'utilizzo di un documento qui potrebbe essere fatto funzionare, ma è un materiale complicato, non a una riga: qualunque cosa scegliate come marcatore finale, dovete scegliere qualcosa che non appare nella stringa.
Gilles 'SO- smetti di essere malvagio' il

Il secondo comando non funziona quando $VARcontiene %. Il terzo comando non funziona sempre con valori contenenti più righe (anche dopo aver aggiunto le doppie virgolette ovviamente mancanti).
Gilles 'stop SO-essere malvagio'

@Gilles - So che è il pid - L'ho usato come semplice fonte di impostazione di un delimitatore unico. Cosa intendi con "non sempre" esattamente? E non capisco cosa mancano le doppie virgolette: tutte sono assegnazioni variabili. Le virgolette doppie confondono la situazione solo in quel contesto.
Mikeserv,

@Gilles - Ritiro la cosa del compito - questo è un argomento env. Sono ancora curioso di sapere cosa intendi per le linee multiple - sedelimina tutte le linee fino a quando non si incontrano VAR=fino all'ultima - quindi tutte le linee $VARvengono passate. Potete per favore fornire un esempio che lo rompe?
Mikeserv,

Ah, mi scuso, il terzo metodo funziona (con la correzione del preventivo). Bene, supponendo che il nome della variabile (qui VAR) non sia cambiato PWDo _o forse altri definiti da alcune shell. Il secondo metodo richiede bash; il formato di output da -vnon è standardizzato (nessuno dei trattini dash, ksh93, mksh e zsh).
Gilles 'SO- smetti di essere malvagio' il

-2

Quasi uguale ma un po 'diverso:

Dalla tua sceneggiatura:

#!/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"

Questa volta è stato testato.


Vedo che non hai provato! La logica di base funziona, ma non è difficile. La parte difficile è citare le cose correttamente, e non stai facendo nulla di tutto ciò. Prova variabili i cui valori contenere ritorni a capo, ', *, ecc
Gilles 'SO-tappa è male'

echo "$LVALUE=\"$RVALUE\""dovrebbe mantenere anche le nuove righe e il risultato nel file cfg_ dovrebbe essere simile a: MY_VAR1 = "Line1 \ nLine 2" Quindi quando valuterà MY_VAR1 conterrà anche le nuove righe. Naturalmente potresti avere problemi se il valore memorizzato contiene se stesso "char. Ma anche questo potrebbe essere curato.
Vadimbog,

1
A proposito, perché votare verso il basso qualcosa che sta rispondendo correttamente alla domanda posta qui? Sopra funziona molto bene per me e usando ovunque nei miei script?
Vadimbog,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.