在bash脚本中导出数组

时间:2011-04-06 09:48:19

标签: arrays bash export

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

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

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

export myArray=("Hello" "World")

由于多种原因,我需要将数组初始化为多行。你有任何解决方案吗?

13 个答案:

答案 0 :(得分:44)

  

可能尚未导出数组变量。

来自ubuntu 10.04下的bash版本4.1.5的联机帮助页。

Chet Ramey(截至2011年的当前bash维护者)的以下声明可能是关于此“bug”的最官方文档:

  

将数组变量编码到环境中并不是一个好方法。

http://www.mail-archive.com/bug-bash@gnu.org/msg01774.html

答案 1 :(得分:31)

TL; DR:可导出的数组不受直接支持,包括bash-4.3,但您可以(有效地)以两种方式之一导出数组:< / p>

  • 对子脚本调用方式的简单修改
  • 使用导出的函数来存储数组初始化,并对子脚本进行简单修改

或者,您可以等到bash-4.3发布(从2014年2月开始/处于RC状态,请参阅更改日志中的ARRAY_EXPORT)。更新:此功能在4.3中启用。如果在构建时定义ARRAY_EXPORT,则构建将失败。作者has stated并未计划完成此功能。


首先要理解的是bash环境(更恰当command execution environment)与环境的POSIX概念不同。 POSIX environment是未列出的name=value对的集合,可以从流程传递到其子in various ways(实际上是IPC的有限形式)。

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

当你调用另一个bash shell时,你开始一个新进程,你就会失去一些bash状态。但是,如果您点源脚本,脚本将在同一环境中运行;或者如果您通过( )运行子shell,则环境也会被保留(因为bash forks,保留其完整状态,而不是使用进程环境重新初始化)。


@ lesmana的答案中引用的限制是因为POSIX环境只是name=value对而没有额外的意义,所以没有同意的方式来编码或格式化类型变量,见下文关于函数的一个有趣的bash怪癖,以及即将发生的bash-4.3 的变化(建议的数组功能被抛弃)。

有几种简单的方法可以使用declare -p(内置)将一些bash环境输出为一组一个或多个declare语句,可用于重构名称&#34;的类型和价值。这是基本的serialisation,但其中一些其他答案意味着更少complexitydeclare -p保留数组索引,稀疏数组和引用麻烦的值。对于数组的简单序列化,您可以逐行转储值,并使用read -a myarray来恢复它(使用连续的0索引数组,因为read -a自动分配索引)。

这些方法不需要对要传递数组的脚本进行任何修改。

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 "..."形式的变化有时(错误地)用于crontabs中以设置变量。

替代方案包括:

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

或者,作为一个单行:

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

