Serialize variável de shell no bash ou zsh

12

Existe alguma maneira de serializar uma variável de shell? Suponha que eu tenha uma variável $VARe que queira salvá-la em um arquivo ou qualquer outra coisa e, em seguida, leia novamente mais tarde para recuperar o mesmo valor?

Existe uma maneira portátil de fazer isso? (Acho que não)

Existe uma maneira de fazer isso no bash ou no zsh?

fwenom
fonte
2
Atenção: a versão da minha resposta que você aceitou no outro dia teve um problema sério que será interrompido em alguns cenários. Eu o reescrevi para incluir correções (e adicionar recursos) e você realmente deve relê-lo do zero e portar seu código para usar a versão fixa.
Caleb
^ Outro ^ exemplo da cidadania destacada de @ Caleb.
mikeserv

Respostas:

14

Aviso: Com qualquer uma dessas soluções, você precisa estar ciente de que está confiando na integridade dos arquivos de dados, pois eles serão executados como código de shell em seu script. Protegê-los é fundamental para a segurança do seu script!

Implementação simples em linha para serializar uma ou mais variáveis

Sim, no bash e no zsh, você pode serializar o conteúdo de uma variável de uma maneira fácil de recuperar usando o typesetbuiltin e o -pargumento. O formato de saída é tal que você pode simplesmente sourcea saída para recuperar suas coisas.

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

Você pode recuperar suas coisas assim mais tarde em seu script ou em outro script completamente:

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

Isso funcionará para o bash, zsh e ksh, incluindo a passagem de dados entre diferentes shells. O Bash traduzirá isso para sua declarefunção embutida, enquanto o zsh implementa isso com typesetmas o bash tem um alias para que isso funcione de qualquer maneira, pois usamos typesetaqui para compatibilidade com o ksh.

Implementação generalizada mais complexa usando funções

A implementação acima é realmente simples, mas se você chamá-la com frequência, pode querer oferecer uma função de utilitário para facilitar. Além disso, se você tentar incluir as funções personalizadas internas acima, encontrará problemas com o escopo da variável. Esta versão deve eliminar esses problemas.

Observe tudo isso: para manter a compatibilidade cruzada do bash / zsh, corrigiremos os dois casos typesete, declareportanto, o código deve funcionar em um ou ambos os shells. Isso adiciona algum volume e confusão que poderiam ser eliminados se você estivesse fazendo isso apenas para um shell ou outro.

O principal problema com o uso de funções para isso (ou incluindo o código em outras funções) é que a typesetfunção gera código que, quando originado em um script de dentro de uma função, o padrão é criar uma variável local em vez de global.

Isso pode ser corrigido com um dos vários hacks. Minha tentativa inicial de corrigir isso foi analisar a saída do processo de serialização sedpara adicionar o -gsinalizador, para que o código criado defina uma variável global quando retornado.

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

Observe que a sedexpressão funky deve corresponder apenas à primeira ocorrência de 'typeset' ou 'declare' e adicionar -gcomo primeiro argumento. É necessário corresponder apenas à primeira ocorrência, porque, como Stéphane Chazelas apontou corretamente nos comentários, também corresponderá aos casos em que a seqüência de caracteres serializada contenha novas linhas literais seguidas pela palavra declarar ou digitar.

Além de corrigir meu falso passo de análise inicial , Stéphane também sugeriu uma maneira menos frágil de hackear isso, que além de solucionar os problemas de análise de seqüências de caracteres, poderia ser um gancho útil para adicionar funcionalidade adicional usando uma função de invólucro para redefinir as ações tomada ao fornecer os dados de volta. Isso pressupõe que você não esteja jogando nenhum outro jogo com os comandos declare ou typeset, mas essa técnica seria mais fácil de implementar em uma situação em que você estivesse incluindo essa funcionalidade como parte de outra função própria ou você não estava no controle dos dados que estavam sendo gravados e se o -gsinalizador foi adicionado ou não . Algo semelhante também pode ser feito com aliases, consulte a resposta de Gilles para uma implementação.

Para tornar o resultado ainda mais útil, podemos iterar várias variáveis ​​passadas para nossas funções assumindo que cada palavra na matriz de argumentos seja um nome de variável. O resultado se torna algo como isto:

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
}

Com qualquer solução, o uso ficaria assim:

