Advanced Shell

基础知识 - 变量和函数

Linux 下的 Shell 有很多, sh, bash, csh, zsh 等, 这里主要记录一下 bash 和 zsh 的一些不同之处

Parameter Expansion

Use an alternate value

1
2
${var:+WORD}
${var+WORD}

如果 var 没有设置或为空,则这个变量展开为 Nothing (注意:不是空 empty, 是 nothing), 如果被设置了(不包括被设置成空),它展开为 + 后面的 WORD.

如果冒号被省略,则 var 即使被设置为空,它也展开为 + 后面的 WORD

Indirect

1
${!var}

如果 var 的值是 MESA_DEBUG, 那么这个形式展开后是变量 MESA_DEBUG 的值,例如 export MESA_DEBUG=1, var=MESA_DEBUG, 则最后的展开结果是 1

Quoted

1
${var@Q}

@Q 指变量展开后的值被单引号引起来,例如 export ABC=abc, echo "ABC=${ABC@Q}" 的结果是 ABC='abc'

反斜杠 backslash \

\ 在 shell 中是用来转义字符的,就是说 echo "\\\\" 显示的实际只有一个 \, 而且 while read var 时要注意加 -r 选项,读入原始字串

经典命令

exec

exec 命令有 2 个特点:

  • 直接覆盖当前进程,就是说进程 PID 不变,但执行的代码被更换了
  • 原来的 shell 环境被销毁,这样当前的代码结束后,也就不会返回原 shell(没得返回), 直接退出, 所以循环中慎用 exec
  • 可以把它想像成系统调用 execve()

下面是 linux kernel 安装 bzImage 的一段代码,其中就使用了 exec, 保证列出的 4 个安装脚本中,只执行第 1 个存在的,不会重复安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# User/arch may have a custom install script
for file in "${HOME}/bin/${INSTALLKERNEL}" \
"/sbin/${INSTALLKERNEL}" \
"${srctree}/arch/${SRCARCH}/install.sh" \
"${srctree}/arch/${SRCARCH}/boot/install.sh"
do
if [ ! -x "${file}" ]; then
continue
fi

# installkernel(8) says the parameters are like follows:
#
# installkernel version zImage System.map [directory]
exec "${file}" "${KERNELRELEASE}" "${KBUILD_IMAGE}" System.map "${INSTALL_PATH}"
done

timeout

timeout 命令用来给一个 COMMAND 设定一个 DURATION, 这在自动化中很有用,比如

1
for i in `fd --type x`; do timeout -k 0.1 10 ./$i; done

假如当前在 VulkanExamples 的 bin 目录下,上面的命令表示让每个 demo 执行 10s 后结束(timeout 给它发 TERM 信号), 如果发了 TERM 信号后,又经过 0.1s (-k 0.1),这个 demo 还未能退出,就再发 KILL 信号,强制结束它

tree

tree 像一个简单的文件浏览器,但它并不是 shell 内置的命令,apt install treepacman -S tree 都可以安装。有时一个目录中包含太多的文件,tree 的默认输出就不太好浏览,这时可以只打印目录,并限制搜索深度

1
tree -L 2 -d

crontab

crontab -e (添加定时任务), 不光可以添加周期性的定时任务,也可以添加开机时一次性任务

1
@reboot /home/luc/mystart.sh

crontab 是每用户的, 就是说当前用户设定的任务,只有当前用户的权限,所以如果有些情况下任务执行需要 root 权限,就需要切换到 root 用户后 crontab -e

文本处理 - awk, sed, grep(rg) 三剑客

Linux 下的文本处理三剑客: grep, sed, awk, 除了它们其实还有一些小巧的命令,如 tr, cut 也可以帮助我们快速处理和格式化文本。
下面以一个例子为例。

awk

1
awk 'program' inputfile1 inputfile2 ...

awk 的调用形式就是上面这样的,而其中的 program 由若干条 rules 组成,而一条 rule 由一个 pattern 和一个 action 组成

