Sérialiser la variable shell en bash ou zsh

12

Existe-t-il un moyen de sérialiser une variable shell? Supposons que j'ai une variable $VARet que je souhaite pouvoir l'enregistrer dans un fichier ou autre chose, puis la relire plus tard pour obtenir la même valeur?

Existe-t-il un moyen portable de le faire? (Je ne pense pas)

Existe-t-il un moyen de le faire en bash ou zsh?

fwenom
la source
2
Attention: la version de ma réponse que vous avez acceptée l'autre jour avait un problème grave qui se brisera dans certains scénarios. Je l'ai réécrit pour inclure des correctifs (et ajouter des fonctionnalités) et vous devriez vraiment le relire à partir de zéro et porter votre code pour utiliser la version fixe.
Caleb
^ Un autre ^ exemple de la citoyenneté éminente de @ Caleb.
mikeserv

Réponses:

14

Avertissement: avec l'une de ces solutions, vous devez être conscient que vous faites confiance à l'intégrité des fichiers de données pour être sûrs car ils seront exécutés en tant que code shell dans votre script. Les sécuriser est primordial pour la sécurité de votre script!

Implémentation en ligne simple pour sérialiser une ou plusieurs variables

Oui, à la fois dans bash et zsh, vous pouvez sérialiser le contenu d'une variable d'une manière qui est facile à récupérer à l'aide de la fonction typesetintégrée et de l' -pargument. Le format de sortie est tel que vous pouvez simplement sourcela sortie pour récupérer vos trucs.

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

Vous pouvez récupérer vos informations comme ceci plus tard dans votre script ou dans un autre script:

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

Cela fonctionnera pour bash, zsh et ksh, y compris le passage de données entre différents shells. Bash traduira cela dans sa declarefonction intégrée tandis que zsh l'implémentera avec typesetmais comme bash a un alias pour que cela fonctionne dans les deux cas car nous utilisons typesetici pour la compatibilité avec ksh.

Implémentation généralisée plus complexe à l'aide de fonctions

L'implémentation ci-dessus est vraiment simple, mais si vous l'appelez fréquemment, vous voudrez peut-être vous donner une fonction utilitaire pour le rendre plus facile. De plus, si jamais vous essayez d'inclure les fonctions personnalisées ci-dessus, vous rencontrerez des problèmes avec la portée des variables. Cette version devrait éliminer ces problèmes.

Remarque pour tous ces éléments, afin de maintenir la compatibilité croisée bash / zsh, nous allons corriger les deux cas de typesetet declaredonc le code devrait fonctionner dans l'un ou les deux shells. Cela ajoute du volume et des dégâts qui pourraient être éliminés si vous ne faisiez cela que pour un shell ou un autre.

Le principal problème lié à l'utilisation de fonctions à cet effet (ou à l'inclusion du code dans d'autres fonctions) est que la typesetfonction génère du code qui, lorsqu'il est renvoyé dans un script depuis l'intérieur d'une fonction, par défaut, crée une variable locale plutôt que globale.

Cela peut être résolu avec l'un des nombreux hacks. Ma tentative initiale de résoudre ce problème a été d'analyser la sortie du processus de sérialisation sedpour ajouter l' -gindicateur afin que le code créé définisse une variable globale lorsqu'il est récupéré.

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

Notez que l' sedexpression funky doit correspondre uniquement à la première occurrence de 'typeset' ou 'declare' et l'ajouter -gcomme premier argument. Il est nécessaire de ne faire correspondre que la première occurrence parce que, comme Stéphane Chazelas l'a souligné à juste titre dans les commentaires, sinon, il correspondra également aux cas où la chaîne sérialisée contient des retours à la ligne littéraux suivis du mot declare ou typeset.

En plus de corriger mon faux pas d' analyse initial , Stéphane a également suggéré une façon moins cassante de pirater cela qui non seulement contourne les problèmes avec l'analyse des chaînes, mais pourrait être un crochet utile pour ajouter des fonctionnalités supplémentaires en utilisant une fonction wrapper pour redéfinir les actions lors de la récupération des données. Cela suppose que vous ne jouez à aucun autre jeu avec les commandes declare ou typeset, mais cette technique serait plus facile à mettre en œuvre dans une situation où vous incluiez cette fonctionnalité dans le cadre d'une autre fonction de votre choix ou vous ne contrôliez pas les données en cours d'écriture et si le -gdrapeau avait été ajouté ou non . Quelque chose de similaire pourrait également être fait avec les alias, voir la réponse de Gilles pour une implémentation.

Pour rendre le résultat encore plus utile, nous pouvons itérer sur plusieurs variables passées à nos fonctions en supposant que chaque mot du tableau d'arguments est un nom de variable. Le résultat devient quelque chose comme ceci:

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
}

