Bash中的动态变量名称

Kon*_*nos 135 variables syntax bash dynamic

我对bash脚本感到困惑.

我有以下代码:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}
Run Code Online (Sandbox Code Playgroud)

我希望能够创建一个包含命令的第一个参数并带有例如最后一行的值的变量名ls.

所以说明我想要的东西:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt
Run Code Online (Sandbox Code Playgroud)

那么,我应该如何定义/声明$magic_way_to_define_magic_variable_$1以及如何在脚本中调用它?

我已经试过eval,${...},\$${...},但我仍然感到困惑.

Yor*_*sar 210

我最近一直在寻找更好的方法.关联数组听起来像是矫枉过正.看我发现了什么:

suffix=bzz
declare prefix_$suffix=mystr
Run Code Online (Sandbox Code Playgroud)

...然后...

varname=prefix_$suffix
echo ${!varname}
Run Code Online (Sandbox Code Playgroud)

  • @NeilMcGill:参见"man bash"http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html#Shell-Parameter-Expansion:参数扩展的基本形式是$ {参数}.<...>如果参数的第一个字符是感叹号(!),则引入一个变量间接的级别.Bash使用从参数的其余部分形成的变量的值作为变量的名称; 然后展开此变量,并将该值用于替换的其余部分,而不是参数本身的值. (9认同)
  • 最好使用封装的变量格式:`prefix _ $ {middle} _postfix`(即你的格式不适用于`varname = $ prefix_suffix`) (7认同)
  • @Yorik.sar 您能否在答案中包含手册的摘录?考虑到你是作者,这样更有意义。TIA。 (3认同)
  • @openCivilization 你从哪个 shell 获取它?您可以使用“echo $SHELL”进行检查。如今,macOS 将 zsh 作为默认 shell,而 `${!varname}` 是 Bash 专用的。 (2认同)

che*_*ner 133

使用关联数组,命令名称作为键.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}
Run Code Online (Sandbox Code Playgroud)

如果你不能使用关联数组(例如,你必须支持bash3),你可以declare用来创建动态变量名:

declare "magic_variable_$1=$(ls | tail -1)"
Run Code Online (Sandbox Code Playgroud)

并使用间接参数扩展来访问该值.

var="magic_variable_$1"
echo "${!var}"
Run Code Online (Sandbox Code Playgroud)

请参阅BashFAQ:间接 - 评估间接/参考变量.

  • `${!varname}` 更简单且广泛兼容 (8认同)
  • 为什么不只是`声明$ varname ="foo"`? (5认同)
  • @DeaDEnD` -a`声明一个索引数组,而不是一个关联数组.除非`grep_search`的参数是一个数字,否则它将被视为带有数值的参数(如果未设置参数,则默认为0). (4认同)

Maë*_*lan 17

除了关联数组之外,还有多种方法可以在Bash中实现动态变量。请注意,所有这些技术都存在风险,将在本答案的最后进行讨论。

在下面的示例中,我将假设i=37并且您要为var_37初始值为的名为的变量起别名lolilol

方法1.使用“指针”变量

您可以简单地将变量名存储在间接变量中,与C指针不同。然后Bash具有读取别名变量的语法:${!name}扩展为名称为变量值的变量的值name。您可以将其视为两个阶段的扩展:${!name}扩展为$var_37,扩展为lolilol

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.
Run Code Online (Sandbox Code Playgroud)

不幸的是,没有用于修改别名变量的对应语法。相反,您可以使用以下技巧之一实现分配。

1a。分配给eval

eval是邪恶的,但也是实现我们目标的最简单,最便携的方法。您必须小心地逃避作业的右侧,因为它将被评估两次。一种简单而系统的方法是事先评估右侧(或使用printf %q)。

