在 bash 或 zsh 中序列化 shell 变量

在 bash 或 zsh 中序列化 shell 变量

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

有没有一种便携的方法可以做到这一点? (我不这么认为)

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

答案1

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

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

typeset是的,在 bash 和 zsh 中,您可以使用内置函数和参数以易于检索的方式序列化变量的内容-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 则使用 来实现此功能,typeset但由于 bash 有一个别名,因此我们可以typeset在此处使用它以实现 ksh 兼容性。

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

上面的实现非常简单,但是如果您经常调用它,您可能想给自己一个实用函数以使其更容易。此外,如果您尝试将上述内容包含在自定义函数中,您将遇到变量作用域的问题。这个版本应该可以消除这些问题。

请注意所有这些,为了保持 bash/zsh 交叉兼容性,我们将修复这两种情况typesetdeclare因此代码应该在任一或两个 shell 中工作。这会增加一些体积和混乱,如果您只对一个或另一个外壳执行此操作,则可以消除这些体积和混乱。

为此使用函数(或在其他函数中包含代码)的主要问题是,函数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为第一个参数。有必要只匹配第一次出现,因为,斯蒂芬·查泽拉斯在注释中正确指出,否则它也会匹配序列化字符串包含文字换行符后跟单词声明或排版的情况。

除了纠正我最初的解析之外失礼,史蒂芬也建议一种不太脆弱的破解方法,不仅可以解决解析字符串的问题,而且可以是一个有用的钩子,通过使用包装函数来重新定义重新获取数据时所采取的操作,从而添加附加功能。这假设您不使用声明或排版命令玩任何其他游戏,但在您将此功能作为您自己的另一个功能的一部分包含在内的情况下,或者您无法控制正在写入的数据以及是否或没有-g添加标志。类似的事情也可以使用别名来完成,请参阅吉尔斯的回答进行实施。

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

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"

答案2

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

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

答案3

全部序列化 — POSIX

在任何 POSIX shell 中,您可以使用以下命令序列化所有环境变量export -p。这不包括非导出的 shell 变量。输出被正确引用,以便您可以在同一个 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)。输出包含正确的声明,以便在读回时导出环境变量(但如果在读回时已经导出变量,则不会取消导出),以便将数组作为数组读回等。这里还有输出已正确引用,但仅保证在同一 shell 中可读。您可以传递一组变量以在命令行上序列化;如果您不传递任何变量,则所有变量都会被序列化。

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 上,如果尝试读回只读变量,则会收到错误消息。

答案4

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对其中任何一个执行 import 的值 - 例如$PWD- shell 也会简单地将它们重置为无论如何,立即得到正确的值 - 尝试一下PWD=any_value并亲自看看。

而且因为 - 至少对于 GNU 来说- 调试输出会自动安全引用以重新输入到 shell,因此无论中的硬引号bash数量如何,这都有效:'"$VAR"

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

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

. ./VAR.file

相关内容