在bash脚本中导出数组

Exporting an array in bash script

我无法将数组从bash脚本导出到另一个bash脚本,如下所示:

1
2
export myArray[0]="Hello"
export myArray[1]="World"

当我这样写的时候,没有问题:

1
export myArray=("Hello""World")

出于多种原因,我需要将数组初始化为多行。你有什么解决办法吗?


Array variables may not (yet) be exported.

来自Ubuntu 10.04下的bash 4.1.5版的手册页。

Chet Ramey(截至2011年的当前bash维护人员)的以下声明可能是有关此"bug"的最官方文档:

There isn't really a good way to encode an array variable into the environment.

http://www.mail-archive.com/[email protected]/msg01774.html


tl;dr:在bash-4.3之前(包括bash-4.3),不直接支持可导出数组,但您可以(有效地)通过以下两种方式之一导出数组:好的。

  • 对子脚本调用方式的简单修改
  • 使用导出的函数存储数组初始化,只需简单修改子脚本即可

或,您可以等待,直到bash-4.3发布(从2014年2月起处于开发/rc状态,请参见array_export in the changelog)。update:this feature is not enabled in 4.3.如果在构建时定义ARRAY_EXPORT,则构建将失败。作者已声明不打算完成此功能。好的。

首先要了解的是,bash环境(更合适的命令执行环境)与环境的posix概念不同。posix环境是非类型name=value对的集合,可以通过各种方式(实际上是一种有限形式的ipc)从进程传递到其子进程。好的。

bash执行环境实际上是其中的一个超集,具有类型化变量、只读和可导出标志、数组、函数等等。这部分解释了为什么EDOCX1(bash builtin)和envprintenv的输出不同。好的。

当您调用另一个bash shell时,您正在启动一个新的进程,您会释放一些bash状态。但是,如果您点源一个脚本,则该脚本在同一个环境中运行;或者如果您通过( )运行子shell,则该环境也会被保留(因为bash分叉,保留其完整状态,而不是使用进程环境重新初始化)。好的。

@lesmana的答案中引用的限制出现了,因为posix环境是简单的name=value对,没有额外的意义,因此没有商定的编码或格式化类型化变量的方法,请参阅下面关于函数的有趣bash quirk,以及bash-4.3中即将发生的更改(建议的数组功能已放弃)。好的。

有两种简单的方法可以做到这一点:使用EDOCX1(内置)将一些bash环境输出为一个或多个declare语句集,这些语句可用于重建"名称"的类型和值。这是基本的系列化,但其他一些答案暗示的复杂性要小一些。declare -p保留数组索引、稀疏数组和麻烦值的引用。对于数组的简单串行化,您可以一行一行地转储值,然后使用read -a myarray恢复它(使用连续的0索引数组,因为read -a自动分配索引)。好的。

这些方法不需要修改要传递数组的脚本。好的。

1
2
declare -p array1 array2 > .bash_arrays       # serialise to an intermediate file
bash -c". .bash_arrays; . otherscript.sh"    # source both in the same environment

上面的bash -c"..."形式的变体有时(mis-)在crontabs中用于设置变量。好的。

备选方案包括:好的。

1
2
declare -p array1 array2 > .bash_arrays       # serialise to an intermediate file
BASH_ENV=.bash_arrays otherscript.sh          # non-interactive startup script

或者,作为一条直线:好的。

1
BASH_ENV=<(declare -p array1 array2) otherscript.sh

最后一个使用进程替换将declare命令的输出作为rc脚本传递。(此方法仅在bash-4.0或更高版本中有效:早期版本无条件使用fstat()rc文件,并一次性使用返回到read()文件的大小;FIFO返回的大小为0,因此无法按预期工作。)好的。

在非交互shell(即shell脚本)中,由BASH_ENV变量指向的文件自动获得源代码。您必须确保正确调用bash,可能使用shebang显式调用"bash",而不是#!/bin/sh,因为bash在历史/posix模式下不会尊重BASH_ENV。好的。

