如何在Shell脚本中动态生成新的变量名?

40

我正在尝试在shell脚本中生成动态变量名,以便在循环中处理一组具有不同名称的文件,如下所示:

#!/bin/bash

SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'

for (( i = 1; i <= 2; i++ ))
do
  echo SAMPLE{$i}
done

我期望的输出是:

1-first.with.custom.name
2-second.with.custom.name

但我得到了:

SAMPLE{1}
SAMPLE{2}

能否在运行时生成变量名称?


4
你为什么不使用数组?参见BashFAQ/006 - Dennis Williamson
@DennisWilliamson 主要是因为这是我脑海中首先想到的想法,并且需要快速进行测试。 - pQB
请参阅Bash中的动态变量名 - pjh
6个回答

75
你需要使用变量间接引用:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'

for (( i = 1; i <= 2; i++ ))
do
   var="SAMPLE$i"
   echo ${!var}
done

来自 Bash man page,在“参数扩展”下:

“如果参数的第一个字符是感叹号(!),则引入变量间接级别。Bash使用从参数其余部分形成的变量值作为变量的名称;然后扩展该变量并将该值用于其余的替换,而不是参数本身的值。这被称为间接扩展。”


1
这在bash中实际上是有效的,太棒了!@johnshen64,你从哪里学到的?在man页面中它叫什么? - Miquel
11
请查看Bash手册中的“参数扩展”部分。对于${parameter},如果parameter的第一个字符是感叹号(!),则会引入一级变量间接性。Bash使用由parameter剩余部分形成的变量值作为变量名;然后该变量被扩展,并且该值在其余替换中被使用,而不是parameter本身的值。这被称为间接扩展。 - dogbane
1
太棒了,非常感谢提供参考。我认为这是最好的答案。 - Miquel
如果我可以多次点赞,我一定会的!非常感谢你! - user1118321
有人知道用纯POSIX的方式来做这件事吗?并且能够在sh/dash中工作吗? - Robin Winslow
显示剩余3条评论

20

问题

你正在像使用数组索引一样使用变量i的值。但它不是,因为SAMPLE1和SAMPLE2是单独的变量,而不是一个数组。

此外,在调用echo SAMPLE{$i}时,你只是将i的值附加到单词"SAMPLE"上。在这个语句中,你只是取消引用的唯一变量是$i,这就是你得到的结果。

解决问题的方法

有两种主要的解决方法:

  1. 通过eval内置函数或间接变量扩展对插值变量进行多级解引用。
  2. 迭代数组,或使用i作为数组索引。

使用eval进行解引用

在这种情况下,最简单的做法是使用eval

SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'

for (( i = 1; i <= 2; i++ )); do
    eval echo \$SAMPLE${i}
done
这将把变量i的值附加到末尾,然后重新处理生成的行,扩展插入的变量名(例如SAMPLE1SAMPLE2)。

使用间接变量进行取消引用

这个问题的被接受答案是:

SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'

for (( i = 1; i <= 2; i++ ))
do
   var="SAMPLE$i"
   echo ${!var}
done

这其实是一个三步骤的过程。首先,它会给var分配一个插值变量名,然后对存储在var中的变量名进行取消引用,并最终展开结果。这看起来更加简洁,有些人比使用eval语法更为舒适,但结果基本相同。

遍历数组

你可以通过遍历数组来简化循环和展开。例如:

SAMPLE=('1-first.with.custom.name' '2-second.with.custom.name')
for i in "${SAMPLE[@]}"; do
    echo "$i"
done

使用数组的方法相较于其他方法具有额外的好处,具体包括:

  1. 不需要指定复杂的循环测试。
  2. 使用$SAMPLE[$i]语法可以访问单个数组元素。
  3. 可以通过${#SAMPLE}变量扩展获取元素总数。

适用于原始示例的实际等效性

对于原始问题中给出的示例,所有三种方法都可以工作,但是使用数组解决方案提供了最大的灵活性。选择最适合手头数据的方法即可。


同意chepner的观点——这是一个很好的答案,但是如果保留使用eval的示例,则应该发出警告以避免这种用法。如果全面性是目标,还可以添加一个关于Bash 4的关联数组的示例。 - Charles Duffy
4
如果您感到有必要,可以随时指出eval在此用例中与间接变量的不同之处。然而,很多人下意识地避免使用eval,即使它是完成工作的正确工具--如果您信任输入源,您就可以信任eval。在每篇帖子中添加关于eval潜在危害的免责声明,有点像那些无所不在的标签上写着“警告:这杯咖啡可能很烫”。 - Todd A. Jacobs
1
我已经取消了我的踩。我认为在可用间接变量扩展的情况下使用eval是过度杀伤力的。这两种方法都不够好,因为它们只是模拟数组索引,而数组索引已经可用。 - chepner
@chepner +1 对于数组索引。我完全同意这是最好的解决方案,我甚至在我的答案中强调了我认为的原因。 - Todd A. Jacobs
谢谢!运行良好! - Sakthivel
3
三种解决方案中,只有eval在ash环境下能正常工作。点赞此答案。如果命令从未被使用过,它就不会存在。 - Hamy

3
据我所知,@johnshen64所说的方法是可行的。另外,您可以使用如下数组来解决您的问题:
SAMPLE[1]='1-first.with.custom.name'
SAMPLE[2]='2-second.with.custom.name'

for (( i = 1; i <= 2; i++ )) do
    echo ${SAMPLE[$i]}
done

请注意,您不需要使用数字作为索引,SAMPLE[hello]同样有效。

1
这不是一个关联数组,而是一个普通数组。关联数组使用任意字符串作为索引。 - chepner
或者您可以这样完成任务(从零开始):SAMPLE=('1-first.with.custom.name' '2-second.with.custom.name') 或者从一开始:SAMPLE=([1]='1-first.with.custom.name' '2-second.with.custom.name')。如果需要,您可以将赋值拆分成多行。 - Dennis Williamson

3
您可以按照以下方式使用eval
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'

for (( i = 1; i <= 2; i++ ))
do
  eval echo \$SAMPLE$i
done

1
Eval引入了安全问题,而关联数组或间接变量则不会。请参见http://mywiki.wooledge.org/BashFAQ/048。 - Charles Duffy

2

这不是一个独立的答案,只是对Miquel的回答的补充,我无法在评论中很好地适应。

你也可以使用循环、+=运算符和here文档来填充数组:

SAMPLE=()
while read; do SAMPLE+=("$REPLY"); done <<EOF
1-first.with.custom.name
2-second.with.custom.name
EOF

在Bash 4.0中,它非常简单,只需要:
readarray SAMPLE <<EOF
1-first.with.custom.name
2-second.with.custom.name
EOF

Bash 4.0版本非常方便!谢谢! - Sakthivel

0

eval "echo $SAMPLE${i}"


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接