The Missing Semester of Your CS Education 课程笔记


课程为 MIT 推出的课程: The Missing Semester of Your CS Education 。

双语字幕课程

中文课程讲义

笔记内容多来自与课程视频与讲义。


第一讲 课程概览与 shell

使用环境: Linux 虚拟机终端。

shell 是什么

几乎所有容易接触到的平台都支持某种形式的 shell ,有些甚至还提供了多种 shell 供选择。虽然它们之间有些细节上的差异,但是其核心功能都是一样的:它允许你执行程序,输入并获取某种半结构化的输出。

使用 shell

打开终端,看到一个提示符。

hoshiuz@HoshiuZ:~$ 

这是 shell 最主要的文本接口,显示出我的主机名为 HoshiuZ ,当前的工作目录为 ~$ 符号表示当前的身份不是 root 用户。在这个提示符中,可以输入命令,命令最终会被 shell 解析。最简单的命令是执行一个程序:

hoshiuz@HoshiuZ:~$ date
Mon Aug 21 02:51:25 AM CDT 2023
hoshiuz@HoshiuZ:~$ 

这里执行了 date 这个程序,容易发现其打印出了目前的日期和时间,然后 shell 等待输入其他命令。

可以在执行命令的同时向程序传递参数:

hoshiuz@HoshiuZ:~$ echo hello
hello

上例中,让 shell 执行 echo ,同时指定参数 helloecho 程序将该参数打印出来。shell 基于空格分割命令并进行解析,然后执行第一个单词代表的程序,并将后续的单词作为程序可以访问的参数。如果希望传递的参数中包含空格(例如 hello world ),则可以用引号包裹起来或者是使用转移符号 \ 进行处理:

hoshiuz@HoshiuZ:~$ echo "hello world"
hello world
hoshiuz@HoshiuZ:~$ echo 'hello world'
hello world
hoshiuz@HoshiuZ:~$ echo hello\ world
hello world

当在 shell 中执行命令时,实际上是在执行一段 shell 可以解释执行的简短代码。如果要求 shell 执行某个命令,但是该指令并不是 shell 所了解的编程关键字,那么它会去咨询环境变量 $PATH ,它会列出当 shell 接到某条指令时,进行程序搜索的路径:

hoshiuz@HoshiuZ:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin
hoshiuz@HoshiuZ:~$ which echo
/usr/bin/echo
hoshiuz@HoshiuZ:~$ /bin/echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin

当执行 echo 命令时, shell 了解到需要执行 echo 这个程序,随后它便会在 $PATH 中搜索由 : 所分割的一系列目录,基于名字搜索该程序,当知道该程序时便执行。确定某个程序名代表的是哪个具体的程序,可以使用 which 程序。也可以绕过 $PATH ,通过直接指定需要执行的程序的路径来执行该程序。

在 shell 中导航

shell 中的路径是一组被分割的目录,在 Linux 和 macOS 上使用 / 分割, 而在 Windows 上是 \ 。路径 / 代表的是系统的根目录,所有的文件夹都包括在这个路径之下,在 Windows 上每个盘都有一个根目录。在我使用的 Linux 文件系统中,如果某个路径以 / 开头,那么它是一个绝对路径,其他的都是相对路径。相对路径是指相对于当前工作目录的路径,当前工作目录可以使用 pwd 命令来获取 ( print working directory ) 。此外,切换目录需要使用 cd 命令。在路径中, . 表示的是当前目录,而 .. 表示上级目录:

hoshiuz@HoshiuZ:~$ pwd
/home/hoshiuz
hoshiuz@HoshiuZ:~$ cd /home
hoshiuz@HoshiuZ:/home$ pwd
/home
hoshiuz@HoshiuZ:/home$ cd ..
hoshiuz@HoshiuZ:/$ pwd
/
hoshiuz@HoshiuZ:/$ cd ./home
hoshiuz@HoshiuZ:/home$ pwd
/home
hoshiuz@HoshiuZ:/home$ cd hoshiuz
hoshiuz@HoshiuZ:~$ pwd
/home/hoshiuz
hoshiuz@HoshiuZ:~$ ../../bin/echo hello
hello

shell 会实时显示当时的路径信息。

一般来说,当运行一个程序时,如果没有指定路径,则该程序会在当前目录下执行。

为了查看指定目录下包含哪些文件,使用 ls 命令:

hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       Videos
Documents  Music      Public    Templates  workarea
hoshiuz@HoshiuZ:~$ cd ..
hoshiuz@HoshiuZ:/home$ ls
hoshiuz
hoshiuz@HoshiuZ:/home$ cd ..
hoshiuz@HoshiuZ:/$ ls
bin    dev   lib    libx32      mnt   root  snap      sys  var
boot   etc   lib32  lost+found  opt   run   srv       tmp
cdrom  home  lib64  media       proc  sbin  swapfile  usr

除非利用第一个参数来指定目录,否则 ls 会打印当前目录下的文件。大多数的命令接受标记和选项(带有值的标记),它们以 - 开头,并可以改变程序的行为。通常,在执行程序时使用 -h--help 标记可以打印帮助信息,以便了解有哪些可用的标记或选项。

hoshiuz@HoshiuZ:~$ ls -l /home
总计 4
drwxr-x--- 16 hoshiuz hoshiuz 4096 Mar  9 20:05 hoshiuz

-l 参数可以更加详细地列出目录下文件或文件夹的信息。最后一行中,第一个字符 d 表示 hoshiuz 是一个目录。然后接下来的九个字符,每三个字符构成一组( rwx ),分别代表了文件所有者、用户组以及其他人所具有的权限,其中 - 表示该用户不具备相应的权限。可修改为 w ,可执行为 x ,可读为 r 。

其他一些比较有用的命令:

mv 命令 ( move file ) ,用来为文件或目录改名、或将文件或目录移入其他位置。

将 test.txt 重命名为 TEST.txt :

hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       TEST.txt  workarea
Documents  Music      Public    Templates  Videos
hoshiuz@HoshiuZ:~$ mv TEST.txt test.txt
hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       test.txt  workarea
Documents  Music      Public    Templates  Videos

将 test.txt 移入目录 Desktop 中:

hoshiuz@HoshiuZ:~$ mv test.txt Desktop/
hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       Videos
Documents  Music      Public    Templates  workarea
hoshiuz@HoshiuZ:~$ cd Desktop
hoshiuz@HoshiuZ:~/Desktop$ ls
test.txt

cp 命令 ( copy file ) ,主要用于复制文件或目录。

将 test.txt 复制到 Desktop 的父目录中 :

hoshiuz@HoshiuZ:~/Desktop$ cp test.txt ../
hoshiuz@HoshiuZ:~/Desktop$ ls
test.txt
hoshiuz@HoshiuZ:~/Desktop$ cd ..
hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       test.txt  workarea
Documents  Music      Public    Templates  Videos
hoshiuz@HoshiuZ:~$ 

将 test.txt 复制到 Desktop 的父目录中并且名为 TEST.txt :

hoshiuz@HoshiuZ:~/Desktop$ cp test.txt ../TEST.txt
hoshiuz@HoshiuZ:~/Desktop$ cd ..
hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       test.txt  Videos
Documents  Music      Public    Templates  TEST.txt  workarea

rm 命令 ( remove ),用于删除一个文件或者目录。

删除当前目录中的 test.txt 与 TEST.txt 文件:

hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       test.txt  Videos
Documents  Music      Public    Templates  TEST.txt  workarea
hoshiuz@HoshiuZ:~$ rm test.txt
hoshiuz@HoshiuZ:~$ rm TEST.txt
hoshiuz@HoshiuZ:~$ ls
Desktop    Downloads  Pictures  snap       Videos
Documents  Music      Public    Templates  workarea

删除目录需要加 -r 参数:

hoshiuz@HoshiuZ:~/Desktop$ ls
test
hoshiuz@HoshiuZ:~/Desktop$ rm test
rm: 无法删除 'test': 是一个目录
hoshiuz@HoshiuZ:~/Desktop$ rm -r test
hoshiuz@HoshiuZ:~/Desktop$ ls

类似的,有 rmdir 命令可以删除目录,但是其只可以删除空目录,这是一种安全机制。

mkdir 命令 ( make directory ) ,用于创建目录。

创建目录 my code

hoshiuz@HoshiuZ:~$ mkdir "my code"
hoshiuz@HoshiuZ:~$ ls
 Desktop     Downloads  'my code'   Public   Templates   workarea
 Documents   Music       Pictures   snap     Videos

man 命令,后跟其他命令名称,可查询手册获取后跟命令的用法。

快捷键 ctrl+L 可以清除终端并回到顶部。

在程序间创建连接

