在bash或zsh中序列化shell变量


12

有什么方法可以序列化shell变量吗?假设我有一个变量$VAR,并且希望将其保存到文件或其他文件中,然后稍后再读回以获取相同的值?

有便携式的方法吗?(我不这么认为)

有没有办法在bash或zsh中做到这一点?


2
注意:前几天您接受的答案版本存在严重问题,在某些情况下可能会中断。我已经对其进行了重新编写,以包含修复程序(并添加功能),并且您真的应该从头开始重新阅读它,并移植代码以使用固定版本。
Caleb 2014年

^ Caleb崇高的公民身份的另一个例子。
mikeserv

Answers:


14

警告:使用这些解决方案中的任何一种,您都需要意识到,您信任数据文件的完整性是安全的,因为它们将作为脚本中的Shell代码执行。保护它们对脚本的安全至关重要!

简单的内联实现,用于序列化一个或多个变量

是的,在bash和zsh中,您都可以使用typeset内置-p函数和自变量以易于检索的方式序列化变量的内容。输出格式使您可以简单地source将输出取回。

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

您可以稍后在脚本中或在另一个脚本中将您的东西重新找回:

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

这将适用于bash,zsh和ksh,包括在不同的shell之间传递数据。Bash会将其转换为内置declare函数,而zsh则通过Bash将其转换为内置函数,typeset但由于bash对此具有别名,因此我们可以typeset在这里使用它来实现ksh兼容性。

使用函数的更复杂的通用实现

上面的实现确实很简单,但是如果您经常调用它,则可能需要给自己一个实用程序函数以使其变得更容易。此外,如果您尝试在自定义函数中包含上述内容,则会遇到变量范围问题。此版本应消除这些问题。

注意所有这些,为了保持庆典/ zsh的交叉兼容性,我们将在固定的两种情况typeset,并declare因此代码应该在一个或两个壳工作。如果您仅对一个或另一个外壳执行此操作,则会增加一些体积和混乱。

为此使用函数(或将代码包含在其他函数中)的主要问题是,typeset函数生成的代码在从函数内部返回到脚本时会默认创建局部变量,而不是全局变量。

可以使用以下几种技巧之一解决此问题。我最初试图解决此问题的方法是解析序列化过程的输出,sed以添加-g标志,以便所创建的代码在返回时定义全局变量。

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

请注意,时髦的sed表达式仅匹配“ typeset”或“ declare”的首次出现,并添加-g为第一个参数。仅需要匹配第一个匹配项,因为正如StéphaneChazelas在注释中正确指出的那样,否则它也将匹配序列化字符串包含文字换行符后跟“声明”或“排版”一词的情况。

除了纠正我的初步分析失礼,斯特凡还提出一个较不脆的方式来破解这不仅侧步骤,解析字符串的问题,但可能是一个有用的钩子使用的包装功能重新定义了行动,以添加额外的功能假设您没有使用声明或排版命令玩任何其他游戏,但是在您将此功能作为自己或其他功能的一部分包含在内的情况下,更容易实现此技术。您无法控制要写入的数据以及是否-g添加了标志。别名也可以完成类似的操作,有关实现请参见Gilles的答案

为了使结果更加有用,我们可以通过假设参数数组中的每个单词都是变量名来遍历传递给函数的多个变量。结果变成这样:

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
}

无论使用哪种解决方案,用法都将如下所示:

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

declarebash等价kshtypesetbash,在这方面zshtypeset如此支持,typeset更加便携。export -p是POSIX,但不带任何参数,并且其输出依赖于外壳程序(尽管已为POSIX外壳程序很好地指定了它,例如,将bash或ksh称为sh)。请记住引用您的变量;在这里使用split + glob运算符没有意义。
斯特凡Chazelas

注意,-E仅在某些BSD中可以找到sed。变量值可能包含换行符,因此sed 's/^.../.../'不能保证可以正常工作。
斯特凡Chazelas

这正是我想要的!我想要一种在外壳之间来回推送变量的便捷方法。
fwenom 2014年

我的意思是:a=$'foo\ndeclare bar' bash -c 'declare -p a'for install将输出以开头的行declare。它可能会更好做declare() { builtin declare -g "$@"; }调用之前source(和取消之后)
斯特凡Chazelas

2
@Gilles,别名在函数内部不起作用(需要在函数定义时定义),并且使用bash表示您需要shopt -s expandalias在不交互时进行操作。使用函数,您还可以增强declare包装器,以便仅还原您指定的变量。
斯特凡Chazelas

