Serialize a variável shell no bash ou zsh

10

Existe alguma maneira de serializar uma variável shell? Suponha que eu tenha uma variável $VAR , e eu queira ser capaz de salvá-la em um arquivo ou algo assim, e depois ler mais tarde para obter o mesmo valor de volta?

Existe uma maneira portátil de fazer isso? (Eu não penso assim)

Existe uma maneira de fazer isso em bash ou zsh?

    
por fwenom 14.06.2014 / 08:23

6 respostas

13

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

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

Sim, tanto no bash quanto no zsh você pode serializar o conteúdo de uma variável de maneira fácil de recuperar usando o argumento typeset builtin e -p . O formato de saída é tal que você pode simplesmente source da 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 no script ou em outro script:

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

Isso funcionará para bash, zsh e ksh, incluindo a passagem de dados entre diferentes shells. O Bash irá traduzir isso para a sua função declare enquanto o zsh implementa isso com typeset mas como o bash tem um alias para isto funcionar de qualquer forma pois usamos typeset aqui para compatibilidade com o ksh.

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

A implementação acima é realmente simples, mas se você a chamar com frequência, pode querer dar a si mesmo uma função de utilidade para facilitar. Além disso, se você tentar incluir as funções personalizadas em cima, você terá problemas com o escopo da variável. Esta versão deve eliminar esses problemas.

Nota para tudo isso, para manter a compatibilidade cruzada do bash / zsh, corrigiremos os casos de typeset e declare , para que o código funcione em um ou nos dois 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 função typeset gera código que, quando originado de volta em um script de dentro de uma função, padroniza a criação de uma variável local um global.

Isso pode ser corrigido com um dos vários hacks. Minha tentativa inicial de consertar isso foi analisar a saída do processo de serialização por meio de sed para adicionar o -g flag para que o código criado defina uma variável global quando originado de volta.

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

Observe que a expressão funky sed deve corresponder apenas à primeira ocorrência de 'typeset' ou 'declare' e adicionar -g como primeiro argumento. É necessário apenas coincidir com a primeira ocorrência porque, como corretamente apontado em Stéphane Chazelas comentários, caso contrário, ele também corresponderá aos casos em que a sequência serializada contém novas linhas literais seguidas pela palavra declare ou typeset.

Além de corrigir minha análise inicial faux pas , Stéphane também sugeriu uma maneira menos frágil de hackear isso, que não apenas corrige as questões com a análise das cadeias de caracteres, mas pode ser um gancho útil para adicionar funcionalidade adicional usando uma função de wrapper para redefinir as ações Quando você obtém os dados de volta. Isso pressupõe que você não está 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ê incluía essa funcionalidade como parte de outra função própria ou você não estava no controle dos dados sendo gravados e se ele tinha ou não o sinalizador -g adicionado. 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 é um nome de variável. O resultado se torna algo assim:

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 seria 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"
    
por 14.06.2014 / 17:19
3

Use o redirecionamento, a substituição de comandos e a expansão de parâmetros. Aspas duplas são necessárias para preservar espaços em branco e caracteres especiais. O trailing x salva as novas linhas à direita que seriam removidas da substituição do comando.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}
    
por 14.06.2014 / 13:41
1

Serialize 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áveis. A saída pode não ser legível em outro shell, por exemplo, o ksh usa a sintaxe% POSI $'…' .

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

Serialize alguns ou todos - ksh, bash, zsh

Ksh (ambos pdksh / mksh e ATT ksh), bash e zsh fornecem uma facilidade melhor com o typeset embutido. typeset -p imprime todas as variáveis definidas e seus valores (zsh omite os valores das variáveis que foram ocultadas com typeset -H ). A saída contém a declaração apropriada para que as variáveis de ambiente sejam exportadas quando lidas de volta (mas se uma variável já é exportada quando lida de volta, não será não exportada), de modo que as matrizes sejam lidas de volta como matrizes, etc. é corretamente citado, mas só é garantido para ser lido 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, todos serão serializados.

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 instruções typeset dentro de uma função têm escopo para essa função. Você precisa executar . ./some_vars no contexto em que deseja usar os valores das variáveis, cuidando para que as variáveis que são globais quando exportadas sejam redeclaradas como globais. Se você quiser ler os valores em uma função e exportá-los, você pode declarar um alias ou função temporária. Em zsh:

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

No bash (que usa declare em vez de typeset ):

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

Em ksh, typeset declara 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ê quiser mais controle, poderá exportar o conteúdo de uma variável manualmente. Para imprimir o conteúdo de uma variável exatamente em um arquivo, use o printf builtin ( echo tem alguns casos especiais, como echo -n em alguns shells 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 remove as novas linhas finais. Para evitar esse enrugamento, providencie para que a saída não termine 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 quiser imprimir várias variáveis, você pode citá-las com aspas simples e substituir todas as aspas simples incorporadas por '\'' . Essa forma de citar pode ser lida de volta em qualquer shell do estilo Bourne / POSIX. O trecho a seguir funciona em qualquer shell POSIX. Ele só funciona para variáveis de string (e variáveis numéricas em shells que as possuem, embora sejam lidas de volta como strings), ele 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 em shells que permitem variáveis somente leitura, você receberá um erro se tentar ler de volta uma variável que seja somente leitura.

    
por 15.06.2014 / 22:55
0

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

Esta técnica não analisa a entrada do shell (ao contrário de declare -a / declare -p ) e, portanto, é segura contra inserção maliciosa de metacaracteres no texto serializado.

Nota: as novas linhas não são ignoradas, porque read elimina o par de caracteres \<newlines> , pelo que -d ... deve ser passado para leitura e as novas linhas sem escape são preservadas.

Tudo isso é gerenciado na função unserialise .

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

Esses caracteres podem ser definidos como FS e RS , mas nenhum deles pode ser definido como newline porque uma nova linha de escape é excluída por read .

O caractere de escape deve ser \ da barra invertida, pois é isso que é usado por read para evitar que o caractere seja reconhecido como um caractere IFS .

serialise serializará "$@" para stdout, serialise_to será serializado 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 desserialize 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)

leia de novo:

$ 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 read do Bash respeita o caractere de escape \ (a menos que você passe o sinalizador -r) para remover o significado especial de caracteres, como a separação de campos de entrada ou a delimitação de linhas.

Se você deseja serializar uma matriz em vez de uma simples lista de argumentos, apenas passe sua matriz como a lista de argumentos:

serialise_array "${my_array[@]}"

Você pode usar unserialise em um loop como faria com read porque é apenas uma leitura encapsulada - mas lembre-se de que o fluxo não é separado por nova linha:

while unserialise array
do ...
done
    
por 29.04.2016 / 11:07
-2
printf 'VAR=$(cat <<\'$$VAR$$'\n%s\n'$$VAR$$'\n)' "$VAR" >./VAR.file

Outra maneira de fazer isso é garantir que você lida com todos os ' hardquotes assim:

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ção funcionam em qualquer shell POSIX, assumindo que o valor da variável não contém a string:

"\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 . No entanto, a verdade é que as únicas variáveis que ele pode tentar definir são definidas e mantidas pelo próprio shell - e, portanto, mesmo se você importar o valor de export para qualquer um deles - como $PWD por exemplo - o O shell irá simplesmente reconfigurá-los para o valor correto imediatamente - tente fazer PWD=any_value e veja por si mesmo.

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

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

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

. ./VAR.file
    
por 14.06.2014 / 15:56
-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.

    
por 14.06.2014 / 16:23