Avec l'une ou l'autre solution, l'utilisation ressemblerait à ceci:

# 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
la source
declareest l' bashéquivalent de ksh« s typeset. bash, prend zshégalement en charge typesetà cet égard, typesetest plus portable. export -pest POSIX, mais il ne prend aucun argument et sa sortie dépend du shell (bien qu'il soit bien spécifié pour les shells POSIX, donc par exemple lorsque bash ou ksh est appelé as sh). N'oubliez pas de citer vos variables; l'utilisation de l'opérateur split + glob ici n'a aucun sens.
Stéphane Chazelas
Notez que cela -Ene se trouve que dans certains BSD sed. Les valeurs des variables peuvent contenir des caractères de nouvelle ligne, il sed 's/^.../.../'n'est donc pas garanti que le fonctionne correctement.
Stéphane Chazelas
Ceci est exactement ce que je cherchais! Je voulais un moyen pratique de pousser les variables d'avant en arrière entre les shells.
fwenom
Je voulais dire: a=$'foo\ndeclare bar' bash -c 'declare -p a'pour l'installation produira une ligne qui commence par declare. Il vaut probablement mieux faire declare() { builtin declare -g "$@"; }avant d'appeler source(et désarmé après)
Stéphane Chazelas
2
@Gilles, les alias ne fonctionneraient pas à l'intérieur des fonctions (doivent être définis au moment de la définition de la fonction), et avec bash cela signifierait que vous auriez besoin de faire un shopt -s expandaliaslorsqu'il n'est pas interactif. Avec les fonctions, vous pouvez également améliorer le declarewrapper pour qu'il ne restaure que les variables que vous spécifiez.
Stéphane Chazelas
3

Utilisez la redirection, la substitution de commandes et l'expansion des paramètres. Des guillemets doubles sont nécessaires pour conserver les espaces et les caractères spéciaux. La fin xenregistre les nouvelles lignes de fin qui seraient autrement supprimées dans la substitution de commande.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}
choroba
la source
Il souhaite probablement enregistrer également le nom de la variable dans le fichier.
user80551
2

Sérialiser tout - POSIX

Dans n'importe quel shell POSIX, vous pouvez sérialiser toutes les variables d'environnement avec export -p. Cela n'inclut pas les variables shell non exportées. La sortie est correctement citée afin que vous puissiez la relire dans le même shell et obtenir exactement les mêmes valeurs de variable. La sortie peut ne pas être lisible dans un autre shell, par exemple ksh utilise la $'…'syntaxe non POSIX .

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

Sérialiser tout ou partie - ksh, bash, zsh

Ksh (à la fois pdksh / mksh et ATT ksh), bash et zsh offrent une meilleure facilité avec le typesetbuiltin. typeset -paffiche toutes les variables définies et leurs valeurs (zsh omet les valeurs des variables qui ont été masquées avec typeset -H). La sortie contient une déclaration appropriée pour que les variables d'environnement soient exportées lors de la lecture (mais si une variable est déjà exportée lors de la lecture, elle ne sera pas exportée), afin que les tableaux soient lus en tant que tableaux, etc. Ici aussi, la sortie est correctement cité mais n'est garanti que pour être lisible dans le même shell. Vous pouvez passer un ensemble de variables à sérialiser sur la ligne de commande; si vous ne transmettez aucune variable, tous sont sérialisés.

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

En bash et zsh, la restauration ne peut pas être effectuée à partir d'une fonction car les typesetinstructions à l'intérieur d'une fonction sont étendues à cette fonction. Vous devez exécuter . ./some_varsdans le contexte où vous souhaitez utiliser les valeurs des variables, en veillant à ce que les variables qui étaient globales lors de l'exportation soient redéclarées comme globales. Si vous souhaitez relire les valeurs d'une fonction et les exporter, vous pouvez déclarer un alias ou une fonction temporaire. En zsh:

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

En bash (qui utilise declareplutôt que typeset):

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

Dans ksh, typesetdéclare les variables locales dans les fonctions définies avec function function_name { … }et les variables globales dans les fonctions définies avec function_name () { … }.

Sérialiser certains - POSIX

Si vous souhaitez plus de contrôle, vous pouvez exporter manuellement le contenu d'une variable. Pour imprimer le contenu d'une variable exactement dans un fichier, utilisez la fonction printfintégrée ( echoa quelques cas spéciaux comme echo -nsur certains shells et ajoute une nouvelle ligne):

printf %s "$VAR" >VAR.content

Vous pouvez relire ceci avec $(cat VAR.content), sauf que la substitution de commande supprime les retours à la ligne en fin de ligne. Pour éviter ce pli, faites en sorte que la sortie ne se termine jamais par une nouvelle ligne.

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

