你不了解 Bash:Bash 数组简介

进入 Bash 数组这个奇怪而美妙的世界。
448 位读者喜欢这篇文章。
hands programming

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

虽然软件工程师经常使用命令行进行开发的许多方面,但数组可能是命令行中较为晦涩的功能之一(尽管不像正则表达式运算符 =~ 那么晦涩)。但撇开晦涩和有问题的语法不谈,Bash 数组可能非常强大。

等等,为什么?

撰写关于 Bash 的文章具有挑战性,因为一篇文章很容易演变成一本侧重于语法怪癖的手册。 但是请放心,本文的意图是避免让您去 RTFM(Read The F***ing Manual,阅读该死的文档)。

一个真实的(真正有用的)例子

为此,让我们考虑一个真实的场景以及 Bash 如何提供帮助:您正在公司领导一项新工作,以评估和优化内部数据管道的运行时。 第一步,您想做一个参数扫描,以评估管道如何很好地利用线程。 为了简单起见,我们将管道视为已编译的 C++ 黑盒,其中我们可以调整的唯一参数是为数据处理保留的线程数:./pipeline --threads 4

基础知识

我们要做的第一件事是定义一个包含我们要测试的 --threads 参数值的数组

allThreads=(1 2 4 8 16 32 64 128)

在这个例子中,所有元素都是数字,但情况不一定是这样——Bash 中的数组可以包含数字和字符串,例如,myArray=(1 2 "three" 4 "five") 是一个有效的表达式。 与任何其他 Bash 变量一样,请确保等号周围没有空格。 否则,Bash 会将变量名视为要执行的程序,并将 = 作为其第一个参数!

现在我们已经初始化了数组,让我们检索它的一些元素。 您会注意到,简单地执行 echo $allThreads 将只输出第一个元素。

为了理解为什么会这样,让我们退一步,回顾一下我们通常如何在 Bash 中输出变量。 考虑以下情况

type="article"
echo "Found 42 $type"

假设变量 $type 作为单数名词提供给我们,我们想在句子的末尾添加一个 s。 我们不能简单地将 s 添加到 $type,因为那会把它变成一个不同的变量,$types。 虽然我们可以利用诸如 echo "Found 42 "$type"s" 之类的代码扭曲,但解决这个问题的最佳方法是使用花括号:echo "Found 42 ${type}s",这允许我们告诉 Bash 变量的名称从哪里开始和结束(有趣的是,这与 JavaScript/ES6 中用于将变量和表达式注入 模板字符串 的语法相同)。

因此,事实证明,虽然 Bash 变量通常不需要花括号,但数组是必需的。 反过来,这允许我们指定要访问的索引,例如,echo ${allThreads[1]} 返回数组的第二个元素。 不包括括号,例如,echo $allThreads[1],会导致 Bash 将 [1] 视为字符串并按原样输出它。

是的,Bash 数组的语法很奇怪,但至少它们是零索引的,不像其他一些语言(我说的是你,R)。

循环遍历数组

虽然在上面的例子中,我们在数组中使用了整数索引,但让我们考虑两种不会出现这种情况的情况:首先,如果我们想要数组的第 $i 个元素,其中 $i 是一个包含感兴趣的索引的变量,我们可以使用以下方式检索该元素:echo ${allThreads[$i]}。 其次,要输出数组的所有元素,我们将数字索引替换为 @ 符号(您可以将 @ 视为代表 all):echo ${allThreads[@]}

循环遍历数组元素

考虑到这一点,让我们循环遍历 $allThreads 并为每个 --threads 值启动管道

for t in ${allThreads[@]}; do
  ./pipeline --threads $t
done

循环遍历数组索引

接下来,让我们考虑一种略有不同的方法。 与循环遍历数组*元素*不同,我们可以循环遍历数组*索引*

for i in ${!allThreads[@]}; do
  ./pipeline --threads ${allThreads[$i]}
done

让我们分解一下:正如我们在上面看到的,${allThreads[@]} 代表我们数组中的所有元素。 添加感叹号使其成为 ${!allThreads[@]} 将返回所有数组索引的列表(在我们的例子中是 0 到 7)。 换句话说,for 循环正在循环遍历所有索引 $i 并从 $allThreads 读取第 $i 个元素以设置 --threads 参数的值。