1
2
3
pattern { action }
pattern { action }
...

sed

sed 和 awk 一样,都是按行处理文本的。

  • sed -n '2 {s/^/#/; p; q}' file
    • sed 默认会将每一行都打印出来, -n 取消这一行为
    • sed 可以在操作的前面指定位置和范围, 如
      • 行号
      • 正则表达式 /^foo/
      • 两个正则表达式锁定范围 /^foo/, /bar$/
    • 如果 -n 后,完全都不打印了,但如果又想将处理后的行打印出来,使用 p 命令
    • q 命令的作用是立即退出,sed 的默认行为是对第2行处理完后,虽然后面的行都不需要处理,但 sed 仍然会继续将后面的每行往模式空间加载。

grep(rg)

在 Linux 内核源码目录下,搜索 drivers/gpu/drm 下所有的 DRIVER_NAME 定义,并排序后格式化输出

命令如下:

1
2
3
4
rg '#define DRIVER_NAME' drivers/gpu/drm --no-heading \
| tr -s '\t' | tr '\t' ' ' | tr -s ' ' \
| awk -F':' '{printf("%-52s%-40s\n",$1,$2)}' \
| sort -k4
  • rg (ripgrep) 比 grep 更快,更强大
  • tr 在不带任何选项时,默认执行替换,例子是中将 tab 替换成 空格, -s 表示 squeeze-repeats, 就是去掉重复的字符,例如多个空格只保留一个
  • awk 天生支持 C-Style printf

引号

当混合使用 awk 和 sed 时,比较便利地处理引号的方法是定义 awk 变量 -v Q="'"

  • awk 的 printf() 函数第一个参数必须使用双引号 printf("%s: %s%d%s\n", $1, Q, $2, Q)
    • awk 中的用户自定义变量和内置变量,使用时都不需要加 $, 如 NR, NF
  • awk 和 sed 的命令字串必须用引号括起来,当用 awk 生成 sed 命令时,将单引号定义为 awk 变量尤其方便,可读性也强

行范围 (Line range)

sed, awk 都支持行范围,如 sed -n '3,5p', awk 'NR >= 3 && NR <= 5',它们的效果是一样的,都是只打印给定文件的 3 到 5 行, 除了行号, sed, awk 还支持通过正则匹配来指定行位置

1
sed -n '/foo/,$p'
  • $ 表示最后一行
  • p 是 sed print 命令的缩写
1
awk '/foo/ {f=1} f'
  • awk 实现行范围的方式与 sed 稍有不同,它在满足匹配 foo 这个条件时,将布尔变量 f 设置为 1,相当于从这一行开始,开启 print 打印这个默认操作
  • awk 的一条 rule 里,可以省略 pattern, 或者省略 action, 但不能同时两个都省略;如果省略 pattern, 则对每个输入行都执行 action, 如果省略 action, 则对每个匹配行执行 print action。这个例子里面, awk 的 program 包括两条 rules
    • /foo/ {f=1} 这条 rule 什么都没有省略
    • f 这条 rule 省略了 action, 所以对每个输入行执行 print action

它们两个的效果也是一样的,都是从第一个匹配 foo 的行开始,一直打印到文件结束。

正则匹配 (Regex match)

awk 中一个 rule 中的 pattern 部分可以只是一条 / 括起来的正则表达式,默认这个正则表达式的匹配对象是 $0, 也就是整行, 如

1
awk '/foo/'

它实际上是

1
awk '$0~/foo/ {print $0}'

它意思是只要当前行里包含 foo 这个字串,就打印这一行。所以 awk 的正则 pattern, 准确来说是用来搜索

默认情况下是 $0~/foo/, 那么不默认情况下,可以指定哪个 field 去“匹配”, 如

1
awk '$1~/foo/ && $3!~/bar/ {print}'

它意思是当前这条记录,如果第1个字段包含 foo 第3个字段不包含 bar, 那么就打印整条记录