Si vous souhaitez imprimer plusieurs variables, vous pouvez les citer avec des guillemets simples et remplacer tous les guillemets simples incorporés par '\''. Cette forme de citation peut être relue dans n'importe quel shell de style Bourne / POSIX. L'extrait suivant fonctionne dans n'importe quel shell POSIX. Il ne fonctionne que pour les variables de chaîne (et les variables numériques dans les shells qui les ont, bien qu'elles soient lues en tant que chaînes), il n'essaie pas de traiter les variables de tableau dans les shells qui les ont.

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
}

Voici une autre approche qui ne bifurque pas un sous-processus mais est plus lourde sur la manipulation de chaînes.

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
}

Notez que sur les shells qui autorisent les variables en lecture seule, vous obtiendrez une erreur si vous essayez de relire une variable en lecture seule.

Gilles 'SO- arrête d'être méchant'
la source
Cela apporte des variables telles que $PWDet $_- veuillez voir vos propres commentaires ci-dessous.
mikeserv
@Caleb Que diriez-vous de créer typesetun alias typeset -g?
Gilles 'SO- arrête d'être méchant'
@Gilles J'ai pensé à cela après que Stephanie ait suggéré la méthode de fonction, mais je ne savais pas comment définir de manière portable les options d'extension d'alias nécessaires entre les shells. Vous pourriez peut-être mettre cela dans votre réponse comme une alternative viable à la fonction que j'ai incluse.
Caleb
0

Un grand merci à @ stéphane-chazelas qui a souligné tous les problèmes avec mes précédentes tentatives, cela semble maintenant fonctionner pour sérialiser un tableau en stdout ou en variable.

Cette technique n'analyse pas en shell l'entrée (contrairement à declare -a/ declare -p) et est donc sûre contre l'insertion malveillante de métacaractères dans le texte sérialisé.

Remarque: les sauts de ligne ne sont pas échappés, car readsupprime la \<newlines>paire de caractères, ils -d ...doivent donc être transmis à la lecture, puis les retours à la ligne non échappés sont conservés.

Tout cela est géré dans la unserialisefonction.

Deux caractères magiques sont utilisés, le séparateur de champ et le séparateur d'enregistrement (afin que plusieurs tableaux puissent être sérialisés dans le même flux).

Ces caractères peuvent être définis comme FSet RSmais aucun ne peut être défini comme newlinecaractère car une nouvelle ligne échappée est supprimée par read.

Le caractère d'échappement doit être \la barre oblique inverse, car c'est ce qui est utilisé par readpour éviter que le caractère soit reconnu comme un IFScaractère.

serialisesérialisera "$@"vers stdout, serialise_tosérialisera vers la variable nommée dans$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
}

et désérialiser avec:

unserialise data # read from stdin

ou

unserialise data "$serialised_data" # from args

par exemple

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

(sans retour à la ligne)

relisez-le:

$ 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

