Bash中的动态变量名称


159

我对bash脚本感到困惑。

我有以下代码:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}

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

因此,以说明我想要什么:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

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

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


3
别。使用关联数组将命令名称映射到数据。
13年

3
VAR = A; VAL = 333;读“ $ VAR” <<<“ $ VAL”; 回声“ A = $ A”
Grigory K,

Answers:


150

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

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

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

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

declare "magic_variable_$1=$(ls | tail -1)"

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

var="magic_variable_$1"
echo "${!var}"

请参阅BashFAQ:间接-评估间接/引用变量


5
@DeaDEnD -a声明一个索引数组,而不是一个关联数组。除非to的参数grep_search是数字,否则它将被视为带有数字值的参数(如果未设置该参数,则默认为0)。
chepner 2013年

1
嗯 我正在使用bash,4.2.45(2)并且声明不会将其列为选项declare: usage: declare [-afFirtx] [-p] [name[=value] ...]。但是,它似乎工作正常。
13年

declare -h在4.2.45(2)中显示declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]。您可能会仔细检查您实际上是在运行4.x而不是3.2。
chepner 2013年

5
为什么不只是declare $varname="foo"呢?
本·戴维斯

1
${!varname}更简单且兼容广泛
Brad Hein

227

我最近一直在寻找更好的方法。联想数组对我来说听起来像是过分杀了。看我找到了什么:

suffix=bzz
declare prefix_$suffix=mystr

...然后...

varname=prefix_$suffix
echo ${!varname}

如果要在函数内部声明全局,可以在bash> = 4.2中使用“ declare -g”。在较早的bash中,可以使用“只读”而不是“声明”,只要您以后不想更改该值即可。可以进行配置或您拥有什么。
山姆·沃特金斯

7
最好使用封装的变量格式:(prefix_${middle}_postfix即您的格式将不适用于varname=$prefix_suffix
msciwoj 2014年

1
我被bash 3困住了,无法使用关联数组。因此,这可以挽救生命。$ {!...}很难用那个来搜索。我认为它只是扩展了一个var名称。
尼尔·麦吉尔

10
@NeilMcGill:参见“ man bash” gnu.org/software/bash/manual/html_node/…:参数扩展的基本形式为$ {parameter}。<...>如果参数的第一个字符是感叹号(!),则会引入变量间接寻址级别。Bash使用由其余参数形成的变量的值作为变量的名称;然后扩展此变量,并在其余替换中使用该值,而不是参数本身的值。
Yorik.sar 2015年

1
@syntaxerror:您可以使用上面的“ declare”命令随意分配所需的值。
Yorik.sar 2015年

48

除了关联数组之外,还有多种方法可以在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.

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

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”

缺点:

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

1b。分配给read

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

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”

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

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

缺点:

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

1c。分配给printf

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

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”

缺点:

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

方法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”

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

缺点:

  • 不便携。

风险性

所有这些别名技术都存在一些风险。第一个是每次您解决间接寻址(用于读取或分配)时,执行任意代码。确实,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)

第二个风险是创建循环别名。由于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!

资源:


1
这是最好的答案,尤其是因为该${!varname}技术需要一个的中间var varname
RichVel19年

难以理解的是,这个答案并未得到更高的评价
Marcos

18

下面的示例返回$ name_of_var的值

var=name_of_var
echo $(eval echo "\$$var")

4
echo不需要使用命令替换嵌套两个s(它会忽略引号)。另外,-n应该给的选项echo。而且,一如既往eval的不安全。但是所有这些都是不必要的,因为Bash为此目的具有更安全,更清晰和更短的语法:${!var}
马兰(Maëlan)

4

这应该工作:

function grep_search() {
    declare magic_variable_$1="$(ls | tail -1)"
    echo "$(tmpvar=magic_variable_$1 && echo ${!tmpvar})"
}
grep_search var  # calling grep_search with argument "var"

4

这也将工作

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green

就你而言

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val


3

declare

不需要像其他答案一样使用前缀,也不需要使用数组。使用公正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

但是像这样扩展argv列表

for v in "$@"; do declare "${v%=*}=${v#*=}"; done