最后一个使用进程替换declare命令的输出作为rc脚本传递。 (此方法仅适用于bash-4.0或更高版本:早期版本无条件fstat() rc文件,并且一次性使用返回到read()文件的大小; FIFO返回大小为0,因此赢得& #39;按照希望工作。)

非交互式shell (即shell脚本)中,BASH_ENV变量指向的文件为automatically sourced。你必须确保正确调用bash,可能使用shebang来调用&#34; bash&#34;明确地,而不是#!/bin/sh,因为bash在历史/ POSIX模式下不会尊重BASH_ENV

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

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

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

$ 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")'


如上所述,有一个有趣的怪癖:通过环境导出功能的特殊支持:

function myfoo() {
    echo foo
}
使用export -fset +a启用此行为的

将在(进程)环境中生成此内容,并使用printenv显示:

myfoo=() { echo foo
}

变量为functionname(或functioname()以实现向后兼容性),其值为() { functionbody }。 当后续的bash进程启动时,它将从每个这样的环境变量中重新创建一个函数。如果您查看bash-4.2源文件variables.c,您将看到以() {开头的变量被特别处理。 (虽然禁止使用declare -f使用此语法创建函数。)更新:&#34; shellshock"安全问题与此功能相关,当前系统可能会禁用自动从环境中导入的函数作为缓解。

如果您继续阅读,您会看到#if 0(或#if ARRAY_EXPORT)保护代码,用于检查以([开头并以)结尾的变量,并且注释说明&#34; 数组变量可能尚未导出&#34;。 好消息是,在当前的开发版本bash-4.3rc2中,导出索引数组(非关联)的能力已启用此功能不太可能启用,如上所述。

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

% function sharearray() {
    array1=(a b c d)
}

% export -f sharearray 

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

因此,与前一种方法类似,使用以下命令调用子脚本:

bash -c "sharearray; . otherscript.sh"

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

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

注意declare -a函数中没有sharearray,如果这样做,数组隐式本地到函数,这不是想要的。 bash-4.2支持显式地使变量成为全局变量的declare -g,因此可以使用(declare -ga)。 (由于关联数组需要 declare -A,您无法在bash-4.2之前将此方法用于关联数组。)GNU parallel文档具有有用的变体有关此方法的信息,请参阅man page中对--env的讨论。


您提出的问题也表明您可能遇到export本身的问题。您可以在创建或修改名称后导出名称。 &#34;导出&#34;是变量的标志或属性,为方便起见,您还可以在单​​个语句中设置和导出。高达bash-4.2 export只需要一个名称,或者支持简单(标量)变量或函数名称。

即使您(将来)可以导出数组,也可能不支持导出所选索引(切片)(尽管由于数组稀疏,因此没有理由不允许)。虽然bash也支持语法declare -a name[0],但是下标被忽略,&#34; name&#34;只是一个普通的索引数组。

答案 2 :(得分:8)

哎呀。我不知道为什么其他答案让这么复杂。 Bash几乎内置了对此的支持。

在导出脚本中:

myArray=( '  foo"bar  ' $'\n''\nbaz)' )  # an array with two nasty elements

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

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

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

'  foo"bar  ' $'\n\\nbaz)'

然后以下内容将重新构建数组:

eval "myArray=( ${myArray} )"

小心!如果您不信任eval环境变量的来源,请不要myArray这样。这个技巧表现出"Little Bobby Tables"漏洞。想象一下,如果有人要将myArray的值设置为) ; rm -rf / #

答案 3 :(得分:2)

环境只是键-值对的集合,它们都是字符串。一个适用于任何类型数组的正确解决方案都可以

  • 将每个元素保存在不同的变量中(例如MY_ARRAY_0 = myArray [0])。由于dynamic variable names而变得复杂。
  • 将数组保存在文件系统中(声明-p myArray> file)。
  • 将所有数组元素序列化为单个字符串。

这些在其他帖子中都有介绍。如果您知道您的值永远不会包含某个字符(例如|)并且您的键是连续的整数,则可以将数组另存为定界列表:

export MY_ARRAY=$(IFS='|'; echo "${myArray[*]}")

并在子进程中将其还原:

IFS='|'; myArray=($MY_ARRAY); unset IFS

答案 4 :(得分:1)

正如lesmana报道的那样,您无法导出数组。所以你必须在通过环境之前序列化它们。这个序列化也很有用,只有一个字符串适合(su -c'string',ssh host'string')。执行此操作的最短代码方式是滥用“getopt”

# 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=("\$@")"
}

像这样使用:

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_array中减少2'eval'调用。

答案 5 :(得分:0)

我正在编辑一个不同的帖子并犯了一个错误。 Augh。无论如何,也许这可能会有所帮助? https://stackoverflow.com/a/11944320/1594168

请注意,因为shell的数组格式在bash或任何其他shell的一侧没有记录, 以平台无关的方式返回shell数组非常困难。 您必须检查版本,并制作一个简洁的脚本,以便将所有内容连接起来 shell数组到其他进程可以解析的文件中。

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

让我说我有

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

所以我想带回家

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

我们可以通过检查子流程

来看到它被导出
echo ""|awk '{print "from awk =["ENVIRON["take_me_home"]"]";  }'

结果:

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

如果我们绝对必须,请使用env var来转储它。

env > some_tmp_file

然后

在运行另一个脚本之前,

# This is the magic that does it all
source some_tmp_file

答案 6 :(得分:0)

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

此外,您的多行阵列可以导出。

cat&gt;&gt; exportArray.sh

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

source exportArray.sh
list=(a b c)
FUNCarrayFakeExport list
bash
echo ${list[@]} #empty :(
FUNCarrayRestore list
echo ${list[@]} #profit! :D

我可以改进它here

PS:如果有人清除/改进/ makeItRunFaster我想知道/看,thx! :d

答案 7 :(得分:0)

对于值不带空格的数组,我一直在使用一组简单的函数迭代每个数组元素并连接数组:

_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[@]}
}

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

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

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

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

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

答案 8 :(得分:0)

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

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

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

所有这些都在unserialise函数中进行管理。

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

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

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

serialise会将"$@"序列化为标准输出,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

e.g。

$ 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

答案 9 :(得分:0)

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

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

<command>是一个简单的字符串,可作为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 }

