我试图将“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
答案1
描述
理解这一点需要付出一些努力。要有耐心。该解决方案将在 bash 中正常工作。需要一些“bashim”。
第一:我们需要使用“间接”访问变量${!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
如果执行完整的脚本(假设其名为 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
了解“内部”意味着“函数内部”,“外部”则相反。
里面的值$VarName
是 var: 的名称animal_list
,作为字符串。
的值${!VarName}
显示为列表:anaconda, bison, cougar, dingo
现在,为了展示解决方案是如何构建的,有一行带有 echo :
echo a\=\(${!VarName}\)
它显示了以下行eval
执行的内容:
a=(anaconda bison cougar dingo)
一旦那是评估是的,该变量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>
请注意,真正的更改是在函数的最后一次评估中执行的。
就这样,完成了。 var 现在有一个值数组。
其实该函数的核心就一行:eval $VarName\=\(${!VarName}\)
此外,IFS 的值被设置为函数的本地值,这使得它返回到执行函数之前的值,而无需任何额外的工作。谢谢彼得·科德斯对最初想法的评论。
解释到此结束,希望它清楚了。
实函数
如果我们删除所有不需要的行,只留下核心 eval,只为 IFS 创建一个新变量,我们将函数简化为其最小表达式:
delim_to_array() {
local IFS="${2:-$' :|'}"
eval $1\=\(${!1}\);
}
将 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
$ so-setvar.sh
<bison>< a space>< {1..3}><~/><${a}><$a><$((2+2))><$(echo "fail")><./*><*><*>
上面为 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
$ ./so-setvar.sh
NAME: anaconda NAME: bison NAME: cougar NAME: dingo
NAME: alvin NAME: baron NAME: caleb NAME: doug
如您所见,IFS 仅在函数内部设置,不会永久更改,因此不需要将其重新设置为旧值。此外,该函数的第二次调用“people_list”利用了 IFS 的默认值,无需设置第二个参数。
警告 01:
在构造 (eval) 函数时,有一处 var 不加引号地暴露给 shell 解析。这使我们能够使用 IFS 值完成“分词”。但这也会将 var 的值暴露给:“大括号扩展”、“波形符扩展”、“参数、变量和算术扩展”、“命令替换”和“路径名扩展”,其中命令。以及<() >()
支持它的系统中的流程替换。
每个示例(最后一个除外)都包含在这个简单的 echo 中(小心):
a=failed; echo {1..3} ~/ ${a} $a $((2+2)) $(ls) ./*
也就是说,任何以文件名开头{~$`<>
或可能与文件名匹配或包含的字符串?*[]
都是潜在问题。
如果您确定变量不包含此类有问题的值,那么您就安全了。如果有可能有这样的价值观,那么回答你的问题的方式就更复杂,需要更多(甚至更长)的描述和解释。使用read
是一种替代方法。
警告02:
是的,read
它有自己的“龙”。
- 始终使用 -r 选项,我很难想到不需要它的情况。
- 该
read
命令只能获取一行。多行,即使通过设置-d
选项,也需要特别小心。或者将整个输入分配给一个变量。 - 如果
IFS
值包含空格,则前导和尾随空格将被删除。好吧,完整的描述应该包括一些关于 的细节tab
,但我会跳过它。 - 不要通过管道
|
读取数据。如果这样做,阅读将在子 shell 中进行。返回父 shell 后,子 shell 中设置的所有变量都不会保留。嗯,有一些解决方法,但是,我将再次跳过细节。
我本来不想包括阅读的警告和问题,但根据大众的要求,我不得不包括它们,抱歉。
答案2
在简单的情况下,这是eval
其他答案建议的更好的替代方案,这使得引用很多更轻松。
func() { # set the caller's simple non-array variable
local retvar=$1
printf -v "$retvar" '%s ' "${@:2}" # concat all the remaining args
}
Bash 补全(当您点击 Tab 时运行的代码)已切换到printf -v
而不是eval
其内部函数,因为它更具可读性并且可能更快。
对于返回数组,bash 常见问题解答建议使用read -a
读取数组变量的顺序数组索引:
# Bash
aref=realarray
IFS=' ' read -d '' -ra "$aref" <<<'words go into array elements'
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.
}
请注意,bash 手册页的措辞有点令人困惑。它说该-n
属性不能应用于数组变量。这意味着您不能拥有引用数组,但可以拥有引用到数组。
答案3
您无法更改函数内的变量(或本例中的数组),因为您仅传递其内容 - 函数不知道传递了哪个变量。
作为解决方法,您可以通过姓名变量的值并在函数内部eval
使用它来获取内容。
#!/bin/bash
function delim_to_array() {
local list=$1
local delim=$2
local oifs=$IFS;
IFS="$delim"
temp_array=($(eval echo '"${'"$list"'}"'))
IFS=$oifs;
eval "$list=("${temp_array[@]}")"
}
animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","
printf "NAME: %s\n" "${animal_list[@]}"
people_list="alvin|baron|caleb|doug"
delim_to_array "people_list" "|"
printf "NAME: %s\n" "${people_list[@]}"
eval
请密切注意使用行中的引号。表达式的一部分需要用单引号引起来,其他部分需要用双引号引起来。此外,我在最终打印中将循环替换for
为更简单的命令。printf
输出:
NAME: anaconda
NAME: bison
NAME: cougar
NAME: dingo
NAME: alvin
NAME: baron
NAME: caleb
NAME: doug
答案4
function delim_to_array() {
local list=$1
local delim=$2
local oifs=$IFS;
IFS="$delim";
temp_array=($list);
IFS=$oifs;
}
因此,我认为您使用此函数跳过了一个非常简单的细节:如果被调用者仅执行重复处理并且调用者发号施令,那么总是会更容易。在该函数中,您已经让被调用者执行所有调用 - 它不应该以这种方式处理这些名称。
isName()
case "${1##[0-9]*}" in
(${IFS:+*}|*[!_[:alnum:]]*)
IFS= "${IFS:+isName}" "$1"|| ! :
esac 2>/dev/null
setSplit(){
isName "$1" ||
set "" "setSplit(): bad name: '$1'"
eval "shift; set -f
${1:?"$2"}=(\$*)
set +f -$-"
}
这可以安全地验证数组名称,在 stderr 上生成有意义的错误输出,并在使用无效参数调用时适当停止退出。它的错误输出如下所示:
bash: 1: setSplit(): bad name: 'arr@yname'
...在哪里bash
是 shell 的当前值$0
,arr@yname
当我调用它并且它写下该消息时,它是setSplit()
第一个参数。
它也是两个函数 - 因此调用者可以根据自己的判断动态地重新定义测试,isName()
而无需对函数进行任何修改setSplit()
。
它还安全地禁用 shell 文件名生成 glob,以防止它们在分割时无意中扩展 - 如果任何参数包含任何 chars ,默认情况下可能会发生这种情况[*?
。在返回之前,它会恢复任何 shell 选项,这样做可能会更改为找到它们时的状态 - 我的意思是您可以在启用或禁用 shell 文件名通配的情况下调用它,并且除了返回之外,它不会影响该设置。
不过,这里缺少一个关键的东西 -$IFS
没有配置。该函数实现了一种解决方法,解决了应用于模式中 POSIX 括号表达式内容的isName()
相当令人担忧的bash
错误$IFS
case
(说真的:到底是什么?)$IFS
当全局值在返回之前尚未存在时,通过单独的自递归调用来取消其本地值。但这与数组分割完全正交,否则setSplit()
不会执行任何操作$IFS
。这本来就是应该的。你不需要那样做。
这呼叫者应该设置:
IFS=aBc setSplit arrayname 'xyzam*oBabc' x y z
printf '<%q>\n' "$IFS" "${arrayname[@]}"
<$' \t\n'>
<xyz>
<m\*o>
<''>
<b>
<''>
<x>
<y>
<z>
上面的代码bash
通过设置$IFS
被调用函数的本地值来在 shell 中工作。
POSIXly:
IFS=aBc command eval "setSplit arrayname 'xyzam*oBabc' x y z"
...将达到同样的目的。区别在于bash
打破了关于特殊内置函数和函数的永久环境的标准,否则指定在命令行上设置的变量应该影响当前的 shell 环境(这可能是首选,因为您可以通过任何一种方式获得它)。
无论您的偏好如何,重点是调用者在这里发号施令,而被调用者只需执行即可。