在bash中将数组作为参数传递


188

如何将数组作为参数传递给bash函数?

注意:在此处没有关于Stack Overflow的答案之后,我自己发布了一些粗略的解决方案。它只允许传递一个数组,它是参数列表的最后一个元素。实际上,它根本不是传递数组,而是传递其元素的列表,这些元素可以通过named_function()重新组装成数组,但是它对我有用。如果有人知道更好的方法,请随时在此处添加。


1
在这里,您可以得到很好的参考和大量示例。
Artem Barger 2009年

16
Errr ...在同一分钟内,对一个有5年历史的问题进行了3次否决?
DevSolar 2014年

Answers:


219

您可以使用以下方式将多个数组作为参数传递:

takes_ary_as_arg()
{
    declare -a argAry1=("${!1}")
    echo "${argAry1[@]}"

    declare -a argAry2=("${!2}")
    echo "${argAry2[@]}"
}
try_with_local_arys()
{
    # array variables could have local scope
    local descTable=(
        "sli4-iread"
        "sli4-iwrite"
        "sli3-iread"
        "sli3-iwrite"
    )
    local optsTable=(
        "--msix  --iread"
        "--msix  --iwrite"
        "--msi   --iread"
        "--msi   --iwrite"
    )
    takes_ary_as_arg descTable[@] optsTable[@]
}
try_with_local_arys

将回显:

sli4-iread sli4-iwrite sli3-iread sli3-iwrite  
--msix  --iread --msix  --iwrite --msi   --iread --msi   --iwrite

编辑/注释:(来自下面的评论)

  • descTableoptsTable作为名称传递并在函数中扩展。因此没有$,当作为参数给出时不需要。
  • 请注意,即使使用descTable等定义local,因为本地对其调用的函数可见。
  • !${!1}扩展ARG 1可变。
  • declare -a 只是使索引数组变得明确,并不是严格必要的。

14
需要注意的一件事是,如果原始数组是稀疏的,则接收函数中的数组将没有相同的索引。
暂停,直到另行通知。

13
这很棒,但是Ken或某人可以解释一些使我困惑的事情:1-我以为descTable和optsTable作为函数参数传递时必须以$为前缀。2-在“ takes ...”的第一行中,为什么需要显式的数组声明?3-什么!表示$ {!1}中的含义,为什么[@]在这里不需要甚至不允许?-可行,根据我的测试似乎需要所有这些细节,但是我想了解原因!
Jan Hettich 2010年

8
1:descTable和optsTable只是作为名称传递,因此没有$,只能在调用的函数中对其进行扩展2:不是完全确定,但是我认为这不是必须的3 :! 之所以使用,是因为传递给函数的参数需要扩展两次:$ 1扩展为“ descTable [@]”,而应扩展为“ $ {descTable [@]}”。$ {!1}语法就是这样做的。
Elmar Zander 2012年

8
我认为“ declare -a”部分不是必需的。括号的存在已经将赋值的LHS定义为数组。
Erik Aronesty

3
这个答案帮助我解决了一个问题。但是,我想指出的是,在我的机器上(使用bash 4.3.42),“ $ {!1}”和“ $ {!2}”需要删除引号。如果不这样做,则将原始数组的值读取为一个字符串,并分别分配给argAry1 [0]和argAry2 [0],这基本上意味着数组结构丢失了。
user.friendly

85

注意:这是我在堆栈溢出问题上找不到答案后发布的解决方案。它只允许传递一个数组,它是参数列表的最后一个元素。实际上,它根本不是传递数组,而是传递其元素的列表,这些元素可以通过named_function()重新组装成数组,但是它对我有用。后来,肯(Ken)发表了他的解决方案,但我在这里保留了“历史”参考。

calling_function()
{
    variable="a"
    array=( "x", "y", "z" )
    called_function "${variable}" "${array[@]}"
}

called_function()
{
    local_variable="${1}"
    shift
    local_array=("${@}")
}