# 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"
Caleb
fonte
declareé o bashequivalente de ksh's typeset. bash, zshtambém suporte typeset, nesse sentido, typeseté mais portátil. export -pé POSIX, mas não requer nenhum argumento e sua saída depende do shell (embora seja bem especificado para shells POSIX, por exemplo, quando bash ou ksh é chamado como sh). Lembre-se de citar suas variáveis; usar o operador split + glob aqui não faz sentido.
Stéphane Chazelas
Observe que isso -Eé encontrado apenas em alguns BSDs sed. Os valores variáveis ​​podem conter caracteres de nova linha, portanto, sed 's/^.../.../'não é garantido que funcione corretamente.
Stéphane Chazelas
Era exatamente isso que eu estava procurando! Eu queria uma maneira conveniente de empurrar variáveis ​​entre as conchas.
fwenom
Eu quis dizer: a=$'foo\ndeclare bar' bash -c 'declare -p a'for install produzirá uma linha que começa com declare. Provavelmente é melhor fazer declare() { builtin declare -g "$@"; }antes de telefonar source(e desabilitar depois) #
Stéphane Chazelas 16/06/2014
2
@Gilles, os aliases não funcionariam dentro das funções (precisam ser definidas no momento da definição da função) e, com o bash, isso significaria que você precisaria fazer um shopt -s expandaliasquando não interativo. Com as funções, você também pode aprimorar o declarewrapper para restaurar apenas as variáveis ​​especificadas.
Stéphane Chazelas
3

Use o redirecionamento, substituição de comandos e expansão de parâmetros. São necessárias aspas duplas para preservar espaços em branco e caracteres especiais. O final xsalva as novas linhas finais, que seriam removidas na substituição de comando.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}
choroba
fonte
Ele provavelmente deseja salvar o nome da variável também no arquivo.
user80551
2

Serializar tudo - POSIX

Em qualquer shell POSIX, você pode serializar todas as variáveis ​​de ambiente com export -p. Isso não inclui variáveis ​​de shell não exportadas. A saída é citada corretamente para que você possa lê-la novamente no mesmo shell e obter exatamente os mesmos valores de variável. A saída pode não ser legível em outro shell, por exemplo, o ksh usa a $'…'sintaxe não POSIX .

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

Serialize alguns ou todos - ksh, bash, zsh

Ksh (pdksh / mksh e ATT ksh), bash e zsh fornecem uma instalação melhor com o typesetbuiltin. typeset -pimprime todas as variáveis ​​definidas e seus valores (zsh omite os valores das variáveis ​​que foram ocultadas typeset -H). A saída contém uma declaração adequada para que as variáveis ​​de ambiente sejam exportadas quando lidas novamente (mas se uma variável já for exportada quando lidas novamente, não será exportada), para que as matrizes sejam lidas novamente como matrizes, etc. Aqui também, a saída é citado corretamente, mas é garantido apenas para leitura no mesmo shell. Você pode passar um conjunto de variáveis ​​para serializar na linha de comando; se você não passar nenhuma variável, todas serão serializadas.

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

No bash e no zsh, a restauração não pode ser feita a partir de uma função porque as typesetinstruções dentro de uma função têm escopo definido para essa função. Você precisa executar . ./some_varsno contexto em que deseja usar os valores das variáveis, cuidando para que as variáveis ​​globais quando exportadas sejam declaradas como globais. Se você quiser ler novamente os valores em uma função e exportá-los, poderá declarar um alias ou função temporário. No zsh:

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

No bash (que usa em declarevez de typeset):

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

Em ksh, typesetdeclara variáveis ​​locais em funções definidas com function function_name { … }e variáveis ​​globais em funções definidas com function_name () { … }.

Serialize alguns - POSIX

Se você deseja mais controle, pode exportar o conteúdo de uma variável manualmente. Para imprimir o conteúdo de uma variável exatamente em um arquivo, use o printfbuiltin ( echopossui alguns casos especiais, como echo -nem algumas conchas e adiciona uma nova linha):

printf %s "$VAR" >VAR.content

Você pode ler isso de volta com $(cat VAR.content), exceto que a substituição de comando retira as novas linhas à direita. Para evitar esse enrugamento, organize a saída para nunca terminar com uma nova linha.

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