在 shell 中,程序有两个主要的“流”:它们的输入流和输出流。当程序尝试读取信息时,它们会从输入流中进行读取,当程序打印信息时,它们会将信息输出到输出流中。通常,一个程序的输入输出流都是终端,键盘作为输入,显示器作为输出。但是,也可以重定向这些流。

最简单的重定向是 < file>file 。这两个命令可以将程序的输入输出流分别重定向到文件:

cat 命令 ( concatenate ) 是一个打印文件内容的命令。

hoshiuz@HoshiuZ:~$ echo hello > hello.txt
hoshiuz@HoshiuZ:~$ cat hello.txt
hello

该例中,将内容 hello 输出到了 hello.txt 中,输出流重定向, cat hello.txt ,发现文件内容为 hello

hoshiuz@HoshiuZ:~$ cat < hello.txt
hello

该例中,输入流重定向为 hello.txt ,输出流没有重定向,所以输出在终端上为 hello

hoshiuz@HoshiuZ:~$ cat < hello.txt > hello2.txt
hoshiuz@HoshiuZ:~$ cat hello2.txt
hello

该例中,输入流中定向为 hello.txt ,输出流重定向为 hello2.txt ,所以 hello.txt 的内容被打印到 hello2.txt 中。

>> 表示追加而不是覆盖。

hello2.txt 中追加 hellow.txt 的内容:

hoshiuz@HoshiuZ:~$ cat < hello.txt >> hello2.txt
hoshiuz@HoshiuZ:~$ cat hello2.txt
hello
hello

管道( pipes ) 运算符 | 的作用是将左边程序的输出作为右边程序的输入。

获取 ls -l / 输出的最后一行:

hoshiuz@HoshiuZ:~$ ls -l / | tail -n1
drwxr-xr-x  14 root root       4096 Feb 22 22:02 var

ls -l 的输出为一系列文件信息,作为右边 tail -n1 的输入,tail 的输出未重定向,所以显示在终端上。当然也可以重定向其输出到一个文件中:

hoshiuz@HoshiuZ:~$ ls -l / | tail -n1 > ls.txt
hoshiuz@HoshiuZ:~$ cat ls.txt
drwxr-xr-x  14 root root       4096 Feb 22 22:02 var

一个功能全面又强大的工具

对于大多数的类 Unix 系统,有一类用户是非常特殊的——根用户( root user ) 。发现,在上面的输出结果中,根用户几乎不受任何限制,他可以创建、读取、更新和删除系统中的任何文件。通常我们并不会以根用户的身份直接登录系统,因为这样可能会因为某些错误的操作而破坏系统。取而代之的是我们会在需要的时候使用 sudo 命令。顾名思义,它的作用是可以以 su ( super user ) 的身份执行一些操作。当遇到拒绝访问 ( permission denied )的错误时,通常是因为此时必须是根用户才能操作。

由于我的虚拟机的目录 /sys/class/backlight 内啥也没有,所以课中的例子不再尝试。

课后练习

本课程需要使用类Unix shell,例如 Bash 或 ZSH。如果您在 Linux 或者 MacOS 上面完成本课程的练习,则不需要做任何特殊的操作。如果您使用的是 Windows,则您不应该使用 cmd 或是 Powershell;您可以使用 Windows Subsystem for Linux 或者是 Linux 虚拟机。使用echo $SHELL命令可以查看您的 shell 是否满足要求。如果打印结果为/bin/bash/usr/bin/zsh则是可以的。

我是在 Windows 下使用 Linux 虚拟机。

使用 echo $SHELL 命令:

hoshiuz@HoshiuZ:~$ echo $SHELL
/bin/bash

打印结果为 /bin/bash ,可行。

/tmp 下新建一个名为 missing 的文件夹。

hoshiuz@HoshiuZ:~$ cd tmp
hoshiuz@HoshiuZ:~/tmp$ mkdir missing
hoshiuz@HoshiuZ:~/tmp$ ls
missing

man 查看程序 touch 的使用手册。

在终端中输入 man touch 即可查看 touch 的使用手册。

了解到 touch 命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。

touchmissing 文件夹中新建一个叫 semester 的文件。

hoshiuz@HoshiuZ:~/tmp$ cd missing
hoshiuz@HoshiuZ:~/tmp/missing$ touch semester
hoshiuz@HoshiuZ:~/tmp/missing$ ls
semester

将以下内容一行一行地写入 semester 文件:

