【问题标题】:Bash Arrays - Zip array of variable names with corresponding valuesBash Arrays - 具有相应值的变量名称的 Zip 数组
【发布时间】:2021-07-05 13:36:13
【问题描述】:

在自学 Bash 编程时,我遇到了一个有趣的用例,我想获取环境中存在的变量列表,并将它们放入数组中。然后,我想输出一个变量名称及其值的列表,并将该输出存储在一个数组中,每个变量一个条目。

我只用了大约 2 周的时间以任何“真实”的方式编写 Bash shell 脚本,而且我正在对自己进行数组方面的教育。其他编程语言中的一个常见功能是能够“压缩”两个数组,例如as is done in Python。任何编程语言的另一个共同特征是间接性,例如通过pointer indirection 等。这在很大程度上是学术性的,通过一个有点挑战性的例子来教我自己,但我认为如果除了调试、跟踪整体系统状态等原因之外,这具有广泛的用途。

我想要的是以下输入...:

VAR_ONE="LIGHT RED"
VAR_TWO="DARK GREEN"
VAR_THREE="BLUE"
VARIABLE_ARRAY=(VAR_ONE VAR_TWO VAR_THREE)

...要转换成以下输出(作为一个数组,每行一个元素):

VAR_ONE: LIGHT RED
VAR_TWO: DARK GREEN
VAR_THREE: BLUE

约束:

  • 假设我无法控制所有变量,所以我不能回避问题,例如从一开始就使用关联数组。 (即,请不要建议通过从不使用名为“VAR_ONE”的离散变量来完全避免间接引用查找的需要)。但是将结果存储在关联数组中的解决方案很好。
  • 假设变量名永远不会包含空格,但它们的值可能。
  • 最终输出应该不包含单独的元素,因为输入变量的值包含空格。

到目前为止我所读到的内容:

  • 我读过一些 StackOverflow 的帖子,比如这篇文章,这些帖子处理使用对数组的间接引用它们自己(例如,如果您有三个数组并希望根据“数组选择”变量):How to iterate over an array using indirect reference?
  • 我还发现了一篇文章,它以我所说的方式处理 Bash 中的“压缩”数组,您可以在其中配对,例如array1 和 array2 中的第一个元素,然后将第二个元素配对,等等:Iterate over two arrays simultaneously in bash
  • ...但我还没有找到任何可以充分讨论这个独特用例的内容...

问题:

如果给定一个仅包含变量名称列表的数组,我应该如何创建一个包含变量名称列表及其值(以冒号分隔)的数组。我并不是“没有想出任何方法来做到这一点”,但我想在 Bash 中找到“首选”的方法来做到这一点,考虑到性能、安全性和简洁/易于理解。

编辑:我将发布到目前为止我想出的答案作为这篇文章的答案......但不要将其标记为已回答,因为我还想听到一些公正的建议......

【问题讨论】:

  • 如果您不介意使用变量名作为数组索引,请考虑使用关联数组 (declare -A arrayname);至于你的其他问题......我建议你专注于你遇到的一个问题,然后要么a)更新问题,专注于那个问题,要么b)关闭/删除这个问题并提出一个新的更有针对性的问题;这个问题可能会被标记为关闭,因为a)没有关注一个特定的问题,b)你似乎在征求意见
  • 我不明白你为什么要排除关联数组,因为你没有“控制所有变量”——这不重要吗?我同意 @markp-fuso 的观点,这似乎是同时出现的许多问题。
  • 我承认我在中途停止阅读这个问题(没有重点,漫无边际)所以错过了 OP 关于关联数组的评论;由于值中的空格,关于使用关联数组的消息(?)没有意义......关联数组索引和值可能包含空格......?如果不知道所有名称,则不确定 OP 计划如何“压缩”变量列表... ??
  • @markp-fuso :编辑问题以将我自己的想法作为答案而不是在主帖中。关于一个问题,它是:创建一个包含变量名称列表及其值(冒号分隔)的数组,给定一个仅包含变量名称列表的数组。我承认我并不是“没有想出任何方法来做到这一点”,但我想在 Bash 中找到“首选”的方法来做到这一点,考虑到性能、安全性和简洁/易于理解。我会在原帖中注明。
  • @BenjaminW。如果您有使用关联数组的想法,请分享。我的意思是,我读过一些帖子说“从一开始就将名称/值对(例如 [VAR_1] = LIGHT RED)存储在关联数组中,避免让自己完全需要使用间接引用” ”。假设我正在使用预定义的系统环境变量。但是,如果您有办法从这些变量中收集值并将它们放入一个关联数组,那将是一个很好的解决方案。不过,这会使最后的“打印输出”步骤更加冗长吗?