由TheBonsai改进,谢谢。


19
事实发生三年后,这个答案(仅出于历史原因而保留)在几天之内得到了两次投票。就像在SO上一样令人遗憾的是,没有说明为什么人们认为这样做是必要的。请注意,此答案早于所有其他答案,并且我接受Ken的答案为最佳解决方案。我完全知道这还远未达到完美,但是四个月来这是SO上最好的。在Ken的完美解决方案中仅次于第二名的两年后,为什么要拒​​绝它,这超出了我的范围。
DevSolar 2012年

@geirha:我想请您检查谁发布了问题,谁发布了这个答案以及谁可能接受了您所说的“不好”的答案。;-)您可能还想查看问题中的注释,该注释指出了为什么此解决方案不及Ken的解决方案。
DevSolar 2014年

2
我知道您问了这个问题,您写下了这个答案,并且您接受了错误的答案。这就是为什么我这样说。可接受的答案很差的原因是因为它试图通过引用传递数组,这是您应该真正避免的事情。另外,该示例将多个参数混入单个字符串中。如果确实需要通过引用传递数组,那么bash就是一门错误的语言。即使使用bash 4.3的新nameref变量,也无法安全避免名称冲突(循环引用)。
盖尔哈2014年

4
好吧,如果您包含每个数组的元素数,则可以传递多个数组。called_function "${#array[@]}" "${array[@]}" "${#array2[@]}" "${array2[@]}"等等……仍然有一些明显的限制,但实际上,以一种语言支持的方式解决该问题要好得多,而不是试图使该语言以其他语言习惯的方式工作。
盖尔哈

1
@geirha:好吧,我想我们必须同意我们不同意,而您将不得不让我作为最能回答我问题的法官。就我个人而言,无论如何用语言,我都更喜欢通过引用传递数组(以节省数据复制);当替代方案是向后弯腰并将数组大小作为附加参数传递时,
更是如此

38

评论Ken Bertelson解决方案并回答Jan Hettich:

这个怎么运作

函数中的takes_ary_as_arg descTable[@] optsTable[@]try_with_local_arys()发送:

  1. 这实际上是创建的副本descTableoptsTable它们的访问阵列takes_ary_as_arg功能。
  2. takes_ary_as_arg()函数接收descTable[@]optsTable[@]作为字符串,表示$1 == descTable[@]$2 == optsTable[@]
  3. takes_ary_as_arg()函数的开头,它使用${!parameter}语法,称为间接引用或有时称为双重引用,这意味着我们将使用的扩展的值,而不是$1使用的值$1,例如:

    baba=booba
    variable=baba
    echo ${variable} # baba
    echo ${!variable} # booba

    同样地$2

  4. 将其放置为具有expand的数组(在后面的方括号)argAry1=("${!1}")会创建argAry1一个数组,就像直接在此处编写一样。在那里不是必需的。=descTable[@]argAry1=("${descTable[@]}")declare

注意:值得一提的是,使用此括号形式的数组初始化根据IFS内部字段分隔符(默认情况下为tabnewlinespace)来初始化新数组。在这种情况下,由于它使用了[@]表示法,因此每个元素本身都被视为自己被引用了(与[*])。

我的预订

在中BASH,局部变量作用域是当前函数以及从中调用的每个子函数,这转化为以下事实:takes_ary_as_arg()函数“看到”了这些函数,descTable[@]并且optsTable[@]数组数组,因此该函数起作用(请参见上面的说明)。

在这种情况下,为什么不直接查看这些变量呢?就像在这里写:

argAry1=("${descTable[@]}")

参见上面的说明,该说明仅descTable[@]根据当前值复制数组的值IFS

综上所述

从本质上讲,这与往常一样没有价值传递。

我也想强调丹尼斯·威廉姆森的上述评论:稀疏数组(没有定义所有键的数组-其中带有“孔”)将无法按预期工作-我们将松开键并“压缩”该数组。