#!/bin/sh
curl --head --silent https://missing.csail.mit.edu

第一行可能有点棘手, # 在Bash中表示注释,而 ! 即使被双引号(")包裹也具有特殊的含义。 单引号(')则不一样,此处利用这一点解决输入问题。更多信息请参考 Bash quoting 手册

一行一行写入,考察 >>> 的使用。

hoshiuz@HoshiuZ:~/tmp/missing$ echo '#!/bin/sh' > semester
hoshiuz@HoshiuZ:~/tmp/missing$ echo 'curl --head --silent https://missing.csail.mit.edu' >> semester
hoshiuz@HoshiuZ:~/tmp/missing$ cat semester
#!/bin/sh
curl --head --silent https://missing.csail.mit.edu

尝试执行这个文件。例如,将该脚本的路径(./semester)输入到您的shell中并回车。如果程序无法执行,请使用 ls 命令来获取信息并理解其不能执行的原因。

hoshiuz@HoshiuZ:~/tmp/missing$ ./semester
bash: ./semester: 权限不够

如上,发现权限不够。

hoshiuz@HoshiuZ:~/tmp/missing$ ls -l
总计 4
-rw-rw-r-- 1 hoshiuz hoshiuz 61 Aug 23 22:39 semester

发现没有可执行权限 ( x ) ,因而无法执行该文件。

查看 chmod 的手册(例如,使用 man chmod 命令)

在终端中输入 man chmod 即可查看 chmod 的手册。

了解到 chmod ( change mode ) 命令是控制用户对文件的权限的命令。

使用 chmod 命令改变权限,使 ./semester 能够成功执行,不要使用 sh semester 来执行该程序。您的 shell 是如何知晓这个文件需要使用 sh 来解析呢?更多信息请参考:shebang)

上面已经发现了文件 semester 没有可执行权限,所以直接将其权限设置成 777

hoshiuz@HoshiuZ:~/tmp/missing$ chmod 777 semester
hoshiuz@HoshiuZ:~/tmp/missing$ ls -l
总计 4
-rwxrwxrwx 1 hoshiuz hoshiuz 61 Aug 23 22:39 semester

如此便可执行该文件。

但是发现运行结果仍有问题:

hoshiuz@HoshiuZ:~/tmp/missing$ ./semester
./semester: 2: curl: not found

后发现是 curl 命令未安装,输入 sudo snap install curl 后即可使用该命令。

hoshiuz@HoshiuZ:~/tmp/missing$ ./semester
HTTP/2 200 
server: GitHub.com
content-type: text/html; charset=utf-8
x-origin-cache: HIT
last-modified: Fri, 25 Aug 2023 21:51:53 GMT
access-control-allow-origin: *
etag: "64e92279-1fce"
expires: Sat, 26 Aug 2023 08:30:44 GMT
cache-control: max-age=600
x-proxy-cache: MISS
x-github-request-id: BB72:4ADD:45640D:48F2E3:64E9B5DC
accept-ranges: bytes
date: Sat, 26 Aug 2023 16:17:02 GMT
via: 1.1 varnish
age: 0
x-served-by: cache-nrt-rjtf7700056-NRT
x-cache: HIT
x-cache-hits: 1
x-timer: S1693066622.842482,VS0,VE241
vary: Accept-Encoding
x-fastly-request-id: 0f7c30a3bbadbec42fb4ec1216f411b51f12fb06
content-length: 8142

使用 |> ,将 semester 文件输出的最后更改日期信息,写入主目录下的 last-modified.txt 的文件中

最后更改日期信息即为 last-modified 那一行。

如何查找到该行并将该行输出到文本中?

grep 命令 ( global regular expression ) 命令用于查找文件里符合条件的字符串或正则表达式。

查找到相应的字符串后,会将匹配的行给打印出来。

hoshiuz@HoshiuZ:~/tmp/missing$ ./semester | grep last-modified
last-modified: Fri, 25 Aug 2023 21:51:53 GMT

于是再结合 > ,便可以将其写入文件中。

hoshiuz@HoshiuZ:~/tmp/missing$ ./semester | grep last-modified > last-modified.txt
hoshiuz@HoshiuZ:~/tmp/missing$ cat last-modified.txt
last-modified: Fri, 25 Aug 2023 21:51:53 GMT

第二讲 Shell 工具和脚本

Shell 脚本

