Serializar variable de shell en bash o zsh

12

¿Hay alguna forma de serializar una variable de shell? Supongamos que tengo una variable $VAR, y quiero poder guardarla en un archivo o lo que sea, y luego leerla más tarde para recuperar el mismo valor.

¿Hay alguna forma portátil de hacer esto? (No lo creo)

¿Hay alguna manera de hacerlo en bash o zsh?

fwenom
fuente
2
Atención: la versión de mi respuesta que aceptó el otro día tuvo un problema grave que se romperá en algunos escenarios. Lo he reescrito para incluir correcciones (y agregar funciones) y realmente debería volver a leerlo desde cero y portar su código para usar la versión fija.
Caleb
^ Otro ^ ejemplo de la ciudadanía honrada de @ Caleb.
mikeserv

Respuestas:

14

Advertencia: con cualquiera de estas soluciones, debe tener en cuenta que confía en la integridad de los archivos de datos para que estén seguros, ya que se ejecutarán como código shell en su secuencia de comandos. ¡Asegurarlos es primordial para la seguridad de su script!

Implementación en línea simple para serializar una o más variables

Sí, tanto en bash como en zsh puede serializar el contenido de una variable de una manera fácil de recuperar utilizando typesetel -pargumento y el argumento incorporado . El formato de salida es tal que puede simplemente sourcela salida para recuperar sus cosas.

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

Puede recuperar sus cosas así más adelante en su secuencia de comandos o en otra secuencia de comandos por completo:

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

Esto funcionará para bash, zsh y ksh, incluido el paso de datos entre diferentes shells. Bash traducirá esto a su declarefunción incorporada mientras que zsh implementa esto, typesetpero como bash tiene un alias para que esto funcione de cualquier manera, ya que lo usamos typesetaquí para la compatibilidad de ksh.

Implementación generalizada más compleja usando funciones

La implementación anterior es realmente simple, pero si la llama con frecuencia, es posible que desee tener una función de utilidad para que sea más fácil. Además, si alguna vez intenta incluir las funciones personalizadas anteriores, se encontrará con problemas con el alcance variable. Esta versión debería eliminar esos problemas.

Tenga en cuenta que para todo esto, para mantener la compatibilidad cruzada de bash / zsh, arreglaremos ambos casos typesety, declarepor lo tanto, el código debería funcionar en uno o ambos shells. Esto agrega algo de volumen y desorden que podrían eliminarse si solo estuvieras haciendo esto para un shell u otro.

El principal problema con el uso de funciones para esto (o incluir el código en otras funciones) es que la typesetfunción genera código que, cuando se origina en un script desde dentro de una función, por defecto crea una variable local en lugar de una global.

Esto se puede solucionar con uno de varios hacks. Mi intento inicial de solucionar esto fue analizar la salida del proceso de serialización sedpara agregar el -gindicador para que el código creado defina una variable global cuando se obtiene de nuevo.

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

Tenga en cuenta que la sedexpresión funky es solo coincidir con la primera aparición de 'typeset' o 'declare' y agregar -gcomo primer argumento. Es necesario que solo coincida con la primera aparición porque, como Stéphane Chazelas señaló correctamente en los comentarios, de lo contrario también coincidirá con los casos en que la cadena serializada contenga nuevas líneas literales seguidas de la palabra declarar o componer.

Además de corregir mis análisis sintáctico iniciales paso en falso , Stéphane también sugirió una manera menos frágil que cortar esto que no sólo los pasos laterales de los problemas con el análisis de las cuerdas, pero podría ser un gancho útil añadir funcionalidad adicional mediante el uso de una función de contenedor para redefinir las acciones tomado al obtener los datos nuevamente. Esto supone que no está jugando ningún otro juego con los comandos declarar o componer, pero esta técnica sería más fácil de implementar en una situación en la que estuviera incluyendo esta funcionalidad como parte de otra función propia o usted no tenía el control de los datos que se escribían y si tenía o no -gagregado el indicador. Algo similar también se podría hacer con alias, vea la respuesta de Gilles para una implementación.

Para que el resultado sea aún más útil, podemos iterar sobre múltiples variables pasadas a nuestras funciones asumiendo que cada palabra en la matriz de argumentos es un nombre de variable. El resultado se convierte en algo como esto:

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 cualquiera de las soluciones, el uso se vería así:

# 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
fuente
declarees el bashequivalente de ksh's typeset. bash, zshtambién soporte typesetpor lo que en ese sentido, typesetes más portátil. export -pes POSIX, pero no toma ningún argumento y su salida depende del shell (aunque está bien especificado para shells POSIX, por ejemplo, cuando se llama a bash o ksh como sh). Recuerde citar sus variables; usar el operador split + glob aquí no tiene sentido.
Stéphane Chazelas
Tenga en cuenta que -Esolo se encuentra en algunos BSD sed. Los valores variables pueden contener caracteres de nueva línea, por lo sed 's/^.../.../'que no se garantiza que funcione correctamente.
Stéphane Chazelas
¡Esto es exactamente lo que estaba buscando! Quería una forma conveniente de empujar las variables de un lado a otro entre los depósitos.
fwenom
Quise decir: a=$'foo\ndeclare bar' bash -c 'declare -p a'para instalar generará una línea que comienza con declare. Probablemente sea mejor hacerlo declare() { builtin declare -g "$@"; }antes de llamar source(y desarmar después)
Stéphane Chazelas
2
@Gilles, los alias no funcionarían dentro de las funciones (deben definirse en el momento de la definición de la función), y con bash eso significaría que tendría que hacer un shopt -s expandaliascuando no es interactivo. Con las funciones, también puede mejorar el declarecontenedor para que solo restaure las variables que especifique.
Stéphane Chazelas
3

Utilice la redirección, la sustitución de comandos y la expansión de parámetros. Las comillas dobles son necesarias para preservar espacios en blanco y caracteres especiales. El final xguarda las nuevas líneas finales que de otro modo se eliminarían en la sustitución del comando.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}
choroba
fuente
Probablemente quiera guardar el nombre de la variable también en el archivo.
user80551
2

Serializar todo: POSIX

En cualquier shell POSIX, puede serializar todas las variables de entorno con export -p. Esto no incluye variables de shell no exportadas. La salida se cita correctamente para que pueda volver a leerla en el mismo shell y obtener exactamente los mismos valores variables. Es posible que la salida no sea legible en otro shell, por ejemplo, ksh usa la $'…'sintaxis no POSIX .

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

Serializar algunos o todos: ksh, bash, zsh

Ksh (pdksh / mksh y ATT ksh), bash y zsh proporcionan una mejor instalación con el typesetincorporado. typeset -pimprime todas las variables definidas y sus valores (zsh omite los valores de las variables que se han ocultado con typeset -H). La salida contiene una declaración adecuada para que las variables de entorno se exporten cuando se vuelven a leer (pero si una variable ya se exporta cuando se vuelve a leer, no se exportará), de modo que las matrices se vuelven a leer como matrices, etc. Aquí también, la salida se cita correctamente pero solo se garantiza que se pueda leer en el mismo shell. Puede pasar un conjunto de variables para serializar en la línea de comando; Si no pasa ninguna variable, todas se serializan.

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

En bash y zsh, la restauración no se puede hacer desde una función porque las typesetdeclaraciones dentro de una función tienen el alcance de esa función. Debe ejecutar . ./some_varsen el contexto en el que desea utilizar los valores de las variables, teniendo cuidado de que las variables que fueron globales cuando se exportaron se volverán a declarar como globales. Si desea volver a leer los valores dentro de una función y exportarlos, puede declarar un alias o función temporal. En zsh:

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

En bash (que usa en declarelugar de typeset):

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

En ksh, typesetdeclara variables locales en funciones definidas con function function_name { … }y variables globales en funciones definidas con function_name () { … }.

Serializar algunos - POSIX

Si desea más control, puede exportar el contenido de una variable manualmente. Para imprimir el contenido de una variable exactamente en un archivo, use el printfincorporado ( echotiene algunos casos especiales, como echo -nen algunos shells y agrega una nueva línea):

printf %s "$VAR" >VAR.content

Puede volver a leer esto con $(cat VAR.content), excepto que la sustitución del comando elimina las nuevas líneas finales. Para evitar esta arruga, organice que la salida no termine nunca con una nueva línea.

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

