课程为 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
,同时指定参数 hello
。echo
程序将该参数打印出来。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
命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。
用
touch
在missing
文件夹中新建一个叫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 脚本的开头第一行的 shebang
。 shebang
展示的就是运行这个脚本的程序所在路径。
但是并不是所有的机器的 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 工具
查找文件
可使用 find
, fd
, locate
等工具。
查找代码
可使用 grep
, ack
, ag
, rg
等命令。
查找 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函数
marco
和polo
执行下面的操作。 每当你执行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
find
与 GNU coreutils 中的是不一样的。你可以为find
添加-print0
选项,并为xargs
添加-0
选项。作为 Mac 用户,您需要注意 mac 系统自带的命令行工具和 GNU 中对应的工具是有区别的;如果你想使用 GNU 版本的工具,也可以使用 brew 来安装。
鸽一鸽。
(进阶)编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?
鸽一鸽。