虽然软件工程师经常使用命令行进行开发的许多方面,但数组可能是命令行中较为晦涩的功能之一(尽管不如正则表达式运算符 =~
那么晦涩)。但撇开晦涩难懂和有问题的语法不谈,Bash 数组可能非常强大。
等等,但是为什么?
撰写关于 Bash 的文章具有挑战性,因为文章很容易演变成专注于语法怪癖的手册。然而,请放心,本文的目的是避免让您 RTFM(阅读手册)。
一个真实(真正有用)的例子
为此,让我们考虑一个真实世界的场景以及 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 上,并已获得许可转载。
11 条评论