你所不了解的Bash:关于Bash数组的介绍

系统 Linux
尽管软件工程师常常使用命令行来进行各种开发,但命令行中的数组似乎总是一个模糊的东西(虽然不像正则操作符 =~ 那么复杂隐晦)。除开隐晦和有疑问的语法,Bash 数组其实是非常有用的。

[[236575]]

进入这个古怪而神奇的 Bash 数组的世界。

尽管软件工程师常常使用命令行来进行各种开发,但命令行中的数组似乎总是一个模糊的东西(虽然不像正则操作符 =~ 那么复杂隐晦)。除开隐晦和有疑问的语法,Bash 数组其实是非常有用的。

 

稍等,这是为什么?

写 Bash 相关的东西很难,但如果是写一篇像手册那样注重怪异语法的文章,就会非常简单。不过请放心,这篇文章的目的就是让你不用去读该死的使用手册。

 

真实(通常是有用的)示例

为了这个目的,想象一下真实世界的场景以及 Bash 是怎么帮忙的:你正在公司里面主导一个新工作,评估并优化内部数据管线的运行时间。首先,你要做个参数扫描分析来评估管线使用线程的状况。简单起见,我们把这个管道当作一个编译好的 C++ 黑盒子,这里面我们能够调整的唯一的参数是用于处理数据的线程数量:./pipeline --threads 4

 

基础

我们首先要做的事是定义一个数组,用来容纳我们想要测试的 --threads 参数:

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

本例中,所有元素都是数字,但参数并不一定是数字,Bash 中的数组可以容纳数字和字符串,比如 myArray=(1 2 "three" 4 "five") 就是个有效的表达式。就像 Bash 中其它的变量一样,确保赋值符号两边没有空格。否则 Bash 将会把变量名当作程序来执行,把 = 当作程序的***个参数。

现在我们初始化了数组,让我们解析它其中的一些元素。仅仅输入 echo $allThreads ,你能发现,它只会输出***个元素。

要理解这个产生的原因,需要回到上一步,回顾我们一般是怎么在 Bash 中输出变量。考虑以下场景:

  1. type="article"
  2. echo "Found 42 $type"

假如我们得到的变量 $type 是一个单词,我们想要添加在句子结尾一个 s。我们无法直接把 s 加到 $type 里面,因为这会把它变成另一个变量,$types。尽管我们可以利用像 echo "Found 42 "$type"s" 这样的代码形变,但解决这个问题的***方法是用一个花括号:echo "Found 42 ${type}s",这让我们能够告诉 Bash 变量名的起止位置(有趣的是,JavaScript/ES6 在 template literals 中注入变量和表达式的语法和这里是一样的)

事实上,尽管 Bash 变量一般不用花括号,但在数组中需要用到花括号。这反而允许我们指定要访问的索引,例如 echo ${allThreads[1]} 返回的是数组中的第二个元素。如果不写花括号,比如 echo $allThreads[1],会导致 Bash 把 [1] 当作字符串然后输出。

是的,Bash 数组的语法很怪,但是至少他们是从 0 开始索引的,不像有些语言(说的就是你,R 语言)。

 

遍历数组

上面的例子中我们直接用整数作为数组的索引,我们现在考虑两种其他情况:***,如果想要数组中的第 $i 个元素,这里 $i 是一个代表索引的变量,我们可以这样 echo ${allThreads[$i]} 解析这个元素。第二,要输出一个数组的所有元素,我们把数字索引换成 @ 符号(你可以把 @ 当作表示 all 的符号):echo ${allThreads[@]}

 

遍历数组元素

记住上面讲过的,我们遍历 $allThreads 数组,把每个值当作 --threads 参数启动管线:

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

 

遍历数组索引

接下来,考虑一个稍稍不同的方法。不遍历所有的数组元素,我们可以遍历所有的索引:

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

一步一步看:如之前所见,${allThreads[@]} 表示数组中的所有元素。前面加了个感叹号,变成 ${!allThreads[@]},这会返回数组索引列表(这里是 0 到 7)。换句话说。for 循环就遍历所有的索引 $i 并从 $allThreads 中读取第 $i 个元素,当作 --threads 选项的参数。

这看上去很辣眼睛,你可能奇怪为什么我要一开始就讲这个。这是因为有时候在循环中需要同时获得索引和对应的值,例如,如果你想要忽视数组中的***个元素,使用索引可以避免额外创建在循环中累加的变量。

 

填充数组

到目前为止,我们已经能够用给定的 --threads 选项启动管线了。现在假设按秒计时的运行时间输出到管线。我们想要捕捉每个迭代的输出,然后把它保存在另一个数组中,因此我们最终可以随心所欲的操作它。

 

一些有用的语法

在深入代码前,我们要多介绍一些语法。首先,我们要能解析 Bash 命令的输出。用这个语法可以做到:output=$( ./my_script.sh ),这会把命令的输出存储到变量 $output 中。

我们需要的第二个语法是如何把我们刚刚解析的值添加到数组中。完成这个任务的语法看起来很熟悉:

  1. myArray+=( "newElement1" "newElement2" )

 

参数扫描

万事具备,执行参数扫描的脚步如下:

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

就是这个了!

 

还有什么能做的?

这篇文章中,我们讲过使用数组进行参数扫描的场景。我敢保证有很多理由要使用 Bash 数组,这里就有两个例子:

 

