我对 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
、${...}
、\$${...}
,但我仍然感到困惑。
"${!ARGUMENT_INDEX:-default}"
我最近一直在寻找更好的方法。关联数组对我来说听起来有点矫枉过正。看我发现了什么:
suffix=bzz
declare prefix_$suffix=mystr
...接着...
varname=prefix_$suffix
echo ${!varname}
从 docs:
'$' 字符引入了参数扩展、命令替换或算术扩展。 ...参数扩展的基本形式是${parameter}。参数的值被替换。 ...如果参数的第一个字符是感叹号(!),并且参数不是nameref,则它引入了间接级别。 Bash 使用扩展其余参数形成的值作为新参数;然后将 this 展开,并将该值用于其余的展开,而不是原始参数的展开。这称为间接扩展。该值受波浪号扩展、参数扩展、命令替换和算术扩展的影响。 ...
使用关联数组,以命令名称作为键。
# Requires bash 4, though
declare -A magic_variable=()
function grep_search() {
magic_variable[$1]=$( ls | tail -1 )
echo ${magic_variable[$1]}
}
如果您不能使用关联数组(例如,您必须支持 bash
3),您可以使用 declare
创建动态变量名:
declare "magic_variable_$1=$(ls | tail -1)"
并使用间接参数扩展来访问该值。
var="magic_variable_$1"
echo "${!var}"
请参阅 BashFAQ:Indirection - Evaluating indirect/reference variables。
-a
声明一个索引数组,而不是关联数组。除非 grep_search
的参数是数字,否则它将被视为具有数值的参数(如果未设置参数,则默认为 0)。
4.2.45(2)
并声明未将其列为选项 declare: usage: declare [-afFirtx] [-p] [name[=value] ...]
。然而,它似乎工作正常。
declare $varname="foo"
?
sh
/dash
一起使用的纯 POSIX 方式吗?
${!varname}
更简单且广泛兼容
除了关联数组之外,在 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”
缺点:
不检查变量名的有效性。
评估是邪恶的。
评估是邪恶的。
评估是邪恶的。
1b。用读赋值
内置的 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!
资源:
BashFaq/006:如何使用变量变量(间接变量、指针、引用)或关联数组?
BashFAQ/048: eval 命令和安全问题
${!varname}
技术需要 varname
的中间变量。
function funcname() {
syntax;它在与问题实际相关的所有内容上都很准确。 :)
printf -v
存在哪些风险? (除了不能移植到超过 17 年的 bash
版本。)
name='x[$(evil)]'
则每个 printf -v "$name" '%s' '...'
评估 evil
。
下面的示例返回 $name_of_var 的值
var=name_of_var
echo $(eval echo "\$$var")
echo
是不必要的。另外,应将选项 -n
提供给 echo
。而且,和往常一样,eval
是不安全的。但是所有这些都是不必要的,因为 Bash 有一个更安全、更清晰、更短的语法来实现这个目的:${!var}
。
使用声明
不需要像其他答案一样使用前缀,也不需要使用数组。仅使用 declare
、双引号和参数扩展。
我经常使用以下技巧来解析包含格式为 key=value otherkey=othervalue etc=etc
的 one to n
参数的参数列表,例如:
# 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 test $# -gt 0; do
case "$1" in ?*=?*) declare "${1%=*}=${1#*=}";; *) break;; esac
shift
done
printf
或 eval
将此处的两个高度评价的答案组合成一个完整的示例,该示例希望有用且不言自明:
#!/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 你知道吗,我家有一只宠物猪
这也可以
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
这应该有效:
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"
根据 BashFAQ/006,您可以使用 read
和 here string syntax 来分配间接变量:
function grep_search() {
read "$1" <<<$(ls | tail -1);
}
用法:
$ grep_search open_box
$ echo $open_box
stack-overflow.txt
一种不依赖于您拥有的 shell/bash 版本的额外方法是使用 envsubst
。例如:
newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)
对于 zsh(较新的 mac os 版本),您应该使用
real_var="holaaaa"
aux_var="real_var"
echo ${(P)aux_var}
holaaaa
代替 ”!”
man zshall
, section PARAMETER EXPANSION, subsection Parameter Expansion Flags 中进行了解释: P:这会强制将参数 name 的值解释为进一步的参数名称,其值将在适当的情况下使用。 [...]
哇,大部分语法都很糟糕!如果您需要间接引用数组,这是一种语法更简单的解决方案:
#!/bin/bash
foo_1=(fff ddd) ;
foo_2=(ggg ccc) ;
for i in 1 2 ;
do
eval mine=( \${foo_$i[@]} ) ;
echo ${mine[@]}" " ;
done ;
对于更简单的用例,我推荐使用 syntax described in the Advanced Bash-Scripting Guide。
foo_1
和 foo_2
中的条目没有空格和特殊符号时才有效。有问题的条目示例:'a b'
将在 mine
内创建两个条目。 ''
不会在 mine
内创建条目。 '*'
将扩展到工作目录的内容。您可以通过引用来防止这些问题:eval 'mine=( "${foo_'"$i"'[@]}" )'
[@]
构造。 "${array[@]}"
将始终扩展到正确的条目列表,而不会出现分词或 *
扩展等问题。此外,如果您知道任何从未出现在数组中的非空字符,则只能使用 IFS
规避分词问题。此外,通过设置 IFS
无法实现对 *
的字面处理。要么设置 IFS='*'
并在星号处拆分,要么设置 IFS=somethingOther
并且 *
展开。
尽管这是一个老问题,但我仍然很难获取动态变量名称,同时避免使用 eval
(邪恶)命令。
使用创建对动态值的引用的 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
我希望能够创建一个包含命令第一个参数的变量名
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}
间接扩展,如前所述,但它不支持检索数组索引。
如需进一步阅读或示例,请查看 BashFAQ/006 about Indirection。
我们不知道有任何技巧可以在没有 eval 的情况下在 POSIX 或 Bourne shell 中复制该功能,这很难安全地完成。因此,请考虑这是一种使用风险自负的黑客攻击。
但是,您应该按照以下说明重新考虑使用间接。
通常,在 bash 脚本中,您根本不需要间接引用。通常,当人们不了解或不了解 Bash 数组或没有充分考虑其他 Bash 特性(例如函数)时,他们会考虑使用此解决方案。将变量名称或任何其他 bash 语法放入参数中经常会不正确且不恰当地在解决问题时有更好的解决方案。它违反了代码和数据之间的分离,因此使您陷入错误和安全问题的滑坡。间接可以使您的代码不那么透明并且更难遵循。
亲吻方法:
a=1
c="bam"
let "$c$a"=4
echo $bam1
结果 4
对于索引数组,您可以像这样引用它们:
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
关联数组可以类似地引用,但需要在 declare
而不是 -a
上打开 -A
。
符合 POSIX 标准的答案
对于此解决方案,您需要对 /tmp
文件夹具有 r/w 权限。
我们创建一个临时文件来保存我们的变量并利用 set
内置的 -a
标志:
$ 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
虽然我认为 declare -n
仍然是最好的方法,但还有另一种没有人提到的方法,在 CI/CD 中非常有用
function dynamic(){
export a_$1="bla"
}
dynamic 2
echo $a_2
此函数不支持空格,因此 dynamic "2 3"
将返回错误。
对于 varname=$prefix_suffix
格式,只需使用:
varname=${prefix}_suffix
不定期副业成功案例分享
prefix_${middle}_postfix
(即您的格式不适用于varname=$prefix_suffix
)