Si desea imprimir varias variables, puede citarlas con comillas simples y reemplazar todas las comillas simples incrustadas '\''. Esta forma de cita se puede volver a leer en cualquier shell de estilo Bourne / POSIX. El siguiente fragmento funciona en cualquier shell POSIX. Solo funciona para variables de cadena (y variables numéricas en shells que las tienen, aunque se volverán a leer como cadenas), no trata de tratar con variables de matriz en shells que las tienen.

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
}

Aquí hay otro enfoque que no bifurca un subproceso pero es más pesado en la manipulación de cadenas.

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
}

Tenga en cuenta que en los shells que permiten variables de solo lectura, obtendrá un error si intenta volver a leer una variable que es de solo lectura.

Gilles 'SO- deja de ser malvado'
fuente
Esto trae variables como $PWDy $_- por favor vea sus propios comentarios a continuación.
mikeserv
@Caleb ¿Qué tal hacer typesetun alias typeset -g?
Gilles 'SO- deja de ser malvado'
@Gilles Pensé en eso después de que Stephanie sugirió el método de función, pero no estaba seguro de cómo configurar de forma portátil las opciones de expansión de alias necesarias en los shells. Tal vez podría poner eso en su respuesta como una alternativa viable a la función que incluí.
Caleb
0

Muchas gracias a @ stéphane-chazelas que señaló todos los problemas con mis intentos anteriores, ahora parece funcionar para serializar una matriz en stdout o en una variable.

Esta técnica no analiza la entrada de shell (a diferencia de declare -a/ declare -p) y, por lo tanto, es segura contra la inserción maliciosa de metacaracteres en el texto serializado.

Nota: las nuevas líneas no se escapan, ya que readelimina el \<newlines>par de caracteres, por lo que -d ...debe pasarse a leer, y luego se conservan las nuevas líneas sin escapes.

Todo esto se gestiona en la unserialisefunción.

Se utilizan dos caracteres mágicos, el separador de campo y el separador de registros (para que se puedan serializar múltiples matrices en la misma secuencia).

Estos caracteres se pueden definir como FSy, RSpero tampoco se pueden definir como newlinecaracteres porque se elimina una nueva línea escapada read.

El carácter de escape debe ser \la barra diagonal inversa, ya que eso es lo que se utiliza readpara evitar que el personaje sea reconocido como un IFSpersonaje.

serialisese serializará "$@"en stdout, serialise_tose serializará en la variable nombrada en$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
}

y no serializar con:

unserialise data # read from stdin

o

unserialise data "$serialised_data" # from args

p.ej

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

(sin una nueva línea al final)

léelo de nuevo:

$ 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 readrespeta el carácter de escape \(a menos que pase el indicador -r) para eliminar el significado especial de los caracteres, como la separación del campo de entrada o la delimitación de línea.

Si desea serializar una matriz en lugar de una simple lista de argumentos, simplemente pase su matriz como la lista de argumentos:

serialise_array "${my_array[@]}"

Puede usarlo unserialiseen un bucle como lo haría readporque es solo una lectura ajustada, pero recuerde que la secuencia no está separada por una nueva línea:

while unserialise array
do ...
done
Sam Liddicott
fuente
No funciona si los elementos contienen caracteres no imprimibles (en la configuración regional actual) o de control como TAB o nueva línea como entonces bashy los zshrepresenta como $'\xxx'. Prueba con bash -c $'printf "%q\n" "\t"'obash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas
maldita sea, tienes razón! Modificaré mi respuesta para no usar printf% q pero $ {@ // .. / ..} iteraciones para escapar del espacio en blanco
Sam Liddicott
Esa solución depende de $IFSque no se modifique y ahora no puede restaurar los elementos de matriz vacíos correctamente. De hecho, tendría más sentido usar un valor diferente de IFS, y usarlo -d ''para evitar tener que escapar de la nueva línea. Por ejemplo, use :como separador de campo y solo escape de eso y de la barra diagonal inversa y use IFS=: read -ad '' arraypara importar.
Stéphane Chazelas
Sí ... Me olvidé del tratamiento especial de colapso de espacios en blanco cuando se usaba como separador de campo en lectura. ¡Me alegra que estés en la pelota hoy! Tienes razón sobre -d "" para evitar escapar \ n, pero en mi caso quería leer una secuencia de serializaciones, aunque adaptaré la respuesta. ¡Gracias!
Sam Liddicott
Escapar de nueva línea no permite que se conserve, hace que desaparezca una vez read. barra invertida-nueva línea para reades una forma de continuar una línea lógica en otra línea física. Editar: ah, veo que ya mencionas el problema con la nueva línea.
Stéphane Chazelas
0

Podrías usar base64:

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

Otra forma de hacerlo es asegurarte de manejar todas las 'citas físicas como esta:

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 

Las opciones primera y segunda funcionan en cualquier shell POSIX, suponiendo que el valor de la variable no contenga la cadena:

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

La tercera opción debería funcionar para cualquier shell POSIX, pero puede intentar definir otras variables como _o PWD. Sin embargo, la verdad es que las únicas variables que podría intentar definir son establecidas y mantenidas por el propio shell, por lo que incluso si importa exportel valor de cualquiera de ellas, $PWDpor ejemplo, el shell simplemente las restablecerá a el valor correcto inmediatamente de todos modos, intente hacerlo PWD=any_valuey vea por usted mismo.

Y debido a que, al menos con GNU, la bashsalida de depuración se cita automáticamente de forma segura para volver a ingresar al shell, esto funciona independientemente de la cantidad de 'comillas en "$VAR":

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

$VAR luego se puede establecer en el valor guardado en cualquier script en el que la siguiente ruta sea válida con:

. ./VAR.file
mikeserv
fuente
No estoy seguro de lo que intentaste escribir en el primer comando. $$es el PID del shell en ejecución, ¿entendiste mal y mal \$? El enfoque básico de usar un documento aquí podría funcionar, pero es complicado, no es un material de una sola línea: cualquiera que elija como marcador final, debe elegir algo que no aparezca en la cadena.
Gilles 'SO- deja de ser malvado'
El segundo comando no funciona cuando $VARcontiene %. El tercer comando no siempre funciona con valores que contienen varias líneas (incluso después de agregar las comillas dobles obviamente faltantes).
Gilles 'SO- deja de ser malvado'
@Gilles - Sé que es el pid - Lo usé como una fuente simple de establecer un delimitador único. ¿Qué quieres decir con "no siempre" exactamente? Y no entiendo qué comillas dobles faltan: todas esas son asignaciones variables. Las comillas dobles solo confunden la situación en ese contexto.
mikeserv
@Gilles - Retraigo el asunto de la asignación - eso es un argumento para env. Todavía tengo curiosidad por lo que quieres decir con las múltiples líneas: sedelimina cada línea hasta que se encuentran VAR=hasta la última, por lo que todas las líneas $VARse pasan. ¿Puedes dar un ejemplo que lo rompa?
mikeserv
Ah, disculpas, el tercer método funciona (con la corrección de citas). Bueno, suponiendo que el nombre de la variable (en este caso VAR) no se modifica PWDo _o tal vez otros que definen algunas conchas. El segundo método requiere bash; el formato de salida de -vno está estandarizado (ninguno de dash, ksh93, mksh y zsh funcionan).
Gilles 'SO- deja de ser malvado'
-2

Casi igual pero un poco diferente:

De tu guión:

#!/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 tiempo anterior está probado.

vadimbog
fuente
Puedo ver que no has probado! La lógica central funciona, pero esa no es la parte difícil. Lo difícil es citar las cosas correctamente, y no estás haciendo nada de eso. Trate de variables cuyos valores contienen nuevas líneas, ', *, etc.
'SO siendo parada del mal' Gilles
echo "$LVALUE=\"$RVALUE\""se supone que también debe mantener las nuevas líneas y el resultado en el archivo cfg_file debería ser como: MY_VAR1 = "Line1 \ nLine 2" Así, cuando eval MY_VAR1 también contendrá las nuevas líneas. Por supuesto, puede tener problemas si su valor almacenado contiene "char. Pero eso también podría solucionarse.
vadimbog
1
Por cierto, ¿por qué votar hacia abajo algo que responde correctamente a la pregunta que se hace aquí? ¿Lo anterior funciona muy bien para mí y lo uso en todas partes en mis scripts?
vadimbog