在 bash 中为变量赋值的语法是 foo=bar ,访问变量中存储的数值,其语法为 $foo 。需要注意的是, foo = bar (使用空格隔开) 是不能正确工作的,因为解释器会调用程序 foo 并将 =bar 作为参数。在 shell 脚本中,使用空格会起到分割参数的作用,有时候会造成混淆 :

hoshiuz@HoshiuZ:~$ foo=bar
hoshiuz@HoshiuZ:~$ echo $foo
bar
hoshiuz@HoshiuZ:~$ foo = bar
找不到命令 “foo”

Bash 中的字符串通过 '" 分隔符来定义,但是它们的含义并不相同。以 ' 定义的字符串为原义字符串,其中的变量不会被转义,而 " 定义的字符串会将变量值进行替换 :

hoshiuz@HoshiuZ:~$ echo '$foo'
$foo
hoshiuz@HoshiuZ:~$ echo "$foo"
bar

Bash 也可以进行流程控制,例如 for, case, while, if

Bash 也支持函数,它可以接受参数并基于参数进行操作。

在文件 mcd.sh 中写入

mcd () {
        mkdir -p "$1"
        cd "$1"
}

参数 -p :确保目录名称存在,不存在就建一个。

该函数功能为创建一个目录并使用 cd 进入该目录。

这里的 $1 是脚本的第一个参数。bash 使用了很多特殊的变量来表示参数、错误代码和相关变量。以下列举其中的一些变量,更完整的列表参见 这里

  • $0 脚本名。
  • $1$9 脚本的参数。 $1 是第一个参数,以此类推。
  • $@ 所有参数。
  • $? 前一个命令的返回值。
  • $$$$ 当前脚本的进程识别码。
  • !! 完整的上一条命令,包括参数。常见应用:当因权限不足执行命令失败时,可以使用 sudo !! 再试一次。
  • $_ 上一条命令的最后一个参数。若使用的是交互式 shell ,则可以通过按下 Ese 之后键入 . 来获取这个值。
  • $# 传给脚本的参数个数。

输入 source mcd.sh ,这会在 Shell 中加载并执行这个脚本,此后 Shell 中就已经定义了 mcd 函数,可以直接使用。

hoshiuz@HoshiuZ:~$ source mcd.sh
hoshiuz@HoshiuZ:~$ mcd test
hoshiuz@HoshiuZ:~/test$

如上,发现已经创建了目录 test 并且进入了该目录。

命令通常使用 STDOUT 来返回输出值,使用 STDERR 来返回错误及错误码,便于脚本以更加友好的方式来报告错误。返回码或退出状态是脚本/命令之间交流执行状态的方式。返回值 0 表示正常执行,其他所有非 0 的返回值都表示有错误发生。

hoshiuz@HoshiuZ:~$ echo "hello"
hello
hoshiuz@HoshiuZ:~$ echo $?
0

如上,因为上一条命令 echo "hello" 没有出错,所以返回值为 0 ,表示正常执行。

hoshiuz@HoshiuZ:~$ grep foobar mcd.sh
hoshiuz@HoshiuZ:~$ echo $?
1

mcd.sh 中没有字符串 foobar ,所以 grep 不会输出任何内容,但是会返回一个 1 的错误代码。

hoshiuz@HoshiuZ:~$ true
hoshiuz@HoshiuZ:~$ echo $?
0
hoshiuz@HoshiuZ:~$ false
hoshiuz@HoshiuZ:~$ echo $?
1

命令 true 的错误代码始终为 0 ,命令 false 的错误代码始终为 1 。

退出码可以搭配 &&|| 使用(与操作符,或操作符),用来进行条件判断,决定是否执行其他程序。它们都属于短路运算符( short-circuiting ) ,同一行的多个命令可以用 ; 分隔。

hoshiuz@HoshiuZ:~$ false || echo A
A
hoshiuz@HoshiuZ:~$ true || echo B
hoshiuz@HoshiuZ:~$ true && echo C
C
hoshiuz@HoshiuZ:~$ false && echo D
hoshiuz@HoshiuZ:~$ true ; echo E
E
hoshiuz@HoshiuZ:~$ false ; echo F
F

如上。“短路运算符”, 所以当已经能确定当前表达式的返回值时,就不再向下执行。

如何将命令的输出存储在一个变量里?这可以通过 命令替换( command substitution ) 来实现。

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

hoshiuz@HoshiuZ:~$ echo "We are now in $(pwd)"
We are now in /home/hoshiuz

双引号,所以可以替换。

