在bash中使用数组

程序员经常使用bash解决与软件开发相关的许多任务。 同时,bash数组通常被认为是此shell最不可理解的功能之一(在这方面,数组仅次于正则表达式)。 该材料的作者(我们今天将其翻译发表)邀请所有人进入bash数组的奇妙世界,如果您习惯了其不寻常的语法,则可以带来很多好处。

图片

bash阵列派上用场的真正挑战


关于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 。 但是,您不能仅在变量名的末尾添加该字母,因为这会将变量调用命令转换为$types ,也就是说,我们将使用完全不同的变量。 在这种情况下,可以使用类似echo "Found 42 "$type"s"的构造。 但是最好使用大括号解决此问题: echo "Found 42 ${type}s" ,它使我们可以告诉bash变量名称的开始和结束位置(有趣的是,JavaScript ES6使用相同的语法来嵌入变量)在模式字符串的表达式中)。

现在回到数组。 事实证明,尽管在使用变量时通常不需要花括号,但在数组中则需要花括号。 它们允许您设置索引以访问数组的元素。 例如,格式为echo ${allThreads[1]}将输出数组的第二个元素。 如果您忘记了上述结构中的花括号,bash会认为[1]是字符串,并处理相应的情况。

如您所见,bash中的数组的语法很奇怪,但是至少在其中,元素的编号从头开始。 这使得它们类似于来自许多其他编程语言的数组。

访问数组元素的方法


在上面的示例中,我们在显式指定的数组中使用了整数索引。 现在考虑使用数组的两种其他方法。

如果我们需要数组的第$i个元素,则第一种方法适用,其中$i是包含所需数组元素的索引的变量。 您可以使用echo ${allThreads[$i]}形式的构造从数组中提取此元素。

第二种方法允许您显示数组的所有元素。 它包括用@符号替换数字索引(可以将其解释为指向数组所有元素的命令)。 看起来像这样: echo ${allThreads[@]}

循环遍历数组元素


上述处理数组元素的原理对我们解决枚举数组元素的问题很有用。 在我们的例子中,这意味着使用每个值启动正在研究的pipeline命令,这表示线程数并存储在数组中。 看起来像这样:

 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的数组的所有索引上for ,并且在循环的主体中,使用${allThreads[$i]}构造--thread参数值的数组元素。

与上一个示例相比,读取此代码更加困难。 因此,提出了所有这些困难是为了什么的问题。 我们之所以需要这样做,是因为在某些情况下,当在循环中处理数组时,您需要知道元素的索引和值。 举例来说,如果您需要跳过数组的第一个元素,那么对索引进行迭代将为我们节省例如创建额外变量的需要,以及避免在循环中递增它以使用数组元素的需要。

填充数组


到目前为止,我们一直在通过调用pipeline命令并向其传递感兴趣的--threads参数的每个值来探索该系统。 现在假设该命令以秒为单位给出了某个过程的持续时间。 我们希望拦截每次迭代返回的数据并将其保存在另一个数组中。 所有测试结束后,这将使我们有机会使用存储的数据。

有用的语法构造


在讨论如何向数组添加数据之前,让我们看一下一些有用的语法构造。 首先,我们需要一种通过bash命令获取数据输出的机制。 为了捕获命令的输出,您需要使用以下构造:

 output=$( ./my_script.sh ) 

执行此命令后, myscript.sh$output将存储在$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作业脚本,如果在相应的日志文件中发现问题,则将通过电子邮件通知负责每个模块的人员:

 #  -    logPaths=("api.log" "auth.log" "jenkins.log" "data.log") logEmails=("jay@email" "emma@email" "jon@email" "sophia@email") #         for i in ${!logPaths[@]}; do log=${logPaths[$i]} stakeholder=${logEmails[$i]} numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l ) #       5  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查询。 但是,您可以使用各种AP​​I来访问此类数据。

为了避免长时间讨论身份验证和令牌,我们将使用公共API测试服务JSONPlaceholder作为端点。 从服务接收发布并从评论者的电子邮件地址从其代码中提取数据之后,我们可以将该数据放入数组中:

 endpoint="https://jsonplaceholder.typicode.com/comments" allEmails=() #   10  for postId in {1..10}; do #    API       response=$(curl "${endpoint}?postId=${postId}") #  jq   JSON       allEmails+=( $( jq '.[].email' <<< "$response" ) ) done 

请注意,这里使用了jq工具,该工具允许在命令行上解析JSON。 如果您对此工具感兴趣,我们将不涉及使用jq的详细信息-请参阅有关它的文档。

Bash还是Python?


数组-一个有用的功能,不仅在bash中可用。 为命令行编写脚本的人可能会遇到一个逻辑问题,即在哪些情况下值得使用bash,以及在哪种情况下(例如Python)。

我认为,该问题的答案在于程序员在多大程度上依赖特定技术。 说,如果可以直接在命令行上解决问题,那么没有什么可以阻止bash的使用。 但是,例如,如果您感兴趣的脚本是用Python编写的项目的一部分,则可以很好地使用Python。

例如,要解决此处考虑的问题,您可以使用以Python编写的脚本,但是,这归结为针对bash的Python包装器编写包装器:

 import subprocess all_threads = [1, 2, 4, 8, 16, 32, 64, 128] all_runtimes = [] #         for t in all_threads: cmd = './pipeline --threads {}'.format(t) #   subprocess   ,    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) output = p.communicate()[0] all_runtimes.append(output) 

在不涉及其他技术的情况下,使用bash解决此问题的方法也许更短,更容易理解,并且在这里您可以完全不用Python。

总结


在本文中,我们分析了许多用于数组的设计。 这是一张表格,您可以在其中找到我们已审查的内容和新内容。
语法构造内容描述
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的元素到具有索引s+(n-1)元素获取数组元素

乍一看,bash数组可能看起来很奇怪,但是它们提供的可能性值得应对这些奇怪情况。 我们相信掌握了bash数组后,您将经常使用它们。 容易想象出无数种情况,这些阵列可以派上用场。

亲爱的读者们! 如果您有在bash脚本中使用数组的有趣示例,请分享。

Source: https://habr.com/ru/post/zh-CN413725/


All Articles