标签: arrays bash pointers eval indirection


【解决方案1】:

OP 开头为:

VAR_ONE="LIGHT RED"
VAR_TWO="DARK GREEN"
VAR_THREE="BLUE"
VARIABLE_ARRAY=(VAR_ONE VAR_TWO VAR_THREE)

OP 提供了 4 组代码的答案:

# first 3 sets of code generate:

$ typeset -p outputValues
declare -a outputValues=([0]="VAR_ONE: LIGHT RED" [1]="VAR_TWO: DARK GREEN" [2]="VAR_THREE: BLUE")

# the 4th set of code generates the following where the data values are truncated at the first space:

$ typeset -p outputValues
declare -a outputValues=([0]="VAR_ONE: LIGHT" [1]="VAR_TWO: DARK" [2]="VAR_THREE: BLUE")

注意事项:

  • 我假设第 4 组代码的输出是错误的,所以将忽略这个
  • OP 的代码示例涉及我将要使用的几个想法(如下)...nameref 的 (declare -n <variable_name>) 和间接变量引用 (${!<variable_name>})


为了可读性(和其他人的可维护性),我可能会避免各种eval 和扩展想法,而是选择使用bash namesref's (declare -n);一个简单的例子:

$ x=5
$ echo "${x}"
5

$ y=x
$ echo "${y}"
x

$ declare -n y="x"          # nameref => y=(value of x)
$ echo "${y}"
5

将其拉入我们得到的原始问题:

unset outputValues
declare -a outputValues                           # optional; declare 'normal' array

for var_name in "${VARIABLE_ARRAY[@]}"
do
    declare -n data_value="${var_name}"
    outputValues+=("${var_name}: ${data_value}")
done

这给了我们:

$ typeset -p outputValues
declare -a outputValues=([0]="VAR_ONE: LIGHT RED" [1]="VAR_TWO: DARK GREEN" [2]="VAR_THREE: BLUE")

虽然这会产生相同的结果(与 OP 的前 3 组代码一样),但(对我来说)这个新数组将如何使用是一个令人烦恼的问题?

如果唯一的目标是将这些预先格式化的数据打印到标准输出……好吧,不过既然可以对当前数组和 nameref 执行相同的操作,为什么还要为新数组烦恼呢?

如果目标是将这个数组作为变量名/值对的集合来访问以进行处理,那么当前的结构将很难(更)使用,例如,每个数组“值”都需要根据分隔符 :<space> 解析/拆分,以便访问实际的变量名称和值。

在这种情况下,我会选择使用关联数组,例如:

unset outputValues
declare -A outputValues                           # required; declare associative array

for var_name in "${VARIABLE_ARRAY[@]}"
do
    declare -n data_value="${var_name}"
    outputValues[${var_name}]="${data_value}"
done

这给了我们:

$ typeset -p outputValues
declare -A outputValues=([VAR_ONE]="LIGHT RED" [VAR_THREE]="BLUE" [VAR_TWO]="DARK GREEN" )

注意事项:

  • 同样,既然可以对当前数组和 nameref 进行同样的操作,为什么还要为新数组烦恼?
  • 如果变量$data_value 要在后续代码中作为“普通”变量重新使用,则需要删除nameref 属性(unset -n data_value)

使用关联数组(索引=变量名/数组元素=变量值)可以更轻松地引用变量名/值对,例如:

$ myvar=VAR_ONE
$ echo "${myvar}: ${outputValues[${myvar}]}"
VAR_ONE: LIGHT RED

$ for var_name in "${!outputValues[@]}"; do echo "${var_name}: ${outputValues[${var_name}]}"; done
VAR_ONE: LIGHT RED
VAR_THREE: BLUE
VAR_TWO: DARK GREEN


在旧版本的 bash(nameref 可用之前)和新版本的 bash 中仍然可用,可以选择使用间接变量引用;

$ x=5
$ echo "${x}"
5

$ unset -n y        # make sure 'y' has not been previously defined as a nameref
$ y=x
$ echo "${y}"
x

$ echo "${!y}"
5

将其引入关联数组方法:

unset -n var_name                                 # make sure var_name not previously defined as a nameref
unset outputValues
declare -A outputValues                           # required; declare associative array

for var_name in "${VARIABLE_ARRAY[@]}"
do
    outputValues[${var_name}]="${!var_name}"
done

这给了我们:

$ typeset -p outputValues
declare -A outputValues=([VAR_ONE]="LIGHT RED" [VAR_THREE]="BLUE" [VAR_TWO]="DARK GREEN" )