据此,可以将命令的输出存储在变量里。

hoshiuz@HoshiuZ:~$ directory=$(pwd)
hoshiuz@HoshiuZ:~$ echo $directory
/home/hoshiuz

如上,命令 pwd 的输出就被存储在变量 directory 中。

进制替换( process substitution ) ,<(CMD) 会执行 CMD 并将结果输出到一个临时文件中,并将 <(CMD) 替换成临时文件名。这在我们希望返回值通过文件而不是 STDIN 传递时很有用,如下例:

hoshiuz@HoshiuZ:~$ echo hello > test 
hoshiuz@HoshiuZ:~$ echo Hello > TEST
hoshiuz@HoshiuZ:~$ diff <(cat test) <(cat TEST)
1c1
< hello
---
> Hello

一个脚本的例子( example.sh ) :

echo "Starting program at $(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 
        # -ne ( not equal )
                echo "File $file does not have any foobar, adding one"
                echo "# foobar" >> "$file"
                # 如果没有找到 foobar ,则将其作为注释加到文件中
        fi
done

PID 表示进程标识符 ( Process Identifier ),每个运行中的进程都有一个唯一的 PID ,用于在操作系统中标识该进程。

注意要向脚本添加可执行权限后才可以执行该脚本。

运行示例:

hoshiuz@HoshiuZ:~/course$ ./example.sh test Test TEST
Starting program at Wed Aug 30 11:28:15 AM CDT 2023
Running program ./example.sh with 3 arguments with pid 50691
File test does not have any foobar, adding one

打开 test ,发现其末尾添加了 # foobar

有趣的是, example.sh 脚本本身也可以作为参数。

在 bash 中进行比较时,尽量使用双方括号 [[]] 而不是单方括号 [] ,这样会降低犯错的几率,尽管这样并不能兼容 sh

当执行脚本时,经常需要提供形式类似的参数, bash 可以轻松地实现这一操作,它可以基于文件扩展名展开表达式,这一技术被称为 shell 的通配( globbing ) 。

  • 通配符:当想要利用通配符进行匹配时,可以分别使用 ?* 来匹配一个或任意个字符。

    hoshiuz@HoshiuZ:~/course$ ls
    bar  e.sh  example.sh  foo  foo1  foo10  foo2
    hoshiuz@HoshiuZ:~/course$ rm foo?
    hoshiuz@HoshiuZ:~/course$ ls
    bar  e.sh  example.sh  foo  foo10

    ? 通配符匹配一个字符,所以 foo10 没有被删除。

    hoshiuz@HoshiuZ:~/course$ ls
    bar  e.sh  example.sh  foo  foo1  foo10  foo2
    hoshiuz@HoshiuZ:~/course$ rm foo*
    hoshiuz@HoshiuZ:~/course$ ls
    bar  e.sh  example.sh

    * 通配符匹配任意个字符,所以 foo foo1 foo10 foo2 全被删除。

  • 花括号 {} :当有一系列指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或者转换文件时非常方便。

    例如 convert image.{png,jpg} 会展开为 convert image.png image.jpg

    对多个大括号组成成的目录进行操作会对其笛卡尔积进行操作。

终端里也可以调用其他语言写的脚本。比如,这是一个 Python 脚本 script.py

#!/usr/bin/python3
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

在终端中,可以用 python3 命令来使用该脚本:

hoshiuz@HoshiuZ:~/course$ python3 script.py a b c
c
b
a

同时也可以直接运行该脚本:

hoshiuz@HoshiuZ:~/course$ ./script.py a b c
c
b
a

这是因为该 Python 脚本的开头第一行的 shebangshebang 展示的就是运行这个脚本的程序所在路径。

但是并不是所有的机器的 python3 的位置都是一样的。所以可以使用 env 命令来提高脚本的可移植性,适用于大部分机器。env 会利用 PATH 环境变量来进行定位。

#!/usr/bin/env python3
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

编写 bash 脚本有时候会很别扭和反直觉,像 shellcheck 这样的工具可以帮助你定位 sh/bash 脚本中的错误。

hoshiuz@HoshiuZ:~/course$ shellcheck example.sh

In example.sh line 1:
echo "Starting program at $(date)"
^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.


In example.sh line 5:
	if [[ "$?" -ne 0 ]]; then
              ^--^ SC2181 (style): Check exit code directly with e.g. 'if ! mycmd;', not indirectly with $?.