如果您的所有数组名称恰好都有一个公共前缀,那么您可以使用declare -p ${!myprefix*}扩展它们的列表,而不是枚举它们。好的。

您可能不应该尝试使用此方法导出和重新导入整个bash环境,一些特殊的bash变量和数组是只读的,在修改特殊变量时可能会有其他副作用。好的。

(您也可以通过将数组定义序列化为可导出变量,并使用eval,来做一些稍微有点不愉快的事情,但不要鼓励使用eval……好的。

1
2
3
4
5
6
$ array=([1]=a [10]="b c")
$ export scalar_array=$(declare -p array)
$ bash # start a new shell
$ eval $scalar_array
$ declare -p array
declare -a array='([1]="a" [10]="b c")'

)好的。

如上所述,有一个有趣的特性:通过环境导出函数的特殊支持:好的。

1
2
3
function myfoo() {
    echo foo
}

使用export -fset +a来实现这种行为,将在(过程)环境中导致这种情况,用printenv可以看到:好的。

1
2
myfoo=() { echo foo
}

变量为functionname(或functioname()表示向后兼容),其值为() { functionbody }。当随后的bash进程启动时,它将从每个这样的环境变量中重新创建一个函数。如果您查看bash-4.2源文件variables.c,您将看到以() {开头的变量是专门处理的。(尽管禁止使用此语法与declare -f一起创建函数。)更新:"shellshock"安全问题与此功能相关,但现代系统可能会禁用环境中的自动功能导入作为缓解措施。好的。

如果你继续阅读,你会看到一个#if 0(或#if ARRAY_EXPORT)保护代码,它检查从([开始到)结束的变量,以及一条说明"数组变量可能尚未导出"的注释。好消息是,在当前开发版本bash-4.3rc2中,已启用导出索引数组(非关联)的功能。如上文所述,此功能不太可能被启用。好的。

我们可以使用它创建一个函数来恢复所需的任何数组数据:好的。

1
2
3
4
5
6
7
% function sharearray() {
    array1=(a b c d)
}

% export -f sharearray

% bash -c 'sharearray; echo ${array1[*]}'

因此,与前面的方法类似,调用子脚本时使用:好的。

1
bash -c"sharearray; . otherscript.sh"

或者,您可以通过在适当的位置添加来有条件地调用子脚本中的sharearray函数:好的。

1
["`type -t sharearray`" ="function" ] && sharearray

注意,如果执行此操作,则sharearray函数中没有declare -a,因为数组隐式地位于函数的局部,这不是所需的。bash-4.2支持显式地使变量全局化的declare -g,因此(declare -ga可以使用。(由于关联数组需要declare -a,因此在bash-4.2之前,您将无法对关联数组使用此方法。)GNU parallel文档对此方法有有用的变化,请参见手册页中的--env的讨论。好的。

你的措辞也表明你可能与export本身有问题。您可以在创建或修改后导出名称。"exportable"是变量的标志或属性,为了方便您也可以在单个语句中设置和导出。在bash-4.2之前,export只需要一个名称,支持简单(标量)变量或函数名。好的。

即使可以(将来)导出数组,也可能不支持导出选定的索引(切片)(尽管由于数组是稀疏的,所以没有理由不允许这样做)。尽管bash也支持语法declare -a name[0],但是下标被忽略,"name"只是一个普通的索引数组。好的。好啊。


哎呀。我不知道为什么其他的答案会让事情变得如此复杂。bash对此几乎有内置支持。

在导出脚本中:

1
2
3
4
5
myArray=( '  foo"bar  ' $'
'
'
baz)'
)  # an array with two nasty elements

myArray="${myArray[@]@Q}" ./importing_script.sh

(注意,双引号对于正确处理数组元素中的空白是必需的。)

进入importing_script.sh时,myArray环境变量的值正好包含26个字节:

1
2
3
'  foo"bar  ' $'
\
baz)'

然后,下面将重新构建阵列:

1
eval"myArray=( ${myArray} )"

