我对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, ${…}, \$${...},但我还是很困惑。


当前回答

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

在下面的例子中,我将假设I =37,并且您想要别名名为var_37的变量,其初始值为lolilol。

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

您可以简单地将变量名存储在间接变量中,这与C指针没有什么不同。然后Bash有一个读取别名变量的语法:${!Name}展开为变量名为变量名的变量的值。您可以把它看作是一个两阶段的扩展:${!Name}展开为$var_37,而$var_37展开为lolilol。

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.

不幸的是,没有用于修改别名变量的对应语法。相反,你可以用以下技巧之一来完成任务。

1一个。用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是邪恶的。

1 b。用read赋值

内置的read允许你给一个变量赋值,这个变量的名字是你指定的,这个事实可以和here-strings结合使用:

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

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

注意,因为我们使用的是here-string,所以值后面会附加一个换行符。

缺点:

有点模糊的; 返回非零退出码; 将换行符追加到值。

1 c。使用printf赋值

自Bash 3.1(2005年发布)以来,内置printf还可以将其结果赋值给给定名称的变量。与之前的解决方案相比,它只是工作,不需要额外的努力来转义东西,防止分裂等等。

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

缺点:

不那么便携(但是,好吧)。

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

自Bash 4.3(2014年发布)以来,声明内置有一个选项-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”

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

缺点:

不可移植的。

风险

All these aliasing techniques present several risks. The first one is executing arbitrary code each time you resolve the indirection (either for reading or for assigning). Indeed, instead of a scalar variable name, like var_37, you may as well alias an array subscript, like arr[42]. But Bash evaluates the contents of the square brackets each time it is needed, so aliasing arr[$(do_evil)] will have unexpected effects… As a consequence, only use these techniques when you control the provenance of the alias.

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命令和安全问题

其他回答

下面的示例返回$name_of_var的值

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

使用声明

没有必要像其他答案一样使用前缀,也没有数组。只使用声明、双引号和参数展开。

我经常使用下面的技巧来解析包含1到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 test $# -gt 0; do
  case "$1" in ?*=?*) declare "${1%=*}=${1#*=}";; *) break;; esac
  shift
done

将两个评分较高的答案结合成一个完整的例子,希望有用且不言自明:

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

输出:

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

同样,但是读取的是常规变量: 你知道吗,我家里有一只宠物猫 你知道吗,我家里有只宠物鸡 你知道吗,我家里有一头宠物牛 你知道吗,我家里有一只宠物狗 你知道吗,我家里有只宠物猪

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

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

#!/bin/bash

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

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

对于更简单的用例,我推荐使用高级bash脚本编写指南中描述的语法。