名校课程推荐 | MIT《CS 实用工具课程》-Shell和Scripting
Shell 和 Scripting
shell 是一个高效的文本计算机接口。
shell 提示符:打开一个终端,你会看到shell提示符,它会让你运行程序和命令;常见的shell提示符有:
cd
更改目录ls
列出文件和目录mv
和cp
移动及复制文件
但shell可以做的不止于此;你可以调用计算机上的任何程序以及命令行工具,它几乎可以执行你想要的任何操作。它们通常比图形化工具更高效。这门课我们会讲到很多。
Shell提供了一种交互式的编程语言-脚本(scripting)。shell有多种类型:
- 你们可能用过
sh
或者bash
- 具有C语言风格的
csh
- 或者“更好的“shell:
fish
、zsh
、ksh
这堂课我们将重点关注较为普遍的sh
和bash
,当然你们可以随自己喜欢用哪个,我个人就喜欢用fish
。
Shell编程是工具箱中非常有用的工具。既可以在提示符下直接编写程序,也可以将程序写入文件。通过# !/bin/sh
+ chmod +x
命令让shell可执行。
使用shell
使用for循环重复执行一条命令:
for i in $(seq 1 5); do echo hello; done
for x in list; do BODY; done
;
终结命令,相当于换行符- 拆分
list
,给x
赋值,然后运行body
- 分割是“空格分割”,我们还会讲到
- shell中没有花括号,所以用
do
+done
$(seq 1 5)
- 输入参数
1
和5
运行seq
程序 - 用该程序的输出替代整个
$()
- 相当于
- 输入参数
for i in 1 2 3 4 5
echo hello
- shell script中的所有内容都是命令
- 在该案例中,运行
echo
命令,将打印参数hello
- 在
$PATH
中搜索所有命令(冒号分隔)
我们有变量:
for f in $(ls); do echo $f; done
将打印当前目录下的每个文件名。也可以使用=
(前后没有空格!)设置变量:
foo=bar
echo $foo
也有很多“特殊”变量:
$1
到$9
:脚本的参数$0
脚本名$#
参数个数$$
当前脚本的进程识别码
只打印目录
for f in $(ls); do if test -d $f; then echo dir $f; fi; done
这里的解析:
if CONDITION; then BODY; fi
CONDITION
是一个命令; 如果返回退出状态为0(成功),则运行BODY
- 也可以联系
else
或elif
- 同样,没有花括号,所以用的是
then
+fi
test
是另一个提供各种检查和比较的程序,如果为真($?
),则以0退出man COMMAND
是你的朋友:man test
- 也可以用
[
+]
:[ -d $f ]
调用- 看一下
man test
和which "["
- 看一下
但是等等!这是错误的!如果一个文件叫做“我的文档(My Documents)”呢?
for f in $(ls)
扩展到for f in My Documents
- 首先在
My
上测试,然后在Documents
测试 - 这不是我们想要的
- shell脚本错误的最大来源
参数分割
Bash使用空格分割参数;但并不总是你想要的那样!
- 需要使用引号来处理参数中的空格
for f in "My Documents"
- 其他地方也是同样的问题,
test -d $f
中如果$f
中有空格,test
将出错 echo
是可以的,因为通过空格拆分+连接,但如果文件名中包含换行符怎么办?那就需要空格!- 引用所有你不希望分割的变量
- 那要如何修复上面的脚本?你认为
for f in "$(ls)"
是什么作用?
答案是通配(globbing)!
- bash知道如何使用模式查找文件
*
匹配任意个字符?
匹配一个字符{a,b,c}
匹配其中任意字符
for f in *
:该目录下所有文件- 通配时,每个匹配文件会成为它自己的参数
- 仍需要确保使用时引用:
test -d "$f"
- 仍需要确保使用时引用:
- 可生成高级模式:
for f in a*
: 当前目录中所有以a
开头的文件for f in foo/*.txt
: 当前目录中所有的.txt
文件for f in foo/*/p??.txt
在当前子目录中所有以p
开头的三个字母的文本文件
回到空格问题:
if [ $foo = "bar" ]; then
– 看到问题了吗?- 如果
$foo
是空的怎么办?[
参数就是=
和bar
… - 可以用
[ x$foo = "xbar" ]
解决这个问题,但是, - 相反,可以用
[[
: 具有特殊解析的Bash内置比较器- 也可以用
&&
代替-a
,||
代替-o
- 也可以用
可组合性
Shell之所以强大,部分原因在于它的可组合性。它可以将多个程序连接在一起,而不是让一个程序做所有事情。
关键字符是 |
(管道)
a | b
的意思是运行a
和b
,将a
的所有输出结果输入给b
,打印b
的输出结果
所有启动的程序(”进程“)都有三个”流“:
STDIN
: 程序读取的输入来自这里STDOUT
: 程序打印其中的内容STDERR
: 程序可以选择使用的第二个输出- 默认情况下,
STDIN
是你的键盘,STDOUT
和STDERR
是你的终端。但你可以改变这个情况。
a | b
使得a
的STDOUT
变成b
的STDIN
- 同时:
a > foo
(a
的STDOUT
输出到该文件)a 2> foo
(a
的STDERR
输出到 该文件)a < foo
(a
的STDIN
从 该文件读取)- 提示:
tail -f
将打印正在写入的文件
- 这为什么有用?你来操作一个程序的输出
ls | grep foo
: 所有包含某个单词的文件ps | grep foo
: 所有包含某个单词的进程journalctl | grep -i intel | tail -n5
: 包含单词Intel(不区分大小写)的最近5个系统日志消息who | sendmail -t me@example.com
将登录用户列表发送到me@example.com
- 形成大量数据处理的基础,我们之后会讨论
Bash还提供了很多其他组合程序的方法。
可以用(a; b) | tac
组合命令:运行a
,然后运行b
,并把他们的所有输出传递给tac
,tac倒序打印其输入。
一个不太为人所知但非常有用的方法是进程替换。b <(a)
将运行a
,为其输出流生成一个临时文件名,并将该文件名传输给b
。例如:
diff <(journalctl -b -1 | head -n20) <(journalctl -b -2 | head -n20)
将显示最后一次启动日志的前20行与它之前20行之间的差异。
工作和进程控制
如果你想在后台运行更长期的东西呢?
- 后缀
&
表示程序在“后台”运行- 它会立刻返回提示符
- 如果你想同时运行两个程序,比如服务器和客户端
server & client
,这会很方便 - 注意,正在运行的程序仍然把你的终端作为
STDOUT
!尝试:server > server.log & client
- 用
jobs
查看所有此类进程- 注意它显示“Running”
- 用
fg %JOB
搬到前台(没有最新的参数) - 如果你想让当前程序进入后台:
^Z
+bg
(这里^Z
是指Ctrl+Z
)^Z
是终止当前进程,让其成为一个任务(job)bg
是在后台运行最新一个任务(就像&
操作一样)
- 后台作业仍然绑定到当前会话,如果登出,则退出。
disown
表示切断连接,或使用nohup
$!
是后台运行的最后一个进程的PID
在计算机上运行的其他东西?
ps
:列出当前运行的进程ps -A
:打印所有用户的进程(也可以用ps ax
)ps
有很多参数:详见man ps
pgrep
: 通过搜索查找进程(比如ps -A | grep
)pgrep -af
:使用参数进行搜索和显示
kill
:通过ID向进程发送信号(通过搜索pkill
+-f
)- 信号告诉进程“执行某操作”
- 最常见的:
SIGKILL
(-9
或-KILL
):告诉它立刻退出,相当于^\
- 同样
SIGTERM
(-15
或-TERM
):告诉它退出,相当于^C
Flags
大多数命令行程序使用flags包解析参数。Flags的短式通常是(-h
),完整表示是(--help
)。通常运行CMD -h
或man CMD
会呈现程序解析的flag列表。Flag短式通常可以组合使用,运行rm -r -f
就相当于运行rm -rf
或rm-fr
,一些常见的flag已经形成标准,你会在很多应用中看到它们:
-a
一般指所有文件(也包括以.开头的文件)-f
通常指强制执行,比如rm -f
-h
显示大多数命令的“帮助“-v
通常提供详细输出-V
通常打印命令的版本
此外,双破折号--
在内置命令以及许多其他命令中使用,表示命令选项的结束,之后只接受位置参数。因此,如果你有一个名为-v
的文件,并想使用grep grep pattern-- -v
将工作,而grep pattern -v
不起作用。实际上,创建此类文件的一种方法是执行touch-- -v
。
练习
- 如果你是shell新手,你可能想要阅读关于它的综合指南,如BashGuide。如果你想了解的更深入,The Linux Command Line是很好的资源。
PATH, which, type
我们简要讨论了
PATH
环境变量用于查找命令的路径。我们进一步探讨一下
- 运行`echo $PATH (或echo $PATH | tr -s ':' '\n'`进行整齐打印)并检查其内容,哪些位置被列出?
- 在用户PATH中`which`命令定位一个程序。尝试对常见的命令如`echo`、`ls`或`mv`运行`which`。注意,which有一点限制因为它不理解shell别名。对于相同的命令,尝试运行`type`和`command -v`,输出会有什么差异?
- 运行`PATH=`并再次尝试运行之前的命令,有些会工作,有些不会,是什么原因?
- 特殊变量
- 变量`~`、 `.` 、 `..` 的展开式是什么?
- 变量`$?`是什么功能?
- 变量`$_` 是什么功能?
- 变量`!!`展开式是?`!!*`,`!l`呢
- 查找这些选项的文档并熟悉这些文档
- Xargs
有时管道(piping)并不十分有效,因为通过管道导入的命令不会有换行符分隔格式。例如,`file`命令告诉你文件的属性。
尝试运行`ls | file`和`ls | xargs file`,`xargs`有什么用?
- Shebang
当你编写脚本的时候,可以通过一个[shebang](https://en.wikipedia.org/wiki/Shebang_(Unix))(#!)行来指定你的shell应该使用什么解释器来解释这个脚本。编写一个名为`hello`的脚本,内容如下:使用`chmod +x hello`使其可执行,然后用`./hello`执行它。然后删除第一行并再次执行?使用第一行的shell如何?
```
#! /usr/bin/python
print("Hello World!")
```
你经常会看到一些程序的shebang类似`#!usr / bin / env bash`。这是一个更易转移的解决方案,有其自身的[优缺点](https://unix.stackexchange.com/questions/29608/why-is-it-better-to-use-usr-bin-env-name-instead-of-path-to-name-as-my)。`env`和`which`有什么不同?`env`使用什么环境变量来决定运行什么程序?
- Pipes, process substitution, subshell
用以下内容创建一个名为`slow_seq.sh`的脚本,执行`chmod +x slow_seq.sh`让它可执行。
```
#! /usr/bin/env bash
for i in $(seq 1 10); do echo $i; sleep 1; done
```
有一种方法管道(和进程替换)与使用子进程执行是有区别的,比如`$()`。执行如下命令,观察其区别:
- `./slow_seq.sh | grep -P "[3-6]"`
- `grep -P "[3-6]" <(./slow_seq.sh)`
- `echo $(./slow_seq.sh) | grep -P "[3-6]"`
- Misc
- 尝试运行`touch {a,b}{a,b}`,然后是`ls`,显示什么?
- 有时你想要保留STDIN,但仍要将其传输到一个文件。尝试运行`echo HELLO | tee hello.txt`
- 尝试运行`cat hello.txt > hello.txt` 你认为会发生什么?实际发生了什么?
- 运行`echo HELLO > hello.txt`,然后运行`echo WORLD >> hello.txt`。 `hello.txt`的内容是什么?`>`和`>>`有什么区别?
- 运行`printf "\e[38;5;81mfoo\e[0m\n"`。输出有什么不同?如果你想了解更多,搜索ANSI颜色转义序列。
- 运行`touch a.txt`,然后运行`^txt^log`,bash会怎么执行?同样地,运行`fc`,会执行什么?
- 键盘快捷键
和我们经常使用的任何应用程序一样,熟悉其键盘快捷键很有用。输入下面这些命令,并尝试了解它们的功能,以及在什么情况下会更方便知道这些功能。其中有些功能可能在网上搜索可能会更直接方便。(记住`^X`是用`Ctrl+X`表示)
- `^A`, `^E`
- `^R`
- `^L`
- `^C`, `^\` 还有 `^D`
- `^U` 以及 `^Y`