Bash's readrespecte le caractère d'échappement \(à moins que vous ne passiez l'indicateur -r) pour supprimer la signification spéciale des caractères tels que la séparation du champ d'entrée ou la délimitation de ligne.

Si vous voulez sérialiser un tableau au lieu d'une simple liste d'arguments, passez simplement votre tableau comme liste d'arguments:

serialise_array "${my_array[@]}"

Vous pouvez utiliser unserialisedans une boucle comme vous le feriez readparce que c'est juste une lecture encapsulée - mais rappelez-vous que le flux n'est pas séparé par des sauts de ligne:

while unserialise array
do ...
done
Sam Liddicott
la source
Cela ne fonctionne pas si les éléments contiennent des caractères non imprimables (dans les paramètres régionaux actuels) ou contrôlent des caractères comme TAB ou la nouvelle ligne comme alors bashet les zshrendent sous $'\xxx'. Essayez avec bash -c $'printf "%q\n" "\t"'oubash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas
sacrément tootin, vous avez raison! Je modifierai ma réponse pour ne pas utiliser les itérations printf% q mais $ {@ // .. / ..} pour échapper à la place
Sam Liddicott
Cette solution dépend de $IFSsa non-modification et ne parvient plus à restaurer correctement les éléments de tableau vides. En fait, il serait plus judicieux d'utiliser une valeur différente d'IFS et d'utiliser -d ''pour éviter d'avoir à échapper à la nouvelle ligne. Par exemple, utilisez :comme séparateur de champ et n'échappez qu'à cela et à la barre oblique inverse et utilisez IFS=: read -ad '' arraypour importer.
Stéphane Chazelas
Ouais .... J'ai oublié le traitement spécial d'effondrement des espaces blancs lorsqu'il est utilisé comme séparateur de champ en lecture. Je suis content que vous soyez sur la balle aujourd'hui! Vous avez raison sur -d "" pour éviter de vous échapper \ n, mais dans mon cas, je voulais lire un flux de sérialisations - j'adapterai cependant la réponse. Merci!
Sam Liddicott
Un retour à la ligne ne permet pas de le conserver, il le fait disparaître une fois read. backslash-newline for readest un moyen de continuer une ligne logique sur une autre ligne physique. Edit: ah je vois que vous mentionnez déjà le problème avec la nouvelle ligne.
Stéphane Chazelas
0

Vous pouvez utiliser base64:

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

Une autre façon de le faire est de vous assurer de gérer tous les 'guillemets comme celui-ci:

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

Ou avec export:

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

Les première et deuxième options fonctionnent dans n'importe quel shell POSIX, en supposant que la valeur de la variable ne contient pas la chaîne:

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

La troisième option devrait fonctionner pour n'importe quel shell POSIX mais peut tenter de définir d'autres variables telles que _ou PWD. La vérité est cependant que les seules variables qu'il pourrait essayer de définir sont définies et maintenues par le shell lui-même - et donc même si vous importez exportla valeur de l'un d'entre eux - comme $PWDpar exemple - le shell les réinitialisera simplement la valeur correcte immédiatement de toute façon - essayez de faire PWD=any_valueet voyez par vous-même.

Et parce que - au moins avec GNU bash- la sortie de débogage est automatiquement citée en toute sécurité pour une nouvelle entrée dans le shell, cela fonctionne quel que soit le nombre de 'guillemets dans "$VAR":

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

$VAR peut être défini ultérieurement sur la valeur enregistrée dans tout script dans lequel le chemin suivant est valide avec:

. ./VAR.file
mikeserv
la source
Je ne sais pas ce que vous avez essayé d'écrire dans la première commande. $$est le PID du shell en cours d'exécution \$. L'approche de base de l'utilisation d'un document ici pourrait fonctionner, mais c'est un matériau délicat et non à une ligne: quel que soit le marqueur de fin, vous devez choisir quelque chose qui n'apparaît pas dans la chaîne.
Gilles 'SO- arrête d'être méchant'
La deuxième commande ne fonctionne pas lorsque $VARcontient %. La troisième commande ne fonctionne pas toujours avec des valeurs contenant plusieurs lignes (même après l'ajout des guillemets doubles évidemment manquants).
Gilles 'SO- arrête d'être méchant'
@ Gilles - Je sais que c'est le pid - je l'ai utilisé comme une simple source pour définir un délimiteur unique. Que voulez-vous dire par «pas toujours» exactement? Et je ne comprends pas les guillemets manquants - ce sont tous des affectations variables. Les guillemets doubles ne font que confondre la situation dans ce contexte.
mikeserv
@ Gilles - Je retire la chose d'affectation - c'est un argument pour env. Je suis toujours curieux de savoir ce que vous voulez dire sur les lignes multiples - sedsupprime chaque ligne jusqu'à la rencontre VAR=jusqu'à la dernière - donc toutes les lignes $VARsont transmises. Pouvez-vous s'il vous plaît fournir un exemple qui le casse?
mikeserv
Ah, toutes mes excuses, la troisième méthode fonctionne (avec la correction des citations). Eh bien, en supposant que le nom de variable (ici VAR) n'est pas changé PWDou _ou peut - être d' autres que quelques coquilles définissent. La deuxième méthode nécessite bash; le format de sortie de -vn'est pas standardisé (aucun de dash, ksh93, mksh et zsh ne fonctionne).
Gilles 'SO- arrête d'être méchant'
-2

Presque identique mais un peu différent:

De votre 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"

Cette fois ci-dessus est testé.

vadimbog
la source
Je vois que tu n'as pas testé! La logique de base fonctionne, mais ce n'est pas la partie difficile. Le plus difficile est de citer les choses correctement, et vous ne faites rien de tout cela. Essayez les variables dont les valeurs contiennent des sauts de ligne, ', *, etc.
Gilles de l' arrêt SO- étant le mal "
echo "$LVALUE=\"$RVALUE\""est censé conserver les nouvelles lignes également et le résultat dans le fichier cfg devrait être comme: MY_VAR1 = "Line1 \ nLine 2" Ainsi, lors de l'évaluation de MY_VAR1, il contiendra également les nouvelles lignes. Bien sûr, vous pourriez avoir des problèmes si votre valeur stockée contient elle-même "char. Mais cela pourrait également être pris en charge.
vadimbog
1
Btw, pourquoi voter contre quelque chose qui répond correctement à la question posée ici? Ci-dessus fonctionne très bien pour moi et utiliser partout dans mes scripts?
vadimbog