如何对 bash 函数中的参数使用按引用调用

mac*_*yle 8 bash shell-script function variable

我试图将“var name”传递给函数,让函数转换具有此类“var name”的变量包含的值,然后能够通过其原始“var name”引用转换后的对象。

例如,假设我有一个将分隔列表转换为数组的函数,并且我有一个名为“animal_list”的分隔列表。我想通过将列表名称传递给函数,然后将 now 数组引用为“animal_list”来将该列表转换为数组。

代码示例:

function delim_to_array() {
  local list=$1
  local delim=$2
  local oifs=$IFS;

  IFS="$delim";
  temp_array=($list);
  IFS=$oifs;

  # Now I have the list converted to an array but it's 
  # named temp_array. I want to reference it by its 
  # original name.
}

# ----------------------------------------------------

animal_list="anaconda, bison, cougar, dingo"
delim_to_array ${animal_list} ","

# After this point I want to be able to deal with animal_name as an array.
for animal in "${animal_list[@]}"; do 
  echo "NAME: $animal"
done

# And reuse this in several places to converted lists to arrays
people_list="alvin|baron|caleb|doug"
delim_to_array ${people_list} "|"

# Now I want to treat animal_name as an array
for person in "${people_list[@]}"; do 
  echo "NAME: $person"
done
Run Code Online (Sandbox Code Playgroud)

小智 9

描述

理解这一点需要一些努力。要有耐心。该解决方案将在 bash 中正常工作。需要一些“bashims”。

第一:我们需要使用“间接”访问变量${!variable}。如果$variable包含字符串animal_name,“参数扩展”:${!variable}将扩展到 的内容$animal_name

让我们看看这个想法在行动,我尽可能保留了您使用的名称和值,以便您更容易理解:

#!/bin/bash

function delim_to_array() {
    local VarName=$1

    local IFS="$2";
    printf "inside  IFS=<%s>\n" "$IFS"

    echo "inside  var    $VarName"
    echo "inside  list = ${!VarName}"

    echo a\=\(${!VarName}\)
    eval a\=\(${!VarName}\)
    printf "in  <%s> " "${a[@]}"; echo

    eval $VarName\=\(${!VarName}\)
}

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","

printf "out <%s> " "${animal_list[@]}"; echo
printf "outside IFS=<%s>\n" "$IFS"

# Now we can use animal_name as an array
for animal in "${animal_list[@]}"; do
    echo "NAME: $animal"
done
Run Code Online (Sandbox Code Playgroud)

如果执行该完整脚本(假设其名为 so-setvar.sh),您应该看到:

$ ./so-setvar.sh
inside  IFS=<,>
inside  var    animal_list
inside  list = anaconda, bison, cougar, dingo
a=(anaconda  bison  cougar  dingo)
in  <anaconda> in  <bison> in  <cougar> in  <dingo> 
out <anaconda> out <bison> out <cougar> out <dingo> 
outside IFS=< 
>
NAME: anaconda
NAME: bison
NAME: cougar
NAME: dingo
Run Code Online (Sandbox Code Playgroud)

理解“内部”意味着“功能内部”,而“外部”则相反。

里面的值$VarName是 var: 的名称animal_list,作为一个字符串。

的值${!VarName}显示为列表:anaconda, bison, cougar, dingo

现在,为了展示解决方案的构建方式,有一行带有 echo:

echo a\=\(${!VarName}\)
Run Code Online (Sandbox Code Playgroud)

这显示了以下行eval执行的内容:

a=(anaconda  bison  cougar  dingo)
Run Code Online (Sandbox Code Playgroud)

一旦被评估,变量a就是一个带有动物列表的数组。在这种情况下,var a 用于准确显示 eval 如何影响它。

然后,每个元素的值a打印为<in> val
同样在函数的外部执行,如<out> val
这两行所示:

in  <anaconda> in  <bison> in  <cougar> in  <dingo>
out <anaconda> out <bison> out <cougar> out <dingo>
Run Code Online (Sandbox Code Playgroud)

请注意,真正的更改是在函数的最后一个 eval 中执行的。
就这样,大功告成。var 现在有一个值数组。

其实函数的核心就是一行: eval $VarName\=\(${!VarName}\)

此外,IFS 的值被设置为函数的本地值,这使其返回到执行函数之前的值,而无需任何额外工作。感谢Peter Cordes对原始想法的评论。

解释到此结束,希望它清楚。


实函数

如果我们删除所有不需要的行,只留下核心 eval,只为 IFS 创建一个新变量,我们将函数减少到它的最小表达式:

delim_to_array() {
    local IFS="${2:-$' :|'}"
    eval $1\=\(${!1}\);
}
Run Code Online (Sandbox Code Playgroud)

将 IFS 的值设置为局部变量,还允许我们为函数设置“默认”值。每当 IFS 所需的值没有作为第二个参数发送给函数时,本地 IFS 就会采用“默认”值。我觉得默认值应该是space( ) (这始终是一个有用的拆分值)、colon(:) 和vertical line(|)。这三个中的任何一个都会拆分值。当然,默认值可以设置为适合您需要的任何其他值。

编辑使用read

为了降低 eval 中未引用值的风险,我们可以使用:

delim_to_array() {
    local IFS="${2:-$' :|'}"
    # eval $1\=\(${!1}\);
    read -ra "$1" <<<"${!1}"
}