3

使用重定向,命令替换和参数扩展。需要双引号来保留空格和特殊字符。尾随x保存尾随换行符,否则将在命令替换中将其删除。

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

他可能想将变量名也保存到文件中。
user80551 2014年

2

全部序列化— POSIX

在任何POSIX Shell中,都可以使用序列化所有环境变量export -p。这不包括未导出的shell变量。输出使用正确的引号引起来,因此您可以在同一外壳中将其读回并获得完全相同的变量值。输出可能无法在另一个shell中读取,例如ksh使用非POSIX $'…'语法。

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

序列化部分或全部-ksh,bash,zsh

Ksh(pdksh / mksh和ATT ksh),bash和zsh都typeset内置了更好的功能。typeset -p打印出所有已定义的变量及其值(zsh省略了用隐藏的变量的值typeset -H)。输出包含正确的声明,以便在回读时导出环境变量(但如果在回读时已导出变量,则不会取消导出),以便将数组读回为数组,等等。这里也是输出被正确引用,但仅保证在同一外壳中可读。您可以在命令行上传递一组变量以进行序列化。如果您不传递任何变量,则全部序列化。

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

在bash和zsh中,无法从函数进行恢复,因为typeset函数内的语句的作用域是该函数。您需要. ./some_vars在要使用变量值的上下文中运行,请注意,导出时全局变量将被重新声明为全局变量。如果要读回函数中的值并导出它们,则可以声明一个临时别名或函数。在zsh中:

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

在bash中(使用declare而不是typeset):

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

在ksh中,typeset用定义的函数声明局部变量,用定义的函数声明function function_name { … }全局变量function_name () { … }

序列化一些— POSIX

如果需要更多控制,可以手动导出变量的内容。要将变量的内容准确地打印到文件中,请使用printf内置功能(echo有一些特殊情况,例如echo -n在某些shell上并添加了换行符):

printf %s "$VAR" >VAR.content

您可以使用来读回该内容$(cat VAR.content),除了命令替换会删除尾随的换行符。为了避免出现这种皱纹,请安排输出不要以换行符结尾。

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

如果要打印多个变量,可以用单引号将它们引起来,并用替换所有嵌入的单引号'\''。可以将这种形式的引用读回到任何Bourne / POSIX样式的shell中。以下代码段可在任何POSIX Shell中使用。它仅适用于字符串变量(以及包含它们的shell中的数字变量,尽管它们将作为字符串读回),但不会尝试处理具有它们的shell中的数组变量。

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
}

这是另一种方法,它不派生子进程,但在字符串操作上较重。

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
}

请注意,在允许只读变量的shell上,如果尝试回读只读变量,则会收到错误消息。


这会带来诸如$PWD和的变量$_-请在下面查看您自己的注释。
mikeserv

@Caleb如何为其创建typeset别名typeset -g
吉尔斯(Gillles)“所以-别再邪恶了”

@Gilles我在Stephanie建议使用函数方法后想到了这一点,但是我不确定如何在各个shell之间可移植地设置必要的别名扩展选项。也许您可以将其作为我提供的功能的一种可行替代方案。
Caleb 2014年

0

非常感谢@stéphane-chazelas指出了我以前的尝试中的所有问题,这现在似乎可以将数组序列化为stdout或变量。

此技术不会解析输入(不同于declare -a/ declare -p),因此可以安全地防止在序列化文本中恶意插入元字符。

注意:由于read会删除\<newlines>字符对,因此-d ...不能转义换行符,因此必须将其传递给read,然后保留未转义的换行符。

所有这些都在unserialise功能中进行管理。

使用了两个魔术字符,即字段分隔符和记录分隔符(以便可以将多个数组序列化到同一流)。

这些字符可以定义为FSRS但都不能定义为newline字符,因为转义的换行符被删除read

转义字符必须为\反斜杠,因为这是read为了避免将该字符识别为IFS字符。

serialise将序列化为"$@"stdout,serialise_to将序列化为中的变量$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
}

并反序列化:

unserialise data # read from stdin

要么

unserialise data "$serialised_data" # from args

例如

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

(没有尾随换行符)

读回去:

$ 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

要么

unserialise array # read from stdin

Bash read尊重转义字符\(除非您传递-r标志)以除去字符的特殊含义,例如用于输入字段分隔或行定界。

如果要序列化数组而不是单纯的参数列表,则只需将数组作为参数列表传递:

serialise_array "${my_array[@]}"

