名校课程推荐 | MIT《CS 实用工具课程》-Shell和Scripting

Shell 和 Scripting

shell 是一个高效的文本计算机接口。

shell 提示符:打开一个终端,你会看到shell提示符,它会让你运行程序和命令;常见的shell提示符有:

  • cd 更改目录
  • ls 列出文件和目录
  • mvcp 移动及复制文件

但shell可以做的不止于此;你可以调用计算机上的任何程序以及命令行工具,它几乎可以执行你想要的任何操作。它们通常比图形化工具更高效。这门课我们会讲到很多。

Shell提供了一种交互式的编程语言-脚本(scripting)。shell有多种类型:

  • 你们可能用过sh或者bash
  • 具有C语言风格的csh
  • 或者“更好的“shell:fishzshksh

这堂课我们将重点关注较为普遍的shbash,当然你们可以随自己喜欢用哪个,我个人就喜欢用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)
    • 输入参数15运行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
    • 也可以联系elseelif
    • 同样,没有花括号,所以用的是then+fi
  • test是另一个提供各种检查和比较的程序,如果为真($?),则以0退出
    • man COMMAND是你的朋友:man test
    • 也可以用[+][ -d $f ]调用
      • 看一下man testwhich "["

但是等等!这是错误的!如果一个文件叫做“我的文档(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的意思是运行ab,将a的所有输出结果输入给b,打印b的输出结果

所有启动的程序(”进程“)都有三个”流“:

  • STDIN: 程序读取的输入来自这里
  • STDOUT: 程序打印其中的内容
  • STDERR: 程序可以选择使用的第二个输出
  • 默认情况下,STDIN 是你的键盘,STDOUT 和 STDERR 是你的终端。但你可以改变这个情况。
  • a | b 使得aSTDOUT 变成b的 STDIN 
  • 同时:
    • a > fooaSTDOUT 输出到该文件)
    • a 2> fooaSTDERR 输出到 该文件)
    • a < fooaSTDIN 从 该文件读取)
    • 提示: 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 -hman CMD会呈现程序解析的flag列表。Flag短式通常可以组合使用,运行rm -r -f就相当于运行rm -rfrm-fr,一些常见的flag已经形成标准,你会在很多应用中看到它们:

  • -a 一般指所有文件(也包括以.开头的文件)
  • -f 通常指强制执行,比如rm -f
  • -h 显示大多数命令的“帮助“
  • -v 通常提供详细输出
  • -V 通常打印命令的版本

此外,双破折号--在内置命令以及许多其他命令中使用,表示命令选项的结束,之后只接受位置参数。因此,如果你有一个名为-v的文件,并想使用grep grep pattern-- -v将工作,而grep pattern -v不起作用。实际上,创建此类文件的一种方法是执行touch-- -v

练习

  1. 如果你是shell新手,你可能想要阅读关于它的综合指南,如BashGuide。如果你想了解的更深入,The Linux Command Line是很好的资源。
  1. PATH, which, type

    我们简要讨论了PATH环境变量用于查找命令的路径。我们进一步探讨一下

- 运行`echo $PATH (或echo $PATH | tr -s ':' '\n'`进行整齐打印)并检查其内容,哪些位置被列出?
- 在用户PATH中`which`命令定位一个程序。尝试对常见的命令如`echo`、`ls`或`mv`运行`which`。注意,which有一点限制因为它不理解shell别名。对于相同的命令,尝试运行`type`和`command -v`,输出会有什么差异?
- 运行`PATH=`并再次尝试运行之前的命令,有些会工作,有些不会,是什么原因?
  1. 特殊变量
- 变量`~`、 `.` 、 `..` 的展开式是什么?
- 变量`$?`是什么功能?
- 变量`$_` 是什么功能?
- 变量`!!`展开式是?`!!*`,`!l`呢
- 查找这些选项的文档并熟悉这些文档
  1. Xargs
有时管道(piping)并不十分有效,因为通过管道导入的命令不会有换行符分隔格式。例如,`file`命令告诉你文件的属性。

尝试运行`ls | file`和`ls | xargs file`,`xargs`有什么用?
  1. 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`使用什么环境变量来决定运行什么程序?
  1. 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]"`
  1. 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`,会执行什么?
  1. 键盘快捷键
和我们经常使用的任何应用程序一样,熟悉其键盘快捷键很有用。输入下面这些命令,并尝试了解它们的功能,以及在什么情况下会更方便知道这些功能。其中有些功能可能在网上搜索可能会更直接方便。(记住`^X`是用`Ctrl+X`表示)


- `^A`, `^E`
- `^R`
- `^L`
- `^C`, `^\` 还有 `^D`
- `^U` 以及 `^Y`
客服