Se você deseja imprimir várias variáveis, pode citá-las entre aspas simples e substituir todas as aspas simples incorporadas por '\''. Essa forma de citação pode ser lida novamente em qualquer shell no estilo Bourne / POSIX. O seguinte trecho funciona em qualquer shell POSIX. Ele funciona apenas para variáveis ​​de string (e variáveis ​​numéricas em shells que as possuem, embora elas sejam lidas como strings), mas não tenta lidar com variáveis ​​de array em shells que as possuem.

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
}

Aqui está outra abordagem que não bifurca um subprocesso, mas é mais pesada na manipulação de strings.

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
}

Observe que nos shells que permitem variáveis ​​somente leitura, você receberá um erro se tentar ler novamente uma variável que é somente leitura.

Gilles 'SO- parar de ser mau'
fonte
Isso traz variáveis ​​como $PWDe $_- por favor, veja seus próprios comentários abaixo.
mikeserv
@Caleb Que tal criar typesetum apelido para typeset -g?
Gilles 'SO- stop be evil'
@ Gilles Pensei nisso depois que Stephanie sugeriu o método de função, mas não sabia como definir de maneira portável o alias necessário para expandir as opções através de shells. Talvez você possa colocar isso em sua resposta como uma alternativa viável à função que incluí.
Caleb
0

Muito obrigado a @ stéphane-chazelas, que apontou todos os problemas com minhas tentativas anteriores, agora parece funcionar para serializar uma matriz para stdout ou para uma variável.

Essa técnica não analisa shell de entrada (diferente de declare -a/ declare -p) e, portanto, é segura contra a inserção mal-intencionada de metacaracteres no texto serializado.

Nota: as novas linhas não são escapadas, porque readexclui o \<newlines>par de caracteres; portanto, -d ...devem ser passadas para leitura e as novas linhas sem escape são preservadas.

Tudo isso é gerenciado na unserialisefunção.

Dois caracteres mágicos são usados, o separador de campos e o separador de registros (para que várias matrizes possam ser serializadas no mesmo fluxo).

Esses caracteres podem ser definidos como FSe, RSmas nenhum deles pode ser definido como newlinecaractere porque uma nova linha de escape é excluída por read.

O caractere de escape deve ser \a barra invertida, pois é isso que é usado readpara evitar que o caractere seja reconhecido como um IFScaractere.

serialiseserializará "$@"para stdout, serialise_toserializará para a variável nomeada em$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 desserializar com:

unserialise data # read from stdin

ou

unserialise data "$serialised_data" # from args

por exemplo

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

(sem uma nova linha à direita)

leia de volta:

$ 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

ou

unserialise array # read from stdin

O Bash readrespeita o caractere de escape \(a menos que você passe o sinalizador -r) para remover um significado especial de caracteres, como separação de campos de entrada ou delimitação de linhas.

Se você deseja serializar uma matriz em vez de uma mera lista de argumentos, basta passar sua matriz como a lista de argumentos:

serialise_array "${my_array[@]}"

Você pode usar unserialiseem um loop como faria readporque é apenas uma leitura agrupada - mas lembre-se de que o fluxo não é separado por nova linha:

while unserialise array
do ...
done
Sam Liddicott
fonte
Não funciona se os elementos contiverem caracteres não imprimíveis (no código do idioma atual) ou de controle como TAB ou nova linha bashe zshrenderizá-los como $'\xxx'. Tente com bash -c $'printf "%q\n" "\t"'oubash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas 29/04
danado tootin, você está certo! Modificarei minha resposta para não usar as printf% q, mas as iterações $ {@ // .. / ..} para escapar do espaço em branco
Sam Liddicott 29/04/16
Essa solução depende de $IFSnão ser modificada e agora falha ao restaurar os elementos vazios da matriz corretamente. De fato, faria mais sentido usar um valor diferente do IFS e -d ''evitar o escape da nova linha. Por exemplo, use :como o separador de campos e apenas escape isso e a barra invertida e use IFS=: read -ad '' arraypara importar.
Stéphane Chazelas 29/04
Sim ... Eu esqueci o tratamento especial em colapso do espaço em branco quando usado como separador de campo na leitura. Estou feliz que você esteja na bola hoje! Você está certo sobre -d "" para evitar escapar \ n, mas no meu caso eu queria ler uma série de serializações - embora eu adapte a resposta. Obrigado!
Sam Liddicott
Escapar da nova linha não permite que ela seja preservada, mas desaparece uma vez read. backslash-newline for readé uma maneira de continuar uma linha lógica em outra linha física. Edit: ah eu vejo você mencionar o problema com a nova linha já.
Stéphane Chazelas
0

Você poderia usar base64:

$ VAR="1/ 
,x"
$ echo "$VAR" | base64 > f
$ VAR=$(cat f | base64 -d)
$ echo "${VAR}X"
1/ 
,xX
aleb
fonte
-2
printf 'VAR=$(cat <<\'$$VAR$$'\n%s\n'$$VAR$$'\n)' "$VAR" >./VAR.file