话虽如此,我确实看到了泛化的价值,因此函数可以在不知道名称的情况下获取数组(或副本):

  • 对于“复制”:此技术足够好,只需要保持警惕,索引(键)就消失了。
  • 对于真实副本:我们可以使用eval作为密钥,例如:

    eval local keys=(\${!$1})

然后循环使用它们创建副本。注意:这里!不使用它以前的间接/双重计算,而是在数组上下文中返回数组索引(键)。

  • 当然,如果我们要传递descTableoptsTable字符串(不带[@]),我们可以将数组本身(如通过引用使用)与eval。用于接受数组的通用函数。

2
Ken Bertelson解释背后的机制很好的解释。对于“既然如此,为什么不直接查看这些变量本身呢?”这个问题,我将回答:仅仅是为了函数的重用。假设我需要先用调用一个函数Array1,然后用调用Array2,传递数组名称变得很方便。
gfrigon 2014年

好的答案,我们需要更多这样的解释!
爱德华·洛佩兹

22

这里的基本问题是设计/实现数组的bash开发人员确实搞砸了狗狗。他们认为这${array}只是简而言之${array[0]},这是一个严重的错误。特别是当你考虑到${array[0]}没有意义并且如果数组类型是关联的,则求值为空字符串时。

分配数组采用array=(value1 ... valueN)value具有语法的形式[subscript]=string,从而将值直接分配给数组中的特定索引。这样一来,就可以有两种类型的数组,即数字索引和哈希索引(在bash术语中称为关联数组)。它还使您可以创建稀疏的数字索引数组。省略该[subscript]=部分是数字索引数组的简写,它从序数索引0开始,并从赋值语句中的每个新值递增。

因此,${array}应该对整个数组,索引和所有对象求值。它的计算结果应为赋值语句的倒数。任何三年级CS专业都应该知道这一点。在这种情况下,此代码将完全按照您期望的那样工作:

declare -A foo bar
foo=${bar}

然后,按值将数组传递给函数,然后将一个数组分配给另一个数组,这将在其余Shell语法中规定。但是由于他们没有正确执行此操作,因此赋值运算符=不适用于数组,并且数组不能按值传递给函数或子shell或一般的输出(echo ${array}),而无需代码来检查所有内容。

因此,如果操作正确,那么以下示例将说明如何在bash中更好地利用数组:

simple=(first=one second=2 third=3)
echo ${simple}

结果输出应为:

(first=one second=2 third=3)

然后,数组可以使用赋值运算符,并按值传递给函数甚至其他shell脚本。通过输出到文件可以轻松存储,也可以从文件轻松加载到脚本中。

declare -A foo
read foo <file

las,我们已经被一个本来就不算什么的bash开发团队失望了。

这样,要将数组传递给函数,实际上只有一个选项,那就是使用nameref功能:

function funky() {
    local -n ARR

    ARR=$1
    echo "indexes: ${!ARR[@]}"
    echo "values: ${ARR[@]}"
}

declare -A HASH

HASH=([foo]=bar [zoom]=fast)
funky HASH # notice that I'm just passing the word 'HASH' to the function

将产生以下输出:

indexes: foo zoom
values: bar fast

由于这是通过引用传递的,因此您也可以在函数中分配给数组。是的,被引用的数组必须具有全局范围,但是考虑到这是Shell脚本,这应该没什么大不了的。要将按值关联或稀疏索引的数组传递给函数,需要将所有索引和值作为单个字符串扔到参数列表中(如果是大数组则不太有用),如下所示:

funky "${!array[*]}" "${array[*]}"

然后在函数内部编写一堆代码以重新组装数组。


1
local -n与接受的答案相比,使用的解决方案更好,最新。该解决方案也适用于任何类型的变量。此答案中列出的示例可以简化为local -n ARR=${1}。但是/ 的-n选项仅在Bash 4.3及更高版本中可用。localdeclare
richardjsimkins's