额外提示

# 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

1
这看起来很干净。没有邪恶的围兜和鲍勃,你使用的相关变量的工具,不能掩盖看似无关的甚至是危险的功能,如printfeval
kvantour

2

哇,大多数语法太可怕了!如果需要间接引用数组,这是一种使用更简单语法的解决方案:

#!/bin/bash

foo_1=("fff" "ddd") ;
foo_2=("ggg" "ccc") ;

for i in 1 2 ;
do
    eval mine=( \${foo_$i[@]} ) ;
    echo ${mine[@]} ;
done ;

对于更简单的用例,我建议使用高级Bash脚本指南》中描述语法


2
ABS因在示例中展示不良做法而臭名昭著。请考虑使用bash-hackers WikiWooledge Wiki,它们具有直接在主题上的BashFAQ#6
查尔斯·达菲

2
仅当foo_1foo_2中的条目没有空格和特殊符号时,此方法才有效。有问题的条目的示例:'a b'将在中创建两个条目mine''不会在内部创建条目mine'*'将扩展到工作目录的内容。您可以通过引用以下内容来防止这些问题:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi '19

@Socowi这是循环遍历BASH中的任何数组的普遍问题。也可以通过临时更改IFS(然后再更改回IFS)来解决。很高兴看到报价成功了。
ingyhere

@ingyhere我希望与众不同。这不是一个普遍的问题。有一个标准的解决方案:始终引用[@]构造。"${array[@]}"将始终扩展到正确的条目列表,而没有单词拆分或扩展的问题*。同样,只有在IFS您知道从未出现在数组内部的任何非空字符时,才可以解决单词拆分问题。此外,*通过不能实现的字面处理IFS。您可以凝视IFS='*'星星并分裂,也可以凝视IFS=somethingOther*扩展。
索科维

@Socowi您假设不希望使用Shell扩展,但情况并非总是如此。当引用所有内容后,shell表达式未扩展时,开发人员会抱怨这些错误。一个好的解决方案是知道数据并适当地构建脚本,甚至使用IFS |LF作为IFS。同样,循环中的一般问题是默认情况下会发生令牌化,因此引用是允许包含令牌的扩展字符串的特殊解决方案。(它是通配符/参数扩展,或者是带引号的扩展字符串,但不是两个都带。)如果需要8个引号来读取var,则shell是错误的语言。
ingyhere

1

对于索引数组,您可以像这样引用它们:

foo=(a b c)
bar=(d e f)

for arr_var in 'foo' 'bar'; do
    declare -a 'arr=("${'"$arr_var"'[@]}")'
    # do something with $arr
    echo "\$$arr_var contains:"
    for char in "${arr[@]}"; do
        echo "$char"
    done
done

可以类似地引用关联数组,但需要-A打开declare而不是-a


1

一种不依赖于您所使用的shell / bash版本的额外方法是使用envsubst。例如:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)

0

我希望能够创建一个包含命令第一个参数的变量名

script.sh 文件:

#!/usr/bin/env bash
function grep_search() {
  eval $1=$(ls | tail -1)
}

测试:

$ source script.sh
$ grep_search open_box
$ echo $open_box
script.sh

按照help eval

将参数作为shell命令执行。


${!var}如前所述,您还可以使用Bash 间接扩展,但是它不支持检索数组索引。


有关进一步的阅读或示例,请参阅BashFAQ / 006中有关间接的内容

我们不知道有任何技巧可以在没有的情况下在POSIX或Bourne shell中复制该功能,而eval这可能很难安全地完成。因此,请您自担风险地将其视为一种用法

但是,您应按照以下说明重新考虑使用间接寻址。

通常,在bash脚本编制中,您根本不需要间接引用。通常,人们在不了解或不了解Bash数组或未完全考虑其他Bash功能(例如功能)的情况下,便会寻求解决方案。

将变量名称或任何其他bash语法放入参数中通常不正确且在不适当的情况下进行,以解决具有较好解决方案的问题。它违反了代码和数据之间的分隔,因此使您容易陷入错误和安全问题。间接可以使您的代码不那么透明,也很难遵循。


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.