Outra maneira de fazer isso é garantir que você lide com todas as 'citações físicas como esta:

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

Ou com export:

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

A primeira e a segunda opções funcionam em qualquer shell POSIX, assumindo que o valor da variável não contenha a sequência:

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

A terceira opção deve funcionar para qualquer shell POSIX, mas pode tentar definir outras variáveis ​​como _ou PWD. A verdade é que as únicas variáveis ​​que ele pode tentar definir são definidas e mantidas pelo próprio shell - e, mesmo que você importe exporto valor de qualquer uma delas - como $PWDpor exemplo -, o shell simplesmente as redefinirá para o valor correto imediatamente de qualquer maneira - tente fazer PWD=any_valuee veja por si mesmo.

E porque - pelo menos com os GNU bash- a saída de depuração é automaticamente citada com segurança para reinserção no shell, isso funciona independentemente do número de 'aspas rígidas em "$VAR":

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

$VAR posteriormente pode ser definido como o valor salvo em qualquer script no qual o caminho a seguir seja válido:

. ./VAR.file
mikeserv
fonte
Não tenho certeza do que você tentou escrever no primeiro comando. $$é o PID do shell em execução, você entendeu errado as citações e significa \$ou algo assim? A abordagem básica do uso de um documento aqui pode ser feita para funcionar, mas é complicado, e não material de uma linha: o que você escolher como marcador final, terá que escolher algo que não apareça na string.
Gilles 'SO- stop be evil'
O segundo comando não funciona quando $VARcontém %. O terceiro comando nem sempre funciona com valores que contêm várias linhas (mesmo depois de adicionar as aspas duplas obviamente ausentes).
Gilles 'SO- stop be evil'
@ Gilles - eu sei que é o pid - usei-o como uma fonte simples de definir um delimitador único. O que você quer dizer com "nem sempre" exatamente? E não entendo quais aspas duplas estão faltando - todas são atribuições variáveis. As aspas duplas apenas confundem a situação nesse contexto.
mikeserv
@ Gilles - eu retiro a coisa da atribuição - isso é um argumento para env. Ainda estou curioso para saber o que você quer dizer com várias linhas - sedexclui todas as linhas até encontrar VAR=a última - para que todas as linhas $VARsejam transmitidas. Você pode fornecer um exemplo que o interrompa?
mikeserv
Ah, desculpas, o terceiro método funciona (com a correção de citação). Bem, assumindo o nome da variável (aqui VAR) não é alterado PWDou _ou talvez outros que algumas conchas definir. O segundo método requer bash; o formato de saída -vnão é padronizado (nenhum traço, ksh93, mksh e zsh funcionam).
Gilles 'SO- stop be evil'
-2

Quase o mesmo, mas um pouco diferente:

Do seu script:

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

Este tempo acima é testado.

vadimbog
fonte
Eu posso ver que você não testou! A lógica central funciona, mas essa não é a parte difícil. O difícil é citar as coisas corretamente, e você não está fazendo nada disso. Tente variáveis cujos valores conter novas linhas, ', *, etc.
Gilles 'SO parada sendo mal'
echo "$LVALUE=\"$RVALUE\""deve manter as novas linhas também e o resultado no arquivo cfg_file deve ser o seguinte: MY_VAR1 = "Linha1 \ nLinha 2" Portanto, quando avaliar MY_VAR1, também conterá as novas linhas. Claro que você pode ter problemas se o seu valor armazenado contiver um "caractere. Mas isso também pode ser resolvido.
vadimbog
1
Btw, por que votar em algo que está respondendo corretamente à pergunta feita aqui? Acima funciona muito bem para mim e usando em todos os lugares nos meus scripts?
vadimbog