并且您应该手动检查左侧是否是有效的变量名或带索引的名称(如果是的话evil_code #)。相比之下,下面的所有其他方法都会自动执行它。

# check that name is a valid variable name:
# note: this code does not support variable_name[index]
shopt -s globasciiranges
[[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit

value='babibab'
eval "$name"='$value'  # carefully escape the right-hand side!
echo "$var_37"  # outputs “babibab”
Run Code Online (Sandbox Code Playgroud)

缺点:

  • 不检查变量名的有效性。
  • eval 是邪恶的。
  • eval 是邪恶的。
  • eval 是邪恶的。

1b。分配给read

read内建让您指定值,其中你给的名称,可以在这里与弦一起利用的事实变量:

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”
Run Code Online (Sandbox Code Playgroud)

IFS部分和选项-r确保该值被分配原样,而选项-d ''允许指定多行值。由于使用了最后一个选项,该命令将返回非零退出代码。

请注意,由于我们使用的是here字符串,因此在值后附加了换行符。

缺点:

  • 有点晦涩
  • 返回非零退出代码;
  • 在值后附加一个换行符。

1c。分配给printf

从Bash 3.1(2005年发布)开始,printf内建函数也可以将其结果分配给已命名的变量。与以前的解决方案相比,它可以正常工作,不需要额外的精力来逃避事物,防止分裂等。

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”
Run Code Online (Sandbox Code Playgroud)

缺点:

  • 便携式性较差(但很好)。

方法2。使用“引用”变量

从Bash 4.3(2014年发布)开始,declare内置-n函数提供了一个用于创建变量的选项,该变量是对另一个变量的“名称引用”,就像C ++引用一样。就像方法1中一样,引用存储别名变量的名称,但是每次访问引用(用于读取或分配)时,Bash都会自动解析该间接引用。

另外,Bash具有一种特殊且非常混乱的语法,用于获取引用本身的值,请自己判断:${!ref}

declare -n ref="var_$i"
echo "${!ref}"  # outputs “var_37”
echo "$ref"     # outputs “lolilol”
ref='babibab'
echo "$var_37"  # outputs “babibab”
Run Code Online (Sandbox Code Playgroud)

这不能避免下面解释的陷阱,但是至少可以使语法简单明了。

缺点:

  • 不便携。

风险性

所有这些别名技术都存在一些风险。每当您解决间接寻址(用于读取或分配)时,第一个方法就是执行任意代码。确实,var_37除了标量变量名称(如)外,您还可以为数组下标(如)加上别名arr[42]。但是Bash每次都需要评估方括号的内容,因此混叠arr[$(do_evil)]会产生意想不到的效果……因此,只有在控制别名的来源时才使用这些技术

function guillemots() {
  declare -n var="$1"
  var="«${var}»"
}

arr=( aaa bbb ccc )
guillemots 'arr[1]'  # modifies the second cell of the array, as expected
guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
            # (once when expanding var, once when assigning to it)
Run Code Online (Sandbox Code Playgroud)

第二个风险是创建循环别名。由于Bash变量是通过它们的名称而不是它们的范围来标识的,因此您可能会无意中为其自身创建一个别名(同时认为它会在一个封闭的范围内为一个变量添加别名)。在使用通用变量名称(例如var)时,尤其可能发生这种情况。因此,仅在控制别名变量的名称时才使用这些技术

function guillemots() {
  # var is intended to be local to the function,
  # aliasing a variable which comes from outside
  declare -n var="$1"
  var="«${var}»"
}

var='lolilol'
guillemots var  # Bash warnings: “var: circular name reference”
echo "$var"     # outputs anything!
Run Code Online (Sandbox Code Playgroud)

资源:

  • 这是最好的答案,特别是因为 `${!varname}` 技术需要 `varname` 的中间变量。 (4认同)
  • 很难理解这个答案没有得到更高的支持 (2认同)

meo*_*eow 16

以下示例返回$ name_of_var的值

var=name_of_var
echo $(eval echo "\$$var")
Run Code Online (Sandbox Code Playgroud)

  • 不需要嵌套两个带有命令替换的“ echo”(省略引号)。另外,应将选项-n给予echo。而且,和往常一样,`eval`是不安全的。但是所有这些都是不必要的,因为Bash为此目的使用了更安全,更清晰,更短的语法:$ {!var}。 (3认同)

lac*_*ass 11

declare

不需要像其他答案一样使用前缀,也不需要使用数组。使用 just declare双引号参数扩展

我经常使用以下技巧来解析包含one to n格式为 的参数的参数列表key=value otherkey=othervalue etc=etc,例如:

# brace expansion just to exemplify
for variable in {one=foo,two=bar,ninja=tip}
do
  declare "${variable%=*}=${variable#*=}"
done
echo $one $two $ninja 
# foo bar tip
Run Code Online (Sandbox Code Playgroud)

但是像这样扩展 argv 列表

for v in "$@"; do declare "${v%=*}=${v#*=}"; done
Run Code Online (Sandbox Code Playgroud)

额外提示

# parse argv's leading key=value parameters
for v in "$@"; do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv's leading key=value parameters
while (( $# )); do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
  shift
done
Run Code Online (Sandbox Code Playgroud)

  • 这看起来是一个非常干净的解决方案。没有邪恶的围兜和鲍勃,你使用与变量相关的工具,而不是掩盖看似不相关甚至危险的函数,例如“printf”或“eval” (3认同)

k_v*_*ath 7

这也行

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green
Run Code Online (Sandbox Code Playgroud)

在你的情况下

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val
Run Code Online (Sandbox Code Playgroud)


Nag*_*gev 6

将这里两个评价很高的答案组合成一个完整的例子,希望有用且不言自明:

#!/bin/bash

intro="You know what,"
pet1="cat"
pet2="chicken"
pet3="cow"
pet4="dog"
pet5="pig"

# Setting and reading dynamic variables
for i in {1..5}; do
        pet="pet$i"
        declare "sentence$i=$intro I have a pet ${!pet} at home"
done

# Just reading dynamic variables
for i in {1..5}; do
        sentence="sentence$i"
        echo "${!sentence}"
done

echo
echo "Again, but reading regular variables:"
echo $sentence1
echo $sentence2
echo $sentence3
echo $sentence4
echo $sentence5
Run Code Online (Sandbox Code Playgroud)

输出:

你知道吗,我家里有一只宠物猫
你知道吗,我家里有一只宠物鸡
你知道吗,我家里有一头宠物牛
你知道吗,我家里有一只宠物狗
你知道吗,我有家里的宠物猪

再次,但阅读常规变量:
你知道吗,我家里有一只宠物猫
你知道吗,我家里有一只宠物鸡
你知道吗,我家里有一头宠物牛
你知道吗,我在家
你知道吗,我家里有一只宠物猪


tom*_*rad 6

对于 zsh(较新的 mac os 版本),您应该使用

real_var="holaaaa"
aux_var="real_var"
echo ${(P)aux_var}
holaaaa
Run Code Online (Sandbox Code Playgroud)

代替 ”!”