test="fail-test"; a="fail-test"

animal_list='bison, a space, {1..3},~/,${a},$a,$((2+2)),$(echo "fail"),./*,*,*'

delim_to_array "animal_list" ","
printf "<%s>" "${animal_list[@]}"; echo
Run Code Online (Sandbox Code Playgroud)
$ so-setvar.sh
<bison>< a space>< {1..3}><~/><${a}><$a><$((2+2))><$(echo "fail")><./*><*><*>
Run Code Online (Sandbox Code Playgroud)

上面为 var 设置的大多数值animal_list都会因 eval 而失败。
但是通过阅读没有问题。

  • 注意:在此代码中尝试 eval 选项是完全安全的,因为在调用函数之前,变量的值已设置为纯文本值。即使真的执行了,它们也只是文本。甚至名称错误的文件也没有问题,因为路径名扩展是最后一次扩展,因此不会在路径名扩展上重新执行变量扩展。再一次,使用原样的代码,这绝不是对eval.

例子

为了真正理解这个函数是什么以及它是如何工作的,我重新编写了你使用这个函数发布的代码:

#!/bin/bash

delim_to_array() {
        local IFS="${2:-$' :|'}"
        # printf "inside  IFS=<%s>\n" "$IFS"
        # eval $1\=\(${!1}\);
        read -ra "$1" <<<"${!1}";
}

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","
printf "NAME: %s\t " "${animal_list[@]}"; echo

people_list="alvin|baron|caleb|doug"
delim_to_array "people_list"
printf "NAME: %s\t " "${people_list[@]}"; echo
Run Code Online (Sandbox Code Playgroud)
$ ./so-setvar.sh
NAME: anaconda   NAME:  bison    NAME:  cougar   NAME:  dingo    
NAME: alvin      NAME: baron     NAME: caleb     NAME: doug      
Run Code Online (Sandbox Code Playgroud)

如您所见,IFS 仅在函数内部设置,不会永久更改,因此不需要将其重新设置为旧值。此外,该函数的第二次调用“people_list”利用了 IFS 的默认值,无需设置第二个参数。


« 这里是龙 » ¯\_(?)_/¯


警告 01:

在构造 (eval) 函数时,有一个地方可以将 var 不加引号地暴露给 shell 解析。这允许我们使用 IFS 值完成“分词”。但这也将 vars 的值(除非某些引用阻止)暴露为:“大括号扩展”、“波浪号扩展”、“参数、变量和算术扩展”、“命令替换”和“路径名扩展”,在那个命令。并<() >()在支持它的系统中进行过程替换。

每个(除了最后一个)的例子都包含在这个简单的回声中(小心):

 a=failed; echo {1..3} ~/ ${a} $a $((2+2)) $(ls) ./*
Run Code Online (Sandbox Code Playgroud)

也就是说,任何{~$`<>以文件名开头或可能与文件名匹配或包含的字符串?*[]都是潜在问题。

如果您确定变量不包含此类有问题的值,那么您就是安全的。如果有可能拥有这样的值,那么回答您的问题的方法会更加复杂,需要更多(甚至更长)的描述和解释。使用read是另一种选择。

警告 02:

是的,read带有它自己的“龙”份额。

  • 总是使用 -r 选项,我很难想到不需要它的条件。
  • read命令可以得到只有一条线。多行,即使是通过设置-d选项,也需要特别小心。或者整个输入将分配给一个变量。
  • 如果IFSvalue 包含空格,则将删除前导和尾随空格。好吧,完整的描述应该包括一些关于 的细节tab,但我会跳过它。
  • 不要通过管道|读取数据。如果这样做, read 将位于子外壳中。在子 shell 中设置的所有变量在返回到父 shell 时不会持续存在。嗯,有一些解决方法,但是,我将再次跳过细节。

我并不是要包括阅读的警告和问题,但应大众要求,我不得不包括它们,抱歉。


Pet*_*des 8

Bash FAQ 有一个关于通过引用/间接调用的完整条目

在简单的情况下,这eval是其他答案所建议的更好替代方案,这使得引用容易。

func() {  # set the caller's simple non-array variable
    local retvar=$1
    printf -v "$retvar"  '%s ' "${@:2}"  # concat all the remaining args
}
Run Code Online (Sandbox Code Playgroud)

Bash-completion(当你点击 tab 时运行的代码)已经切换到printf -v而不是eval为了它的内部函数,因为它更具可读性并且可能更快。

对于返回数组,Bash FAQ建议使用read -a读取数组变量的顺序数组索引:

# Bash
aref=realarray
IFS=' ' read -d '' -ra "$aref" <<<'words go into array elements'
Run Code Online (Sandbox Code Playgroud)

Bash 4.3 引入了一项功能,使按引用调用更加方便。Bash 4.3 仍然是新版本(2014)。

func () { # return an array in a var named by the caller
    typeset -n ref1=$1   # ref1 is a nameref variable.
    shift   # remove the var name from the positional parameters
    echo "${!ref1} = $ref1"  # prints the name and contents of the real variable
    ref1=( "foo" "bar" "$@" )  # sets the caller's variable.
}
Run Code Online (Sandbox Code Playgroud)

请注意,bash 手册页的措辞有点令人困惑。它说该-n属性不能应用于数组变量。这意味着您不能拥有引用数组,但可以拥有数组的引用。