232

我对 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我仍然感到困惑。${...}\$${...}

4

19 回答 19

309

我最近一直在寻找更好的方法。关联数组对我来说听起来有点矫枉过正。看我发现了什么:

suffix=bzz
declare prefix_$suffix=mystr

...接着...

varname=prefix_$suffix
echo ${!varname}
于 2013-08-08T11:00:08.380 回答
193

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

# 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:间接 - 评估间接/参考变量

于 2013-05-14T21:43:30.150 回答
112

除了关联数组之外,在 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变量,这一事实可以与 here-strings 结合使用:

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

IFS零件和选项-r确保按原样分配值,而选项允许-d ''分配多行值。由于最后一个选项,该命令返回一个非零退出代码。

请注意,由于我们使用的是 here-string,因此值会附加一个换行符。

缺点:

  • 有点晦涩;
  • 以非零退出代码返回;
  • 将换行符附加到值。

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!

资源:

于 2019-03-25T03:54:33.807 回答
25

下面的示例返回 $name_of_var 的值

var=name_of_var
echo $(eval echo "\$$var")
于 2017-10-17T13:45:19.837 回答
14

利用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

但是像这样扩展 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
于 2019-10-12T05:09:31.500 回答
10

将此处的两个高度评价的答案组合成一个完整的示例,该示例希望有用且不言自明:

#!/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

输出:

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

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

于 2020-11-26T11:31:30.197 回答
7

这也可以

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
于 2018-10-19T09:38:58.727 回答
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"
于 2015-07-12T13:52:58.377 回答
3

根据BashFAQ/006,您可以在此处使用read字符串语法来分配间接变量:

function grep_search() {
  read "$1" <<<$(ls | tail -1);
}

用法:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt
于 2018-04-14T01:04:49.930 回答
2

哇,大部分语法都很糟糕!如果您需要间接引用数组,这是一种语法更简单的解决方案:

#!/bin/bash

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

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

对于更简单的用例,我推荐使用Advanced Bash-Scripting Guide中描述的语法

于 2017-01-05T02:14:54.767 回答
2

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

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)
于 2019-11-28T19:47:32.023 回答
2

尽管这是一个老问题,但我仍然很难获取动态变量名称,同时避免使用eval(evil) 命令。

解决了declare -n创建对动态值的引用的问题,这在 CI/CD 过程中特别有用,其中 CI/CD 服务所需的秘密名称直到运行时才知道。就是这样:

# Bash v4.3+
# -----------------------------------------------------------
# Secerts in CI/CD service, injected as environment variables
# AWS_ACCESS_KEY_ID_DEV, AWS_SECRET_ACCESS_KEY_DEV
# AWS_ACCESS_KEY_ID_STG, AWS_SECRET_ACCESS_KEY_STG
# -----------------------------------------------------------
# Environment variables injected by CI/CD service
# BRANCH_NAME="DEV"
# -----------------------------------------------------------
declare -n _AWS_ACCESS_KEY_ID_REF=AWS_ACCESS_KEY_ID_${BRANCH_NAME}
declare -n _AWS_SECRET_ACCESS_KEY_REF=AWS_SECRET_ACCESS_KEY_${BRANCH_NAME}

export AWS_ACCESS_KEY_ID=${_AWS_ACCESS_KEY_ID_REF}
export AWS_SECRET_ACCESS_KEY=${_AWS_SECRET_ACCESS_KEY_REF}

echo $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY
aws s3 ls
于 2020-08-29T13:01:09.643 回答
2

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

real_var="holaaaa"
aux_var="real_var"
echo ${(P)aux_var}
holaaaa

代替 ”!”

于 2020-12-25T16:38:25.523 回答
0

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

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.

于 2017-07-20T04:30:24.243 回答
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 命令执行。


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


如需进一步阅读或示例,请查看有关 Indirection 的 BashFAQ/006

我们不知道任何可以在没有 POSIX 或 Bourne shell 的情况下复制该功能的技巧eval,这很难安全地完成。因此,请考虑这是一种使用风险自负的黑客攻击

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

通常,在 bash 脚本中,您根本不需要间接引用。通常,当人们不了解或不了解 Bash 数组或没有充分考虑其他 Bash 特性(例如函数)时,他们会考虑使用此解决方案。

将变量名称或任何其他 bash 语法放入参数中经常会不正确且不恰当地在解决问题时有更好的解决方案。它违反了代码和数据之间的分离,因此使您陷入错误和安全问题的滑坡。间接可以使您的代码不那么透明并且更难遵循。

于 2018-04-14T00:50:51.003 回答
0

亲吻方法:

a=1
c="bam"
let "$c$a"=4
echo bam1

结果 4

于 2021-06-12T02:39:24.233 回答
0

符合 POSIX 标准的答案

对于此解决方案,您需要对该/tmp文件夹具有 r/w 权限。
我们创建一个临时文件来保存我们的变量并利用内置的-a标志:set

$ man set
...
-a创建或修改的每个变量或函数都被赋予导出属性并标记为导出到后续命令的环境。

因此,如果我们创建一个包含动态变量的文件,我们可以使用 set 在脚本中将它们变为现实。

实施

#!/bin/sh
# Give the temp file a unique name so you don't mess with any other files in there
ENV_FILE="/tmp/$(date +%s)"

MY_KEY=foo
MY_VALUE=bar

echo "$MY_KEY=$MY_VALUE" >> "$ENV_FILE"

# Now that our env file is created and populated, we can use "set"
set -a; . "$ENV_FILE"; set +a
rm "$ENV_FILE"
echo "$foo"

# Output is "bar" (without quotes)

解释上面的步骤:

# Enables the -a behavior
set -a

# Sources the env file
. "$ENV_FILE"

# Disables the -a behavior
set +a
于 2022-01-23T00:02:05.913 回答
0

虽然我认为declare -n仍然是最好的方法,但还有另一种没有人提到的方法,在 CI/CD 中非常有用

function dynamic(){
  export a_$1="bla"
}

dynamic 2
echo $a_2

此函数不支持空格,因此dynamic "2 3"会返回错误。

于 2022-02-09T17:57:20.887 回答
-3

对于varname=$prefix_suffix格式,只需使用:

varname=${prefix}_suffix
于 2015-01-13T09:28:53.297 回答