你不了解 Bash: Bash 数组入门

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

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

尽管软件工程师经常使用命令行进行开发的许多方面,但数组可能是命令行中较为晦涩的功能之一(尽管不如正则表达式运算符 =~ 那样晦涩)。但抛开晦涩难懂和值得商榷的语法不谈,Bash 数组可能非常强大。

等等,这是为什么?

撰写关于 Bash 的文章具有挑战性,因为文章很容易演变成侧重于语法怪癖的手册。但请放心,本文的目的在于避免让您阅读手册。

一个真实(且真正有用)的示例

为此,让我们考虑一个真实世界的场景以及 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。我们不能简单地在 $type 中添加一个 s,因为这会将其变成一个不同的变量 $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 数组)的几个好方法(特别是当 bash 现在几乎在每个操作系统(包括 Windows)上可用时)。

我从 2009 年开始使用 BASH 数组,至今已有 9 年了。我想要一个基于 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”,但它非常需要修订。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.