注意:虽然这需要在for 循环中进行较少的编码,但如果您忘记unset -n 变量(在这种情况下为var_name),那么您最终会出错如果 var_name 先前被定义为 nameref,则结果;也许是一个小问题,但它需要编码人员了解并为这个特定问题编写代码......有点太深奥(我的口味)所以我更喜欢坚持使用 namerefs ... ymmv ...

【讨论】:

  • 感谢@markp-fuso,当您在关联数组中打印值时,我特别喜欢使用原始数组。否则,我提出的想法会使那部分变得冗长。我没有完全遵循“再一次,既然可以对当前数组和 nameref 做同样的事情,为什么还要麻烦一个新数组?”我的想法是这样我就可以对输出做任何我想做的事情 - 打印它,格式化它,将它用于以后的分配等等。虽然我同意我的版本中的冒号符号比你的关联数组方法更难!
  • 是的,在第四组代码中,我想我之前在我的机器上设置了IFS=""。从一个新的 shell 运行它,我同意你的发现,它并不完全正确,因为我在这篇文章中列出了代码。一会儿我会更新。
【解决方案2】:

在过去的几天里,我想出了一些可能的解决方案,每一个都有自己的优缺点。不过,我暂时不会将此标记为答案,因为我有兴趣听取公正的建议。


到目前为止我的头脑风暴解决方案:

选项 #1 - FOR-LOOP:

alias PrintCommandValues='unset outputValues
for var in ${VARIABLE_ARRAY[@]}
do outputValues+=("${var}: ${!var}")
done; printf "%s\n\n" "${outputValues[@]}"'
PrintCommandValues

专业人士:传统,易于理解

缺点:有点冗长。我不确定 Bash,但我一直在做很多 Mathematica 编程(命令式),其中这样的循环明显较慢。有人知道 Bash 是否是这样吗?

选项 #2 - 评估:

i=0; outputValues=("${VARIABLE_ARRAY[@]}")
eval declare "${VARIABLE_ARRAY[@]/#/outputValues[i++]+=:\\ $}"
printf "%s\n\n" "${outputValues[@]}"

优点:比 for 循环更短,而且仍然易于理解。

缺点:我不是专家,但由于安全问题,我已经阅读了很多警告,尽可能避免使用eval。当我主要为我的个人机器编写“方便实用的目的”脚本时,我可能不会担心太多,但是......

选项 #3 - 带括号的引用声明:

i=0; declare -a outputValues="(${VARIABLE_ARRAY[@]/%/'\:\ "${!VARIABLE_ARRAY[i++]}"'})"
printf "%s\n\n" "${outputValues[@]}"

优点:超级简洁。我只是偶然发现了这种语法——我在网络上的任何地方都没有找到它。显然,在 Bash 中使用 declare(我使用版本 4.4.20(1)),如果(并且仅当)你在等号之后放置数组样式的 (...) 括号,并引用它,您将获得更多“一轮”扩展/取消引用,类似于eval。正好在玩this post,无意中发现了“额外扩展”的部分。

例如,比较这两个测试:

varName=varOne; varOne=something
declare test1=\$$varName
declare -a test2="(\$$varName)"
declare -p test1 test2

输出:

declare -- test1="\$varOne"
declare -a test2=([0]="something")

相当整洁,我认为...

无论如何,这种方法的缺点是……我从来没有在任何地方看到过它的正式或非正式记录,所以……可移植性……?

此选项的替代方案:

i=0; declare -a LABELED_VARIABLE_ARRAY="(${VARIABLE_ARRAY[@]/%/'\:\ \$"${VARIABLE_ARRAY[i++]}"'})"
declare -a outputValues=("${LABELED_VARIABLE_ARRAY[@]@P}")
printf "%s\n\n" "${outputValues[@]}"

只是为了好玩 - 大括号扩展:

unset outputValues; OLDIFS=$IFS; IFS=; i=0; j=0
declare -n nameCursor=outputValues[i++]; declare -n valueCursor=outputValues[j++]
declare {nameCursor+=,valueCursor+=": "$}{VAR_ONE,VAR_TWO,VAR_THREE}
printf "%s\n\n" "${outputValues[@]}"
IFS=$OLDIFS

优点:???也许速度?

缺点:相当冗长,不太容易理解


不管怎样,这些都是我的方法……它们中的任何一个合理吗,或者你会做一些完全不同的事情吗?

【讨论】:

    猜你喜欢
    • 2011-04-14
    • 1970-01-01
    • 2017-09-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-09
    • 1970-01-01
    • 2018-04-05
    相关资源
    最近更新 更多