或此无函数包装器作为Makefile的可组合字符串:

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

...

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

答案 10 :(得分:0)

我为此编写了自己的函数,并使用IFS改进了方法:

功能

  • 不调用$(...),因此不会产生另一个bash shell进程
  • ?|字符序列化为?00?01序列并返回,因此可以在具有这些字符的数组上使用
  • 将序列化/反序列化之间的行返回字符与其他字符一样处理
  • cygwin bash 3.2.48Linux bash 4.3.48中进行了测试
function tkl_declare_global()
{
  eval "$1=\"\$2\"" # right argument does NOT evaluate
}

function tkl_declare_global_array()
{
  local IFS=$' \t\r\n' # just in case, workaround for the bug in the "[@]:i" expression under the bash version lower than 4.1
  eval "$1=(\"\${@:2}\")"
}

function tkl_serialize_array()
{
  local __array_var="$1"
  local __out_var="$2"

  [[ -z "$__array_var" ]] && return 1
  [[ -z "$__out_var" ]] && return 2

  local __array_var_size
  eval declare "__array_var_size=\${#$__array_var[@]}"

  (( ! __array_var_size )) && { tkl_declare_global $__out_var ''; return 0; }

  local __escaped_array_str=''

  local __index
  local __value
  for (( __index=0; __index < __array_var_size; __index++ )); do
    eval declare "__value=\"\${$__array_var[__index]}\""
    __value="${__value//\?/?00}"
    __value="${__value//|/?01}"
    __escaped_array_str="$__escaped_array_str${__escaped_array_str:+|}$__value"
  done

  tkl_declare_global $__out_var "$__escaped_array_str"

  return 0
}

function tkl_deserialize_array()
{
  local __serialized_array="$1"
  local __out_var="$2"

  [[ -z "$__out_var" ]] && return 1
  (( ! ${#__serialized_array} )) && { tkl_declare_global $__out_var ''; return 0; }

  local IFS='|'
  local __deserialized_array=($__serialized_array)

  tkl_declare_global_array $__out_var

  local __index=0
  local __value
  for __value in "${__deserialized_array[@]}"; do
    __value="${__value//\?01/|}"
    __value="${__value//\?00/?}"
    tkl_declare_global $__out_var[__index] "$__value"
    (( __index++ ))
  done

  return 0
}

示例:

a=($'1 \n 2' "3\"4'" 5 '|' '?')
tkl_serialize_array a b
tkl_deserialize_array "$b" c

答案 11 :(得分:0)

我认为您可以尝试这种方式(在 export 之后通过 sourcing 您的脚本):

export myArray =(Hello World)

。 yourScript.sh

答案 12 :(得分:0)

虽然这个问题/答案已经很老了,但在搜索 "bash serialize array" 时这篇文章似乎是最热门的

而且,虽然最初的问题与序列化/反序列化数组并不完全相关,但答案似乎确实朝着这个方向发展。

所以……我提供了我的解决方案:

优点
  • 所有核心 Bash 概念
  • 没有评估
  • 无子命令
缺点
  • 函数将变量名作为参数(相对于实际值)
  • 序列化要求至少一个字符出现在数组中

serialize_array.sh

#!/usr/bin/env bash

##
# serialize_array
# Serializes a bash array to a string, with a configurable seperator.
#
# $1 = source varname ( contains array to be serialized )
# $2 = target varname ( will contian the serialized string )
# $3 = seperator ( optional, defaults to $'\x01' )
#
# example:
#
#    my_arry=( one "two three" four )
#    serialize_array my_array my_string '|'
#    declare -p my_string
#
# result:
#
#    declare -- my_string="one|two three|four"
#
function serialize_array() {
    declare -n _array=${1} # _array => local
    local IFS
    IFS="${3:-$'\x01'}"
    printf -v ${2} "%s" "${_array[*]}"
}

##
# deserialize_array
# Deserializes a string into a bash array, with a configurable seperator.
#
# $1 = source varname ( contains string to be deserialized )
# $2 = target varname ( will contain the deserialized array )
# $3 = seperator ( optional, defaults to $'\x01' )
#
# example:
#
#    my_string="one|two three|four"
#    deserialize_array my_string my_array '|'
#    declare -p my_array
#
# result:
#
#    declare -a my_array=([0]="one" [1]="two three" [2]="four")
#
function deserialize_array() {
    declare -n _array=${2} #_array => local
    local IFS
    IFS="${3:-$'\x01'}"
    _array=( ${!1} )
}