ChatGPT解决这个技术问题 Extra ChatGPT

Bash 中的动态变量名

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

不。使用关联数组将命令名称映射到数据。
VAR=A;值=333;阅读 "$VAR" <<< "$VAL";回声“A = $A”
这什么时候有用?
@Timo 例如带有参数索引,如 "${!ARGUMENT_INDEX:-default}"

B
Bruno Bronosky

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

suffix=bzz
declare prefix_$suffix=mystr

...接着...

varname=prefix_$suffix
echo ${!varname}

docs

'$' 字符引入了参数扩展、命令替换或算术扩展。 ...参数扩展的基本形式是${parameter}。参数的值被替换。 ...如果参数的第一个字符是感叹号(!),并且参数不是nameref,则它引入了间接级别。 Bash 使用扩展其余参数形成的值作为新参数;然后将 this 展开,并将该值用于其余的展开,而不是原始参数的展开。这称为间接扩展。该值受波浪号扩展、参数扩展、命令替换和算术扩展的影响。 ...


最好使用封装变量格式:prefix_${middle}_postfix(即您的格式不适用于 varname=$prefix_suffix
我被 bash 3 卡住了,无法使用关联数组;因此,这是一个救生员。 ${!...} 用谷歌搜索并不容易。我假设它只是扩展了一个 var 名称。
@NeilMcGill:参见“man bash”gnu.org/software/bash/manual/html_node/…:参数扩展的基本形式是 ${parameter}。 <...>如果参数的第一个字符是感叹号(!),则引入了变量间接级别。 Bash 使用由其余参数形成的变量的值作为变量的名称;然后扩展此变量,并将该值用于替换的其余部分,而不是参数本身的值。
@syntaxerror:您可以使用上面的“声明”命令尽可能多地分配值。
@Yorik.sar 您可以在答案本身中包含手册的摘录吗?这样更有意义,因为无论如何你都是作者。 TIA。
k
kenorb

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

# 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


@DeaDEnD -a 声明一个索引数组,而不是关联数组。除非 grep_search 的参数是数字,否则它将被视为具有数值的参数(如果未设置参数,则默认为 0)。
唔。我正在使用 bash 4.2.45(2) 并声明未将其列为选项 declare: usage: declare [-afFirtx] [-p] [name[=value] ...]。然而,它似乎工作正常。
为什么不只是 declare $varname="foo"
有谁知道可以与 sh/dash 一起使用的纯 POSIX 方式吗?
${!varname} 更简单且广泛兼容
M
Maëlan

除了关联数组之外,在 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 的中间变量。
很难理解这个答案没有得到更高的评价
我对这个答案的唯一疑虑是它使用了 gratuitously incompatible function funcname() { syntax;它在与问题实际相关的所有内容上都很准确。 :)
@Maëlan - 你说:“所有这些混叠技术都存在一些风险。” printf -v 存在哪些风险? (除了不能移植到超过 17 年的 bash 版本。)
@mpb 紧随其后的句子中显示的风险。 :-) 如果 name='x[$(evil)]' 则每个 printf -v "$name" '%s' '...' 评估 evil
M
Martlark

下面的示例返回 $name_of_var 的值

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

使用命令替换(缺少引号)嵌套两个 echo 是不必要的。另外,应将选项 -n 提供给 echo。而且,和往常一样,eval 是不安全的。但是所有这些都是不必要的,因为 Bash 有一个更安全、更清晰、更短的语法来实现这个目的:${!var}
l
laconbass

使用声明

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

我经常使用以下技巧来解析包含格式为 key=value otherkey=othervalue etc=etcone 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

这看起来是一个非常干净的解决方案。没有邪恶的围兜和鲍勃,您使用与变量相关的工具,而不是模糊看似无关甚至危险的功能,例如 printfeval
N
Nagev

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

#!/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 你知道吗,我家有一只宠物猪


k
k_vishwanath

这也可以

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

J
Jahid

这应该有效:

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"

k
kenorb

根据 BashFAQ/006,您可以使用 readhere string syntax 来分配间接变量:

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

用法:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt

j
jpbochi

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

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

thx 为单行版本。唯一的条件是应该导出变量,否则 envsubst 将看不到它。
t
tomascharad

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

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

代替 ”!”


P 是什么意思?
它在 man zshall, section PARAMETER EXPANSION, subsection Parameter Expansion Flags 中进行了解释: P:这会强制将参数 name 的值解释为进一步的参数名称,其值将在适当的情况下使用。 [...]
i
ingyhere

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

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


ABS 因在其示例中展示不良做法而臭名昭著。请考虑改用 bash-hackers wikiWooledge wiki(具有直接主题条目 BashFAQ #6)。
这仅在 foo_1foo_2 中的条目没有空格和特殊符号时才有效。有问题的条目示例:'a b' 将在 mine 内创建两个条目。 '' 不会在 mine 内创建条目。 '*' 将扩展到工作目录的内容。您可以通过引用来防止这些问题:eval 'mine=( "${foo_'"$i"'[@]}" )'
@Socowi 这是循环遍历 BASH 中的任何数组的一般问题。这也可以通过临时更改 IFS(然后当然再改回来)来解决。很高兴看到报价成功。
@ingyhere 我不敢苟同。这不是一个普遍的问题。有一个标准解决方案:始终引用 [@] 构造。 "${array[@]}" 将始终扩展到正确的条目列表,而不会出现分词或 * 扩展等问题。此外,如果您知道任何从未出现在数组中的非空字符,则只能使用 IFS 规避分词问题。此外,通过设置 IFS 无法实现对 * 的字面处理。要么设置 IFS='*' 并在星号处拆分,要么设置 IFS=somethingOther 并且 * 展开。
@Socowi循环中的一般问题是默认情况下会发生标记化,因此引用是允许包含标记的扩展字符串的特殊解决方案。我更新了答案以删除使读者感到困惑的引用数组值。该答案的重点是创建更简单的语法,而不是针对需要引号来详细说明扩展变量的用例的特定答案。特定用例的作业引用可以留给其他开发人员的想象。
M
Meir Gabay

尽管这是一个老问题,但我仍然很难获取动态变量名称,同时避免使用 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

C
Community

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

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 语法放入参数中经常会不正确且不恰当地在解决问题时有更好的解决方案。它违反了代码和数据之间的分离,因此使您陷入错误和安全问题的滑坡。间接可以使您的代码不那么透明并且更难遵循。


N
Noam Manos

亲吻方法:

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

结果 4


“echo bam1”将输出“bam1”,而不是“4”
W
Walf

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

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


T
Teodoro

符合 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

J
JB68

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

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

dynamic 2
echo $a_2

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


f
fedorqui

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

varname=${prefix}_suffix