shell script notes

Lec 02: Shell as a script and a tool

0. 概览

shell 脚本是一种比较复杂的工具,可以用来优化创建命令/执行/读取的过程。这样比对应 c++ 程序要简单/高效得多。

1. Shell 脚本

1.1 变量

如果要给变量赋值,我们可以使用 foo=bar,如果要访问变量的值,我们要使用 $foo 来访问。

值得注意的是,用 ‘ 和 “ 包括字符串的意义并不相同。前者不会转义,后者则会转义。

1
2
3
4
5
foo=bar
echo "$foo"
# 打印 bar
echo '$foo'
# 打印 $foo

1.2 脚本

举个例子,如果我希望评测 code.cpp 中的代码,就可以这样写一个 judge 文件:

1
2
3
4
#!/bin/bash
g++ code.cpp -o code
./code < 1.in > 1.out
diff -qZB 1.out 1.ans

第一行是为了告诉系统这是一个 bash 脚本,这样系统就会依次执行后面的代码。

下面我们写一个跑很多测试点的脚本:

1
2
3
4
5
6
7
8
#!/bin/bash
for i in $(seq 1 10); do
if ./code < $i.in > $i.out; then
diff -qZB $i.{out.ans}
else
echo "RE for testpoint $i"
fi
done

1.3 参数

bash 有很多特殊的变量来表示参数、错误代码和相关变量。下面是一些例子:

  • $0-脚本名称
  • $1-脚本的第一个参数
  • $@-脚本的所有参数
  • $#-参数个数
  • $$-当前进程识别码
  • $?-前一条指令的返回值
  • !!-完整的上一条指令

所有的非 0 返回值都代表运行时有错误,例如程序 false

1.4 替换

另一个常见的模式是以变量的形式获取一个命令的输出,这可以通过 命令替换(command substitution)实现。

当您通过 $( CMD ) 这样的方式来执行 CMD 这个命令时,它的输出结果会替换掉 $( CMD )

例如,如果执行 for file in $(ls) ,shell 首先将调用 ls ,然后遍历得到的这些返回值。

还有一个冷门的类似特性是 进程替换(process substitution), <( CMD ) 会执行 CMD 并将结果输出到一个临时文件中,并将 <( CMD ) 替换成临时文件名。这在我们希望返回值通过文件而不是STDIN传递时很有用。例如, diff <(ls foo) <(ls bar) 会显示文件夹 foo 和 bar 中文件的区别。

1.5 综合的例子

这段脚本会遍历我们提供的参数,使用 grep 搜索字符串 foobar,如果没有找到,则将其作为注释追加到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

echo "Starting program at $(date)" # date会被替换成日期和时间

echo "Running program $0 with $# arguments with pid $$"

for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
# 如果模式没有找到,则grep退出状态为 1
# 我们将标准输出流和标准错误流重定向到Null,因为我们并不关心这些信息
if [[ $? -ne 0 ]]; then
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done

感觉还是容易看懂的。需要注意的事情是比较操作最好用 [[]] 包括,这样会降低犯错的几率。

1.6 glob

bash 允许我们基于文件拓展名展开表达式。

  • 例如我可以用 rm test/*.v 来删除 test 目录下的所有 verilog 源文件
  • 又比如我可以通过 {} 来展示一些有公共子串的输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
convert image.{png,jpg}
# 会展开为
convert image.png image.jpg

cp /path/to/project/{foo,bar,baz}.sh /newpath
# 会展开为
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath

# 也可以结合通配使用
mv *{.py,.sh} folder
# 会移动所有 *.py 和 *.sh 文件

mkdir foo bar

# 下面命令会创建foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h这些文件
touch {foo,bar}/{a..h}
touch foo/x bar/y
# 比较文件夹 foo 和 bar 中包含文件的不同
diff <(ls foo) <(ls bar)
# 输出
# < x
# ---
# > y

2. Shell 作为一种工具

2.1 查找文件

find 是一种绝佳的查找工具(但是很慢)。它会递归地搜索符合条件的文件:

1
2
3
4
5
6
7
8
# 查找所有名称为src的文件夹
find . -name src -type d
# 查找所有文件夹路径中包含test的python文件
find . -path '*/test/*.py' -type f
# 查找前一天修改的所有文件
find . -mtime -1
# 查找所有大小在500k至10M的tar.gz文件
find . -size +500k -size -10M -name '*.tar.gz'

除了单纯的查找,我们还能对查找到的文件进行操作。这是通过 -exec 简述实现的。例如:

1
2
3
4
# 删除全部扩展名为.tmp 的文件
find . -name '*.tmp' -exec rm {} \;
# 查找全部的 PNG 文件并将其转换为 JPG
find . -name '*.png' -exec convert {} {}.jpg \;

2.2 查找代码

grep 指令是一个非常好的工具。其中有很多有用的参数:

  • -C: 获取查找结果的上下午,例如 grep -C 10 就是显示上下十行
  • -v 选出不匹配的结果
  • -R 递归进行子目录

它也有一些替代品,例如 rg,你可以通过 sudo apt install ripgrep 来安装之。下面是一些使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
brucelee@invo1ution:~/Interest/Missing-Semester/2-Shell_script$ rg -t md "#" // 查找所有用了 '#' 的 .md 文件
shell_script_notes.md
1:# Lec 02: Shell as a script and a tool
3:## 0. 概览
6:## 1. Shell 脚本
brucelee@invo1ution:~/Interest/Missing-Semester$ rg Denny -A 1 // 查找所有含有 "Denny" 的文本,并输出上下一行
1-Shell/shell_notes.md
14:如果要传参数的话,我们可以使用单引号/双引号将其包括,也可以使用转移符号进行处理。比方说我要创建一个叫 "Denny Qi" 的文件夹,就可以写:
15-
--
17:invo1lution: mkdir Denny\ Qi
18-```
brucelee@invo1ution:~/Interest/Missing-Semester$ rg --stats qweryy // 打印匹配 "qweryy" 的统计信息
0 matches
0 matched lines
0 files contained matches
4 files searched
0 bytes printed
11241 bytes searched
0.000024 seconds spent searching
0.005344 seconds

2.3 查找 shell 命令

经典 history | grep find。这在编译器反复配 ravel 环境的时候帮了大忙。

同时,你也可以使用 Ctrl + R 来回溯,并输入字串进行匹配。

你可以修改 shell history 的行为,例如,如果在命令的开头加上一个空格,它就不会被加进 shell 记录中。当你输入包含密码或是其他敏感信息的命令时会用到这一特性。 为此你需要在 .bashrc 中添加 HISTCONTROL=ignorespace 或者向 .zshrc 添加 setopt HIST_IGNORE_SPACE。 如果你不小心忘了在前面加空格,可以通过编辑 .bash_history 或 .zhistory 来手动地从历史记录中移除那一项。

2.4 文件夹导航

fasd 工具可以帮助我们根据日常习惯来访问经常访问的目录。具体细节可以查看该仓库的 README.md

4. 参考资料

  1. MIT Missing-Semester Lec02
  2. ACM Class Wiki, shell