这看起来更加刺眼,所以你可能想知道我为什么首先要介绍它。 这是因为有时您需要知道循环中的索引和值,例如,如果您想忽略数组的第一个元素,使用索引可以避免创建另一个变量,然后在循环中递增该变量。

填充数组

到目前为止,我们已经能够为每个感兴趣的 --threads 启动管道。 现在,让我们假设管道的输出是以秒为单位的运行时。 我们希望在每次迭代中捕获该输出并将其保存在另一个数组中,以便我们可以在最后对其进行各种操作。

一些有用的语法

但在深入研究代码之前,我们需要介绍更多的语法。 首先,我们需要能够检索 Bash 命令的输出。 为此,请使用以下语法:output=$( ./my_script.sh ),它会将命令的输出存储到变量 $output 中。

我们需要的第二个语法是如何将我们刚刚检索到的值附加到数组。 执行此操作的语法看起来很熟悉

myArray+=( "newElement1" "newElement2" )

参数扫描

将所有内容放在一起,这是我们用于启动参数扫描的脚本

allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
  runtime=$(./pipeline --threads $t)
  allRuntimes+=( $runtime )
done

瞧!

你还有什么?

在本文中,我们介绍了使用数组进行参数扫描的场景。 但我保证还有更多使用 Bash 数组的理由——这里还有两个例子。

日志警报

在这种情况下,您的应用程序分为多个模块,每个模块都有自己的日志文件。 我们可以编写一个 cron 作业脚本,以便在某些模块出现问题迹象时向正确的人发送电子邮件:

# List of logs and who should be notified of issues
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")

# Look for signs of trouble in each log
for i in ${!logPaths[@]};
do
  log=${logPaths[$i]}
  stakeholder=${logEmails[$i]}
  numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )

  # Warn stakeholders if recently saw > 5 errors
  if [[ "$numErrors" -gt 5 ]];
  then
    emailRecipient="$stakeholder"
    emailSubject="WARNING: ${log} showing unusual levels of errors"
    emailBody="${numErrors} errors found in log ${log}"
    echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
  fi
done

API 查询

假设你想生成一些分析,了解哪些用户对你的 Medium 帖子评论最多。 由于我们没有直接的数据库访问权限,因此 SQL 不可能实现,但我们可以使用 API!

为了避免陷入关于 API 身份验证和令牌的冗长讨论,我们将改用 JSONPlaceholder,一个面向公众的 API 测试服务,作为我们的端点。 一旦我们查询每个帖子并检索所有评论者的电子邮件,我们就可以将这些电子邮件附加到我们的结果数组

endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()

# Query first 10 posts
for postId in {1..10};
do
  # Make API call to fetch emails of this posts's commenters
  response=$(curl "${endpoint}?postId=${postId}")

  # Use jq to parse the JSON response into an array
  allEmails+=( $( jq '.[].email' <<< "$response" ) )
done

请注意,我在这里使用 jq 工具 从命令行解析 JSON。 jq 的语法超出了本文的范围,但我强烈建议您研究一下。

正如你可能想象的那样,还有无数其他场景可以使用 Bash 数组来提供帮助,我希望本文中概述的示例能给你一些思考的方向。 如果您有其他来自您自己工作的示例要分享,请在下面发表评论。

等等,还有更多!

由于我们在本文中介绍了很多数组语法,因此这里是我们所涵盖内容的摘要,以及我们没有涵盖的一些更高级的技巧