小心!如果您不能信任myArray环境变量的来源,请不要这样做。这个技巧展示了"小鲍比表"的脆弱性。想象一下,如果有人将myArray的值设置为) ; rm -rf / #


正如Lesmana报告的那样,不能导出数组。因此,在通过环境之前必须序列化它们。这种序列化在只有字符串适合的其他地方也很有用(su-c"string",ssh主机"string")。最短的代码方法是滥用"getopt"

1
2
3
4
5
6
7
8
9
10
11
12
13
# preserve_array(arguments). return in _RET a string that can be expanded
# later to recreate positional arguments. They can be restored with:
#   eval set --"$_RET"
preserve_array() {
    _RET=$(getopt --shell sh --options"" -- --"$@") && _RET=${_RET# --}
}

# restore_array(name, payload)
restore_array() {
   local name="$1" payload="$2"
   eval set --"$payload"
   eval"unset $name && $name=("\$@")"
}

这样使用:

1
2
3
4
5
6
7
8
9
10
foo=("1: &&& - *""2: two""3: %# abc" )
preserve_array"${foo[@]}"
foo_stuffed=${_RET}
restore_array newfoo"$foo_stuffed"
for elem in"${newfoo[@]}"; do echo"$elem"; done

## output:
# 1: &&& - *
# 2: two
# 3: %# abc

这不会处理未设置/稀疏数组。您可能能够减少restore_数组中的2个"eval"调用。


基于@mr.spuratic使用BASH_ENV,我在这里通过script -f -c隧道$@

script -c 可用于在另一个pty(和进程组)内运行命令,但不能将任何结构化参数传递给

相反,是一个简单的字符串,用作system库调用的参数。

我需要将外部bash的$@导入脚本调用的bash的$@

由于declare -p不能取@,这里我使用了magic bash变量_(使用虚拟的第一个数组值,因为它会被bash覆盖)。这就省去了我对任何重要变量的践踏:

概念证明:BASH_ENV=<( declare -a _=("""$@") && declare -p _ ) bash -c 'set --"${_[@]:1}" && echo"$@"'

"但是,"你说,"你在向bash传递参数——我确实是这样,但这只是一个已知字符的简单字符串。"这是script使用的

SHELL=/bin/bash BASH_ENV=<( declare -a _=("""$@") && declare -p _ && echo 'set --"${_[@]:1}"') script -f -c 'echo"$@"' /tmp/logfile

它给了我这个包装函数in_pty

in_pty() {
SHELL=/bin/bash BASH_ENV=<( declare -a _=("""$@") && declare -p _ && echo 'set --"${_[@]:1}"') script -f -c 'echo"$@"' /tmp/logfile }

或者这个函数将包装器作为makefiles的可组合字符串:

in_pty=bash -c 'SHELL=/bin/bash BASH_ENV=<( declare -a _=("""$$@") && declare -p _ && echo '"'"'set --"$${_[@]:1}"'"'"') script -qfc '"'"'"$$@"'"'"' /tmp/logfile' --

...

$(in_pty) test --verbose $@ $^


多亏了@st_phane chazelas,他指出了我以前的尝试中的所有问题,现在似乎可以将数组序列化为stdout或变量。

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

注意:换行不转义,因为read删除了\字符对,所以必须将-d ...传递给read,然后保留未转义的换行。

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

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

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

转义字符必须是\反斜杠,因为这是read用来避免字符被识别为IFS字符的。

serialise"$@"序列化为stdout,serialise_to将序列化为$1中指定的varable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}

与以下内容不符:

1
unserialise data # read from stdin

1
unserialise data"$serialised_data" # from args

例如

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

(不带尾随换行符)

读回:

1
2
3
4
5
6
7
8
9
10
$ 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[@]/#/$'
'}"


Now is the time
For all good men
To drink $drink
At the `party`
Party   Party   Party

1
unserialise array # read from stdin

bash的read尊重转义字符\(除非传递-r标志),以删除字符的特殊含义,例如输入字段分隔或行分隔。

如果要序列化一个数组而不仅仅是参数列表,那么只需将数组作为参数列表传递:

1
serialise_array"${my_array[@]}"

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

1
2
3
while unserialise array
do ...
done


对于没有空格的值的数组,我使用了一组简单的函数来迭代每个数组元素并连接数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_arrayToStr(){
    array=($@)

    arrayString=""
    for (( i=0; i<${#array[@]}; i++ )); do
        if [[ $i == 0 ]]; then
            arrayString=""${array[i]}""
        else
            arrayString="${arrayString} "${array[i]}""
        fi
    done

    export arrayString="(${arrayString})"
}

_strToArray(){
    str=$1

    array=${str//"/}
    array=(${array//[()]/""})

    export array=${array[@]}
}

第一个函数通过添加左括号和右括号并转义所有双引号将数组转换为字符串。第二个函数将去掉引号和括号,并将它们放入一个虚拟数组中。

为了导出数组,您将传入原始数组的所有元素:

1
2
array=(foo bar)
_arrayToStr ${array[@]}

此时,数组已导出到值$arraystring中。要在目标文件中导入数组,请重命名该数组并执行相反的转换:

1
2
_strToArray"$arrayName"
newArray=(${array[@]})

你(嗨!)可以使用这个,不需要写文件,对于Ubuntu12.04,bash 4.2.24

此外,还可以导出多行数组。

CAT>>导出阵列.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function FUNCarrayRestore() {
    local l_arrayName=$1
    local l_exportedArrayName=${l_arrayName}_exportedArray

    # if set, recover its value to array
    if eval '[[ -n ${'$l_exportedArrayName'+dummy} ]]'; then
        eval $l_arrayName'='`eval 'echo $'$l_exportedArrayName` #do not put export here!
    fi
}
export -f FUNCarrayRestore

function FUNCarrayFakeExport() {
    local l_arrayName=$1
    local l_exportedArrayName=${l_arrayName}_exportedArray

    # prepare to be shown with export -p
    eval 'export '$l_arrayName
    # collect exportable array in string mode
    local l_export=`export -p \
        |grep"^declare -ax $l_arrayName=" \
        |sed 's"^declare -ax '$l_arrayName'"export '$l_exportedArrayName'"'`
    # creates exportable non array variable (at child shell)
    eval"$l_export"
}
export -f FUNCarrayFakeExport

在终端bash上测试此示例(与bash 4.2.24一起使用):

1
2
3
4
5
6
7
source exportArray.sh
list=(a b c)
FUNCarrayFakeExport list
bash
echo ${list[@]} #empty :(
FUNCarrayRestore list
echo ${list[@]} #profit! :D

我可以在这里改进一下

附言:如果有人澄清/改进/使其不受影响,我想知道/看到,THX!D


我正在编辑另一篇文章,犯了一个错误。哦。不管怎样,也许这会有帮助?https://stackoverflow.com/a/11944320/1594168

注意,因为shell的数组格式在bash或任何其他shell端都是未记录的,以平台无关的方式返回shell数组是非常困难的。你必须检查版本,还要制作一个简单的脚本来具体化所有的将数组放入其他进程可以解析为的文件中。

但是,如果您知道要带回家的数组的名称,那么有一种方法,虽然有点脏。

让我说我有

1
2
3
MyAry[42]="whatever-stuff";
MyAry[55]="foo";
MyAry[99]="bar";

所以我想把它带回家

1
2
3
name_of_child=MyAry
take_me_home="`declare -p ${name_of_child}`";
export take_me_home="${take_me_home/#declare -a ${name_of_child}=/}"

通过从子流程中进行检查,我们可以看到它正在被导出。

1
echo""|awk '{print"from awk =["ENVIRON["take_me_home"]"]";  }'

结果:

1
from awk =['([42]="whatever-stuff" [55]="foo" [99]="bar")']

如果必须这样做,请使用env var来转储它。

1
env > some_tmp_file

然后

在运行另一个脚本之前,

1
2
# This is the magic that does it all
source some_tmp_file