For more information:
  https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y...
  https://www.shellcheck.net/wiki/SC2181 -- Check exit code directly with e.g...

如上,使用工具 shellcheck ,其指出了脚本 example.sh 中的一些问题。

shell 函数和脚本有如下一些不同点:

  • 函数只能与 shell 使用相同的语言,脚本可以使用任意语言。因此在脚本中包含 shebang 是很重要的。
  • 函数尽在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本稍快一些,但是每次修改函数定义,都要重新加载一次。
  • 函数会在当前的 shell 环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行修改,比如改变当前工作目录,脚本则不行。脚本需要使用 export 将环境变量导出,并将值传递给环境变量。
  • 与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell 脚本中往往也会包含它们自己的函数定义。

shell 工具

查找文件

可使用 findfdlocate 等工具。

查找代码

可使用 grepackagrg 等命令。

查找 shell 命令

有时候可能想要找到之前输入过的某条命令。按向上的方向键会显示使用过的上一条命令,继续按上键则会遍历整个历史记录。

history 命令允许以程序员的方式来访问 shell 中输入的历史命令。这个命令会在标准输出中打印 shell 中的命令。如果需要搜索历史记录,则可以利用管道将输出结果传递给 grep 进行模式搜索。例如 history | grep find 会打印历史记录中包含 find 子串的命令。

还可以使用快捷键 ctrl+R 对命令历史记录进行回溯搜索。按下后可以输入子串来进行匹配,查找历史命令行。反复按下就会在所有搜索结果中循环。

课后练习

阅读 man ls ,然后使用ls 命令进行如下操作:

  • 所有文件(包括隐藏文件)
  • 文件打印以人类可以理解的格式输出 (例如,使用454M 而不是 454279954)
  • 文件以最近访问顺序排序
  • 以彩色文本显示输出结果

    典型输出如下:

    -rw-r--r--   1 user group 1.1M Jan 14 09:53 baz
    drwxr-xr-x   5 user group  160 Jan 14 09:53 .
    -rw-r--r--   1 user group  514 Jan 14 06:42 bar
    -rw-r--r--   1 user group 106M Jan 13 12:12 foo
    drwx------+ 47 user group 1.5K Jan 12 18:08 ..

分别为 :

  • ls -a
  • ls -h
  • ls -t
  • ls --color=auto

编写两个bash函数 marcopolo 执行下面的操作。 每当你执行 marco 时,当前的工作目录应当以某种形式保存,当执行 polo 时,无论现在处在什么目录下,都应当 cd 回到当时执行 marco 的目录。 为了方便debug,你可以把代码写在单独的文件 marco.sh 中,并通过 source marco.sh命令,(重新)加载函数。

编写两个函数如下:

marco() {
        echo "$(pwd)" > $HOME/marco_history.log
        echo "save pwd $(pwd)"
}
polo() {
        cd "$(cat "$HOME/marco_history.log")"
}

或者可以使用 export 命令,将执行 marco 函数的当前目录存在一个变量内,然后 polo 直接 cd 这个变量:

marco() {
        export MARCO="$(pwd)"
}
polo() {
        cd "$MARCO"
}

假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段bash脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。 加分项:报告脚本在失败前共运行了多少次。

#!/usr/bin/env bash
  
n=$(( RANDOM % 100 ))
  
if [[ n -eq 42 ]]; then
   echo "Something went wrong"
   >&2 echo "The error was using magic numbers"
   exit 1
fi
  
echo "Everything went according to plan"

鸽一鸽。

本节课我们讲解的 find 命令中的 -exec 参数非常强大,它可以对我们查找的文件进行操作。但是,如果我们要对所有文件进行操作呢?例如创建一个zip压缩文件?我们已经知道,命令行可以从参数或标准输入接受输入。在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如tar 则需要从参数接受输入。这里我们可以使用xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。

您的任务是编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs的参数-d,译注:MacOS 上的 xargs没有-d查看这个issue

如果您使用的是 MacOS,请注意默认的 BSD findGNU coreutils 中的是不一样的。你可以为find添加-print0选项,并为xargs添加-0选项。作为 Mac 用户,您需要注意 mac 系统自带的命令行工具和 GNU 中对应的工具是有区别的;如果你想使用 GNU 版本的工具,也可以使用 brew 来安装

鸽一鸽。

(进阶)编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?

鸽一鸽。


文章作者: HoshiuZ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 HoshiuZ !
  目录