日志警告

本场景中,把应用分成几个模块,每一个都有它自己的日志文件。我们可以编写一个 cron 任务脚本,当某个模块中出现问题标志时向特定的人发送邮件:

  1. # 日志列表,发生问题时应该通知的人
  2. logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
  3. logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")
  4.  
  5. # 在每个日志中查找问题标志
  6. for i in ${!logPaths[@]};
  7. do
  8.   log=${logPaths[$i]}
  9.   stakeholder=${logEmails[$i]}
  10.   numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )
  11.  
  12. # 如果近期发现超过 5 个错误,就警告负责人
  13.   if [[ "$numErrors" -gt 5 ]];
  14.   then
  15.     emailRecipient="$stakeholder"
  16.     emailSubject="WARNING: ${log} showing unusual levels of errors"
  17.     emailBody="${numErrors} errors found in log ${log}"
  18.     echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
  19.   fi
  20. done

 

API 查询

如果你想要生成一些分析数据,分析你的 Medium 帖子中用户评论最多的。由于我们无法直接访问数据库,SQL 不在我们考虑范围,但我们可以用 API!

为了避免陷入关于 API 授权和令牌的冗长讨论,我们将会使用 JSONPlaceholder,这是一个面向公众的测试服务 API。一旦我们查询每个帖子,解析出每个评论者的邮箱,我们就可以把这些邮箱添加到我们的结果数组里:

  1. endpoint="https://jsonplaceholder.typicode.com/comments"
  2. allEmails=()
  3.  
  4. # 查询前 10 个帖子
  5. for postId in {1..10};
  6. do
  7. # 执行 API 调用,获取该帖子评论者的邮箱
  8.   response=$(curl "${endpoint}?postId=${postId}")
  9.  
  10. # 使用 jq JSON 响应解析成数组
  11.   allEmails+=( $( jq '.[].email' <<< "$response" ) )
  12. done

注意这里我是用 jq 工具 从命令行里解析 JSON 数据。关于 jq 的语法超出了本文的范围,但我强烈建议你了解它。

你可能已经想到,使用 Bash 数组在数不胜数的场景中很有帮助,我希望这篇文章中的示例可以给你思维的启发。如果你从自己的工作中找到其它的例子想要分享出来,请在帖子下方评论。

 

请等等,还有很多东西!

由于我们在本文讲了很多数组语法,这里是关于我们讲到内容的总结,包含一些还没讲到的高级技巧:

< 如显示不全,请左右滑动 >
语法 效果
arr=() 创建一个空数组
arr=(1 2 3) 初始化数组
${arr[2]} 取得第三个元素
${arr[@]} 取得所有元素
${!arr[@]} 取得数组索引
${#arr[@]} 计算数组长度
arr[0]=3 覆盖第 1 个元素
arr+=(4) 添加值
str=$(ls) ls 输出保存到字符串
arr=( $(ls) ) ls 输出的文件保存到数组里
${arr[@]:s:n} 取得从索引 s 开始的 n 个元素

***一点思考

正如我们所见,Bash 数组的语法很奇怪,但我希望这篇文章让你相信它们很有用。只要你理解了这些语法,你会发现以后会经常使用 Bash 数组。

 

Bash 还是 Python?

问题来了:什么时候该用 Bash 数组而不是其他的脚本语法,比如 Python?

对我而言,完全取决于需求——如果你可以只需要调用命令行工具就能立马解决问题,你也可以用 Bash。但有些时候,当你的脚本属于一个更大的 Python 项目时,你也可以用 Python。

比如,我们可以用 Python 来实现参数扫描,但我们只用编写一个 Bash 的包装:

  1. import subprocess
  2.  
  3. all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
  4. all_runtimes = []
  5.  
  6. # 用不同的线程数字启动管线
  7. for t in all_threads:
  8.   cmd = './pipeline --threads {}'.format(t)
  9.  
  10. # 使用子线程模块获得返回的输出
  11.   p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  12.   output = p.communicate()[0]
  13.   all_runtimes.append(output)

由于本例中没法避免使用命令行,所以可以优先使用 Bash。 

责任编辑:庞桂玉 来源: Linux中国
相关推荐

2019-11-21 15:08:13

DevOps云计算管理

2013-11-11 10:07:43

静态路由配置

2017-03-13 17:25:00

移动支付技术支撑易宝

2017-04-11 09:29:45

WOT

2010-07-27 09:00:32

MySQL锁

2012-03-13 09:32:15

C#协变

2011-03-29 15:44:41

对日软件外包

2020-09-16 07:59:40

数组内存

2016-12-06 08:35:47

浏览器内核Gecko

2010-08-19 10:12:34

路由器标准

2019-04-03 09:10:35

Rediskey-value数据库

2021-07-12 07:01:39

AST前端abstract sy

2018-04-16 23:14:39

SD-WANSDN网络

2010-06-23 16:05:36

Linux Bash

2015-06-05 09:52:41

公有云风险成本

2020-12-10 08:13:15

ARM架构 嵌入式

2022-11-30 07:47:00

Bash脚本

2014-05-06 10:31:21

KillallLinux命令行

2012-02-21 09:20:50

Hadoop大数据

2021-01-14 08:31:54

Web开发应用程序
点赞
收藏

51CTO技术栈公众号