太好了!小陷阱:如果您传递的变量名称与函数的本地参数(例如funky ARR)相同,则shell将发出警告circular name reference,因为基本上该函数会尝试这样做local -n ARR=ARR。关于这个话题的好讨论
Gene Pavlovsky

5

DevSolar的回答有一点我不明白(也许他有一个特定的理由,但我想不到):他从位置参数逐个元素地,迭代地设置数组。

一个更简单的方法是

called_function()
{
  ...
  # do everything like shown by DevSolar
  ...

  # now get a copy of the positional parameters
  local_array=("$@")
  ...
}

1
我之所以不这样做,是因为直到几天前我才真正开始使用bash数组。以前,如果它变得复杂,我会改用Perl,这是我目前的工作所没有的选择。感谢您的提示!
DevSolar 2009年


3

将多个数组作为参数传递的一种简单方法是使用字符分隔的字符串。您可以这样调用脚本:

./myScript.sh "value1;value2;value3" "somethingElse" "value4;value5" "anotherOne"

然后,您可以像这样将其提取到代码中:

myArray=$1
IFS=';' read -a myArray <<< "$myArray"

myOtherArray=$3
IFS=';' read -a myOtherArray <<< "$myOtherArray"

这样,您实际上可以将多个数组作为参数传递,而不必是最后一个参数。


1

即使在有空格的情况下,这也可以使用:

format="\t%2s - %s\n"