您可以unserialise像在循环中那样使用read它,因为它只是一个包装的读取-但请记住,流不是用换行符分隔的:

while unserialise array
do ...
done

如果元素包含或不可打印的(在当前的环境)或控制字符,如TAB换行符作为然后它不能正常工作bash,并zsh使其作为$'\xxx'。尝试使用bash -c $'printf "%q\n" "\t"'bash -c $'printf "%q\n" "\u0378"'
–StéphaneChazelas,2016年

该死,你是对的!我将修改答案以不使用printf%q而是使用$ {@ // .. / ..}迭代来转义空格
Sam Liddicott

该解决方案取决于$IFS未修改,现在无法正确还原空数组元素。实际上,使用不同的IFS值并-d ''避免逃脱换行符会更有意义。例如,:用作字段分隔符,仅将其和反斜杠转义并用于IFS=: read -ad '' array导入。
斯特凡Chazelas

是的....当用作读取中的字段分隔符时,我忘记了空格折叠的特殊处理。我很高兴您今天参加比赛!您应该正确使用-d“”以避免转义\ n,但就我而言,我想阅读一系列序列化信息-尽管我将调整答案。谢谢!
Sam Liddicott '16

转义换行符不允许保留它,而是使它消失一次read。backslash-newline for read是一种将逻辑线延伸到另一条物理线的方法。编辑:啊,我看到您已经提到换行符的问题。
斯特凡Chazelas

0

您可以使用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

另一种方法是确保您处理所有这样的'硬引号:

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

或搭配export

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

假设变量的值不包含字符串,则第一个和第二个选项可在任何POSIX Shell中使用:

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

第三个选项适用于任何POSIX Shell,但可以尝试定义其他变量,例如_PWD。不过,事实是,它可能尝试定义的唯一变量是由Shell本身设置和维护的-因此,即使您确实导入export了其中任何一个的值$PWD,例如-Shell也会将它们简单地重置为无论如何,立即获得正确的值-尝试做PWD=any_value,自己看看吧。

并且因为-至少对于GNU- bash调试输出会自动安全地引用以重新输入到shell,所以无论在'硬引号中有多少,此操作都有效"$VAR"

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

$VAR 以后可以在以下路径有效的任何脚本中将其设置为保存的值:

. ./VAR.file

我不确定您在第一个命令中尝试写了什么。$$是正在运行的Shell的PID,您是否得到了错误的引号和均值\$?使用here文档的基本方法可以起作用,但这是棘手的,而不是单行的材料:无论选择什么作为结束标记,都必须选择字符串中未出现的内容。
吉尔斯(Gillles)“所以-别再作恶了” 2014年

$VAR包含时,第二个命令不起作用%。第三个命令并不总是适用于包含多行的值(即使在添加明显缺失的双引号之后)。
吉尔(Gilles)“所以,别再邪恶了”

@Gilles-我知道它的pid-我用它作为设置唯一定界符的简单来源。您所说的“并非总是”到底是什么意思?而且我不明白缺少什么双引号-所有这些都是变量赋值。双引号只会混淆这种情况下的情况。
mikeserv 2014年

@Gilles-我撤回了分配任务-这是的参数env。我仍然很好奇您对多行的意思- sed删除每行直到遇到VAR=最后一行-因此所有行都$VAR继续传递。您能提供一个打破它的例子吗?
mikeserv

抱歉,第三种方法确实有效(使用引号更正)。好吧,假设变量名(在此处VAR)未更改,PWD或者未更改_某些shell定义的名称。第二种方法需要bash;的输出格式-v未标准化(破折号,ksh93,mksh和zsh均无效)。
吉尔斯(Gillles)“所以-别再邪恶了”

-2

几乎相同,但有些不同:

从您的脚本中:

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

以上时间已通过测试。


我可以看到您没有测试!核心逻辑有效,但这并不是难事。困难的是正确地报价,而您却没有做任何事情。尝试变量,其值包含换行符,'*
吉尔“SO-停止作恶”

echo "$LVALUE=\"$RVALUE\""应该保留换行符,并且cfg_file中的结果应类似于:MY_VAR1 =“ Line1 \ nLine 2”因此,当评估MY_VAR1时,它将也包含换行符。当然,如果您的储值本身包含"char,则可能会遇到问题。但是,也可以注意这一点。
vadimbog 2014年

1
顺便说一句,为什么要拒​​绝正确回答这里问题的东西?以上对我来说非常有效,并且可以在脚本中的任何地方使用?
vadimbog 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.