语法 结果
arr=() 创建一个空数组
arr=(1 2 3) 初始化数组
${arr[2]} 检索第三个元素
${arr[@]} 检索所有元素
${!arr[@]} 检索数组索引
${#arr[@]} 计算数组大小
arr[0]=3 覆盖第一个元素
arr+=(4) 附加值
str=$(ls) ls 输出保存为字符串
arr=( $(ls) ) ls 输出保存为文件数组
${arr[@]:s:n} 检索 从索引 s 开始的 n 个元素

最后一点想法

正如我们所发现的,Bash 数组确实具有奇怪的语法,但我希望本文让你相信它们非常强大。 一旦你掌握了语法,你就会发现自己经常使用 Bash 数组。

Bash 还是 Python?

这就引出了一个问题:*何时应该使用 Bash 数组而不是其他脚本语言(如 Python)?*

对我来说,一切都归结为依赖关系——如果你只使用对命令行工具的调用就可以解决手头的问题,你最好使用 Bash。 但是对于脚本是更大的 Python 项目的一部分的情况,你最好使用 Python。

例如,我们可以转向 Python 来实现参数扫描,但最终我们只会编写一个 Bash 包装器

import subprocess

all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []

# Launch pipeline on each number of threads
for t in all_threads:
  cmd = './pipeline --threads {}'.format(t)

  # Use the subprocess module to fetch the return output
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  output = p.communicate()[0]
  all_runtimes.append(output)

由于在这种情况下无法避免命令行,因此最好直接使用 Bash。

是时候进行无耻的宣传了

本文基于我在 OSCON 会议上所做的演讲,我在会上展示了现场编码工作坊 你不了解 Bash。没有幻灯片,没有遥控器——只有我和观众在命令行上敲代码,探索 Bash 的奇妙世界。

本文最初发表在 Medium 上,经授权转载。

标签
User profile image.
Robert 是 Invitae 的生物信息学软件工程师,这意味着他花时间...为生物信息学目的而设计软件。具体来说,他开发云应用程序,以实现基因组数据的交互式分析和探索。Robert 拥有 CSHL 的生物信息学博士学位和麦吉尔大学的计算机工程学士学位。

11 条评论

太棒了!

您不仅很好地涵盖了一个主题或问题,而且还提供了真实的工作示例,向读者解释特定函数/功能的用处!这是一种经常被大多数作者或演讲者忽视的策略。不仅如此,您还提供了缺乏对 Python 偏见的数据!

感谢这篇精彩的文章!

需要一个小小的更正。您对 ${arr[@]:s:n} 行为的描述“检索索引 n 到 s+n 的元素”应该改为“检索索引 s 到 s+(n-1) 的元素”。或者更清楚地说,“检索从索引 s 开始的 n 个元素”

我喜欢你使用真实世界的例子来使事情更清楚的方式。与 Python 的比较,与 JavaScript 的相似性,都是你用来吸引程序员尝试一些未被充分利用的东西的好方法,比如 bash 数组(尤其是在几乎每个操作系统,包括 Windows 上,现在都可以使用 bash 的情况下)。

我使用 BASH 数组已经 9 年了,从 2009 年开始。我想要一个基于 CLI 菜单的工具来操作/登录我的无线路由器,我使用数组从纯文本文件中加载路由器记录(我不关心安全问题,它存储在 root 帐户中)。它达到了 2900 行代码,但功能非常强大,我可以 ssh 并使用基于菜单的应用程序。

文本中还有另一个小错误。基本上,情况类似于 $* 和 "$@" 之间的差异。 如果你评估数组以使用 * 符号获取所有元素,那么可以写 ${array[*]}。 但是在使用 @ 符号的情况下,它总是必须与双引号一起使用,例如 "${array[@]}"。 除非有带有空格符号的元素,否则它不是那么关键,因为那样你会丢失特定元素的真实值。
这是一个例子

$ array1=(1 "two three" 4 five)

$ for i in ${array1[*]}; do echo ${i}; done
1
two
three
4
five

$ for i in ${array1[@]}; do echo ${i}; done
1
two
three
4
five

$ for i in "${array1[@]}"; do echo ${i}; done
1
two three
4
five

您使用了一个 type="article" 的例子。
建议:在 bash shell 中“help type”,并注意这是一个 shell 内置命令,因此最好将包含单词“article”的 shell 变量命名为其他名称。

我在这里发表了一篇评论

http://mcgowans.org/marty3/commonplace/software/arraysProCon.html

我在函数方面保持了立场。 我长期以来一直是函数的倡导者,否则我就不会这样做,但我鼓励大家利用将数组添加到他们的实践中,然后 _再_ 考虑函数。

这篇文章指向我的“Commonplace”书,主要致力于 bash shell。 此外,我在 leanpub 上发布了“Shell Functions”,但迫切需要修订。

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.