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


当前回答

使用声明

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

我经常使用下面的技巧来解析包含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

其他回答

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

varname=${prefix}_suffix

除了关联数组之外,在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命令和安全问题

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

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

根据帮助评估:

作为shell命令执行参数。


你也可以使用Bash ${!间接展开,正如前面提到的,但是它不支持检索数组下标。


欲进一步阅读或示例,请查看BashFAQ/006关于Indirection的内容。

我们不知道有任何技巧可以在POSIX或Bourne shell中复制该功能,而不需要eval,这可能很难安全地做到。所以,考虑这是一个自担风险的使用黑客。

但是,您应该根据以下注意事项重新考虑使用间接方法。

Normally, in bash scripting, you won't need indirect references at all. Generally, people look at this for a solution when they don't understand or know about Bash Arrays or haven't fully considered other Bash features such as functions. Putting variable names or any other bash syntax inside parameters is frequently done incorrectly and in inappropriate situations to solve problems that have better solutions. It violates the separation between code and data, and as such puts you on a slippery slope toward bugs and security issues. Indirection can make your code less transparent and harder to follow.

我最近一直在寻找更好的方法。联想数组对我来说太夸张了。看看我发现了什么

suffix=bzz
declare prefix_$suffix=mystr

...然后……

varname=prefix_$suffix
echo ${!varname}

从文档中可以看出:

The ‘$’ character introduces parameter expansion, command substitution, or arithmetic expansion. ... The basic form of parameter expansion is ${parameter}. The value of parameter is substituted. ... If the first character of parameter is an exclamation point (!), and parameter is not a nameref, it introduces a level of indirection. Bash uses the value formed by expanding the rest of parameter as the new parameter; this is then expanded and that value is used in the rest of the expansion, rather than the expansion of the original parameter. This is known as indirect expansion. The value is subject to tilde expansion, parameter expansion, command substitution, and arithmetic expansion. ...

POSIX兼容的答案

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

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

因此,如果我们创建一个包含动态变量的文件,我们可以使用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