function doAction
{
  local_array=("$@")
  for (( i = 0 ; i < ${#local_array[@]} ; i++ ))
    do
      printf "${format}" $i "${local_array[$i]}"
  done
  echo -n "Choose: "
  option=""
  read -n1 option
  echo ${local_array[option]}
  return
}

#the call:
doAction "${tools[@]}"

2
我不知道这是什么意思。这只是正常的参数传递。“ $ @”语法适用于空格:“ $ @”等效于“ $ 1”“ $ 2” ...
Andreas Spindler 2013年

我可以将2个数组传递给函数吗?
pihentagy

1

通过一些技巧,您实际上可以将命名参数与数组一起传递给函数。

我开发的方法使您可以访问传递给以下函数的参数:

testPassingParams() {

    @var hello
    l=4 @array anArrayWithFourElements
    l=2 @array anotherArrayWithTwo
    @var anotherSingle
    @reference table   # references only work in bash >=4.3
    @params anArrayOfVariedSize

    test "$hello" = "$1" && echo correct
    #
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct
    # etc...
    #
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
    #
    test "$anotherSingle" = "$8" && echo correct
    #
    test "${table[test]}" = "works"
    table[inside]="adding a new value"
    #
    # I'm using * just in this example:
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}

fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."

test "${assocArray[inside]}" = "adding a new value"

换句话说,不仅可以通过名称来调用参数(这构成了更易读的内核),而且实际上可以传递数组(以及对变量的引用-该功能仅在bash 4.3中有效)!另外,映射变量都在本地范围内,就像$ 1(及其他)一样。

使这项工作有效的代码非常轻巧,并且可以在bash 3和bash 4中使用(这是我测试过的唯一版本)。如果您对更多类似的技巧感兴趣,这些技巧使使用bash进行开发变得更加轻松便捷,则可以看看我的Bash Infinity Framework,下面的代码就是为此目的而开发的。

Function.AssignParamLocally() {
    local commandWithArgs=( $1 )
    local command="${commandWithArgs[0]}"

    shift

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
    then
        paramNo+=-1
        return 0
    fi

    if [[ "$command" != "local" ]]
    then
        assignNormalCodeStarted=true
    fi

    local varDeclaration="${commandWithArgs[1]}"
    if [[ $varDeclaration == '-n' ]]
    then
        varDeclaration="${commandWithArgs[2]}"
    fi
    local varName="${varDeclaration%%=*}"

    # var value is only important if making an object later on from it
    local varValue="${varDeclaration#*=}"

    if [[ ! -z $assignVarType ]]
    then
        local previousParamNo=$(expr $paramNo - 1)

        if [[ "$assignVarType" == "array" ]]
        then
            # passing array:
            execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
            eval "$execute"
            paramNo+=$(expr $assignArrLength - 1)

            unset assignArrLength
        elif [[ "$assignVarType" == "params" ]]
        then
            execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
            eval "$execute"
        elif [[ "$assignVarType" == "reference" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        elif [[ ! -z "${!previousParamNo}" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        fi
    fi

    assignVarType="$__capture_type"
    assignVarName="$varName"
    assignArrLength="$__capture_arrLength"
}

Function.CaptureParams() {
    __capture_type="$_type"
    __capture_arrLength="$l"
}

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias @param='@trapAssign local'
alias @reference='_type=reference @trapAssign local -n'
alias @var='_type=var @param'
alias @params='_type=params @param'
alias @array='_type=array @param'

1

只是添加到已接受的答案中,因为我发现如果数组内容像这样,效果不好:

RUN_COMMANDS=(
  "command1 param1... paramN"
  "command2 param1... paramN"
)

在这种情况下,数组的每个成员都会被拆分,因此函数看到的数组等效于:

RUN_COMMANDS=(
    "command1"
    "param1"
     ...
    "command2"
    ...
)

为了使这种情况起作用,我发现的方法是将变量名传递给函数,然后使用eval:

function () {
    eval 'COMMANDS=( "${'"$1"'[@]}" )'
    for COMMAND in "${COMMANDS[@]}"; do
        echo $COMMAND
    done
}

function RUN_COMMANDS

只是我的2©


1

尽管很丑陋,但是只要您不显式传递数组,而是传递与数组相对应的变量,这是一种可行的解决方法:

function passarray()
{
    eval array_internally=("$(echo '${'$1'[@]}')")
    # access array now via array_internally
    echo "${array_internally[@]}"
    #...
}

array=(0 1 2 3 4 5)
passarray array # echo's (0 1 2 3 4 5) as expected

我敢肯定,有人可以提出一个更清晰的想法,但是,与通过传递数组"{array[@]"}然后在内部使用相比,这是一个更好的解决方案array_inside=("$@")。当还有其他位置/ getopts参数时,这变得很复杂。在这些情况下,我必须先确定和然后使用shift和删除数组元素的组合来删除与数组无关的参数。

一个纯粹主义者的观点可能认为这种方法违反了语言,但是从务实的角度来说,这种方法为我节省了很多麻烦。在一个相关的主题上,我还使用eval了一个内部构造的数组给根据target_varname我传递给函数的参数命名的变量:

eval $target_varname=$"(${array_inside[@]})"

希望这对某人有帮助。


0

要求:在数组中查找字符串的函数。
这是DevSolar解决方案的略微简化,因为它使用传递的参数而不是复制参数。

myarray=('foobar' 'foxbat')

function isInArray() {
  local item=$1
  shift
  for one in $@; do
    if [ $one = $item ]; then
      return 0   # found
    fi
  done
  return 1       # not found
}

var='foobar'
if isInArray $var ${myarray[@]}; then
  echo "$var found in array"
else
  echo "$var not found in array"
fi 

0

我的简短答案是:

function display_two_array {
    local arr1=$1
    local arr2=$2
    for i in $arr1
    do
       "arrary1: $i"
    done
    
    for i in $arr2
    do
       "arrary2: $i"
    done
}

test_array=(1 2 3 4 5)
test_array2=(7 8 9 10 11)

display_two_array "${test_array[*]}" "${test_array2[*]}"
应该注意的是,${test_array[*]}${test_array2[*]}都用“”括起来,否则会失败。


您的示例不正确,因为它不完整。请提供完整的脚本代码。
丹尼斯VR
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.