、基本命令
-
date 命令
bluuue% date
2026年 06月 28日 星期日 23:52:36 CST
date 命令可以返回当前的时间。在这里,date 就是一个程序。
-
echo 命令
还是熟悉的 hello world 环节。
bluuue% echo hello world
hello world
bluuue% echo hello world
hello world
bluuue% echo "hello world"
hello world
bluuue% echo "Bluuue's world"
Bluuue's world
bluuue% echo hello\ \ \ \ world
hello world
echo 命令就是输出,后面跟上想输出的东西即可。
需要注意的是,在输入 echo hello world 的时候,此时的 hello 和 world 都是被当成参数处理的,而 echo 在输出时恰好也是用一个空格来分割多个参数的,所以显示的就是输入的样子。而如果输入 echo hello world,此时就很明显能看到问题所在。
如果就是想要输出多个空格,就可以直接用双引号表示将其当作字符串传入,或者使用反斜杠对空格等字符转义。
-
pwd 命令
bluuue% pwd
/bin
pwd 命令可以显示当前的工作目录。
-
cd 命令
bluuue% cd /bin
bluuue% pwd
/bin
bluuue% cd /
bluuue% pwd
/
bluuue% cd bin
bluuue% pwd
/bin
bluuue% cd .
bluuue% pwd
/bin
bluuue% cd ...
bluuue% pwd
/
bluuue% cd ...
bluuue% pwd
/
bluuue% cd ~
bluuue% pwd
/home/Bluuue
cd 命令可以切换到别的目录。对于其参数,可以输入绝对路径,比如 /bin,那么就会直接跳转到你输入的这个地方。也可以输入相对目录,即相对当前所在位置的目录,比如如果现在处于根目录 /,就可以直接输入 bin 跳转。
除此之外,. 表示当前目录,... 表示当前目录的父目录,~ 表示打开终端时的默认位置。
bluuue% cd Desktop
bluuue% pwd
/home/Bluuue/Desktop
bluuue% cd ...
bluuue% pwd
/home/Bluuue
bluuue% cd Downloads
bluuue% pwd
/home/Bluuue/Downloads
bluuue% cd .../Desktop
bluuue% pwd
/home/Bluuue/Desktop
此时就有一个操作,如果你想要跳转到当前工作目录的兄弟目录,就可以先输入 ... 回到父亲节点,再输入兄弟目录过去。
bluuue% cd /
bluuue% cd b
bin@ boot/
如果忘记了想去往的地方,可以先输入前几个字符,再按 Tab 键就会弹出当前目录下所有匹配的文件名,从中选择即可。
- 路径变量和 which 命令
bluuue% echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
bluuue% which date
/usr/bin/date
在上述说的这些命令,其本质都是一个一个的程序。这些程序都存在一些特殊的地方,这个地方就可以通过 $PATH 来查看。在调用的时候,系统会遍历这些路径寻找给定的程序,然后运行第一个找到的程序。
which 命令可以用来查看每个命令的具体位置,常态是返回第一个程序,如果想返回所有叫这个名字的命令可以用 which -a 查看。
- ls 命令
luuue% ls
Desktop Documents Downloads go Music Pictures Projects Public Templates Videos
bluuue% ls -l
总计 0
drwxr-xr-x 1 Bluuue Bluuue 2036 6月29日 18:51 Desktop
drwxr-xr-x 1 Bluuue Bluuue 56 6月24日 20:16 Documents
drwxr-xr-x 1 Bluuue Bluuue 0 6月29日 23:55 Downloads
drwxr-xr-x 1 Bluuue Bluuue 6 6月16日 23:01 go
drwxr-xr-x 1 Bluuue Bluuue 20 6月15日 22:22 Music
drwxr-xr-x 1 Bluuue Bluuue 24 6月17日 06:26 Pictures
drwxr-xr-x 1 Bluuue Bluuue 0 6月15日 19:58 Projects
drwxr-xr-x 1 Bluuue Bluuue 0 6月15日 19:58 Public
drwxr-xr-x 1 Bluuue Bluuue 0 6月15日 19:58 Templates
drwxr-xr-x 1 Bluuue Bluuue 24 6月25日 14:29 Videos
bluuue% ls -lh
-rw-r--r-- 1 Bluuue Bluuue 4.2M 6月28日 21:45 Patterson6e_MIPS_Ch01_PPT.pdf
-rw-rw-r-- 1 Bluuue Bluuue 4.2M 6月28日 21:47 Patterson6e_MIPS_Ch02_PPT.pdf
-rw-rw-r-- 1 Bluuue Bluuue 6.6M 6月28日 21:47 Patterson6e_MIPS_Ch03_PPT.pdf
-rw-rw-r-- 1 Bluuue Bluuue 8.0M 6月28日 21:47 Patterson6e_MIPS_Ch04_PPT.pdf
-rw-rw-r-- 1 Bluuue Bluuue 6.0M 6月28日 21:47 Patterson6e_MIPS_Ch05_PPT.pdf
ls 命令可以列出所有当前目录下的文件,可以通过在后面加上 -l 参数来返回每个文件或目录的详细信息。当然,也可以通过在后面加上路径来查看指定路径下的所有文件。
-
cat 命令
bluuue% cat tmp.md
3
31
Z
D
A
C
4
4
4
3
4
cat 命令可以输出一个文件的内容。
-
sort 命令
bluuue% sort tmp.md
3
3
31
4
4
4
4
A
C
D
Z
bluuue% sort -u tmp.md
3
31
4
A
C
D
Z
bluuue% uniq tmp.md
3
31
Z
D
A
C
4
3
4
sort 命令可以按字典序从小到大排序并输出一个文件,如果加上 -u 参数,意思就是对排好序的内容去重。uniq 命令就是不排序,直接去重并输出。
-
head 和 tail 命令
bluuue% head -n3 tmp.md
3
31
Z
bluuue% tail -n3 tmp.md
4
3
4
顾名思义,head 就是输出前若干行的内容,tail 就是输出后若干行的内容。
- grep 命令
bluuue% grep 3 tmp.md
3
31
3
bluuue% grep -r ICPC .
./Algo/note/.obsidian/workspace.json: "Algorithm/2026.6.5/2022ICPC西安区域赛 D.md",
./Algo/note/.obsidian/workspace.json: "Algorithm/6月/2026.6.17/2022ICPC南京区域赛 E.md",
./Algo/note/.obsidian/workspace.json: "Algorithm/5月/2026.5.29/2026ICPC上海区域赛 J.md",
./Algo/note/.obsidian/workspace.json: "Algorithm/5月/2026.5.29/2026ICPC上海区域赛 I.md",
./Algo/note/.obsidian/workspace.json: "Algorithm/6月/2026.6.4/2021ICPC上海区域赛 H.md",
./Algo/note/.obsidian/workspace.json: "Algorithm/4月/2026.4.13/2024ICPC昆明邀请赛 J.md",
grep 命令就是在文件中搜索给定的内容,如果加上参数 -r 就是递归地在给定目录中搜索所有文件。
-
sed 命令
bluuue% sed -i 's/3/5/g' tmp.md
bluuue% cat tmp.md
5
51
Z
D
A
C
4
4
4
5
4
sed 命令可以逐行对文件内的指定内容进行修改。在这里,-i 就是原地修改,而不产生新文件。之后的字符串中,s 表示 substitute 替换,然后将所有的 3 改成 5,g 表示全局替换,而不是只替换每行第一个指定的内容。
-
find 命令
bluuue% find ~/Desktop -type f -name "*.pdf" -mtime -3
/home/Bluuue/Desktop/Patterson6e_MIPS_Ch01_PPT.pdf
/home/Bluuue/Desktop/Patterson6e_MIPS_Ch02_PPT.pdf
/home/Bluuue/Desktop/Patterson6e_MIPS_Ch03_PPT.pdf
/home/Bluuue/Desktop/Patterson6e_MIPS_Ch04_PPT.pdf
/home/Bluuue/Desktop/Patterson6e_MIPS_Ch05_PPT.pdf
bluuue% find ~/Desktop -type f -name "*.pdf" -mtime -3 -size +10M -exec ls -lh {} ;
-rw-rw-r-- 1 Bluuue Bluuue 14M 6月28日 20:42 /home/Bluuue/Desktop/1数字媒体技术基础-En-2026.pdf
-rw-rw-r-- 1 Bluuue Bluuue 13M 6月28日 20:43 '/home/Bluuue/Desktop/2数字图像及视频技术-En(5).pdf'
find 命令可以查找指定的文件,其众多的参数赋予了其很强大的能力。-type 可以指定是文件还是目录,-name 可以查找指定名字的文件,可以使用 * 表示通配。
在这里,-mtime 可以查找创建时间在指定范围的文件,正是大于等于,负是小于等于。-size 可以查找是指定大小的文件,正负还是同义。-exec 可以允许在 find 语句内执行别的语句,比如在这里跟上 ls -lh {} ; 就可以以 ls 的格式列出所有符合的文件,同时用人类可读的大小展示。这里 {} 表示将前面条件筛选出的所有路径放到这里来,; 用来分隔,让 find 知道这条命令在哪里结束。
bluuue% find ~/Desktop -type f -name "*.pdf" -mtime -3 -exec ls -lh {} ; -size +10M
-rw-rw-r-- 1 Bluuue Bluuue 14M 6月28日 20:42 /home/Bluuue/Desktop/1数字媒体技术基础-En-2026.pdf
-rw-rw-r-- 1 Bluuue Bluuue 13M 6月28日 20:43 '/home/Bluuue/Desktop/2数字图像及视频技术-En(5).pdf'
-rw-rw-r-- 1 Bluuue Bluuue 5.0M 6月28日 20:43 '/home/Bluuue/Desktop/3计算机视觉技术及应用 - En.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 7.7M 6月28日 20:43 '/home/Bluuue/Desktop/4数字音频技术及应用 - EN.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 6.5M 6月28日 20:43 '/home/Bluuue/Desktop/5计算机图形与动画技术 - EN.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 6.5M 6月28日 20:43 /home/Bluuue/Desktop/6虚拟现实技术及应用.pdf
-rw-rw-r-- 1 Bluuue Bluuue 4.6M 6月28日 20:43 '/home/Bluuue/Desktop/7数字游戏产业与开发 - En.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 6.2M 6月28日 20:43 '/home/Bluuue/Desktop/8数字媒体压缩技术 - EN.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 4.3M 6月28日 20:44 '/home/Bluuue/Desktop/9媒体大数据与可视化技术 - EN.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 8.5M 6月28日 20:44 '/home/Bluuue/Desktop/10媒体存储及网络传输技术 - EN.pdf'
-rw-rw-r-- 1 Bluuue Bluuue 3.7M 6月28日 20:44 /home/Bluuue/Desktop/11融媒体安全技术及应用-EN.pdf
bluuue% find ~/Desktop -type f -name "*.pdf" -size +10M -exec ls -lh {} ; -mtime -3
-rw-rw-r-- 1 Bluuue Bluuue 13M 6月24日 20:23 /home/Bluuue/Desktop/数据库/Lecture-2-DatabaseSetupLab.
-rw-rw-r-- 1 Bluuue Bluuue 16M 6月24日 20:26 /home/Bluuue/Desktop/数据库/Lecture-3-IntroductionOfDat
aManagement.pdf
-rw-rw-r-- 1 Bluuue Bluuue 13M 6月24日 20:28 /home/Bluuue/Desktop/数据库/Lecture-8-MultiTableJoins.p
df
-rw-rw-r-- 1 Bluuue Bluuue 12M 6月24日 20:28 /home/Bluuue/Desktop/数据库/Lecture-9-LogicalDatabaseDe
sign.pdf
-rw-rw-r-- 1 Bluuue Bluuue 14M 6月28日 20:42 /home/Bluuue/Desktop/1数字媒体技术基础-En-2026.pdf
-rw-rw-r-- 1 Bluuue Bluuue 13M 6月28日 20:43 '/home/Bluuue/Desktop/2数字图像及视频技术-En(5).pdf'
需要注意的是,如上述代码所示,在使用 ; 分隔后,由于这些参数是顺序执行的,所以写在 ; 后的参数条件是不会影响 -exec 内的语句的。
bluuue% find ~/Desktop -name "*.md" -exec grep -l "启发式合并" {} ;
/home/Bluuue/Desktop/Algo/note/Algorithm/4月/2026.4.1/ABC451F Make Bipartite 3.md
/home/Bluuue/Desktop/Algo/note/Algorithm/2026.6.22/CF2231E Graph Cutting.md
/home/Bluuue/Desktop/Algo/note/Algorithm/wida-fork.md
bluuue% find ~/Desktop -maxdepth 4 -name "*.md" -exec grep -l "启发式合并" {} ;
/home/Bluuue/Desktop/Algo/note/Algorithm/wida-fork.md
在这个的基础上,结合 grep 命令就可以查找出现指定内容的文件。需要注意的是,find 命令默认是在当前目录下递归查找的,可以通过 -maxdepth 限制最大递归深度。
- awk 命令
bluuue% cat tmp.md
a b c
d e f
g h i
bluuue% awk '{print 2}' tmp.md b e h bluuue% awk -F, '{print 2}' tmp.md
bluuue% awk -F, '{print $1}' tmp.md
a b c
d e f
g h i
bluuue% cat tmp.md
a bb c
d e f
g h i
bluuue% awk '{print $2}' tmp.md
bb
e
h
和 sed 类似,awk 是用来解析文件的,其可以用来从诸如
.csv
.csv 文件中提取特定的部分,比如某一列。默认情况下,awk 是根据空格来切分每一行的。
在这个例子中,第一次的意思是打印每一行的第二列。而如果用参数修改为用逗号切分,可以发现这里就不存在第二列了,每一行的内容都是第一列。
二、Bash 语句
- 输入输出重定向
bluuue% date > tmp.md
bluuue% cat tmp.md
2026年 07月 02日 星期四 14:23:59 CST
bluuue% cat < tmp.md
2026年 07月 02日 星期四 14:23:59 CST
bluuue% date > tmp.md
bluuue% cat tmp.md
2026年 07月 02日 星期四 14:25:20 CST
bluuue% date >> tmp.md
bluuue% cat tmp.md
2026年 07月 02日 星期四 14:25:20 CST
2026年 07月 02日 星期四 14:25:31 CST
可以读取左侧程序的输出,将其输入到右侧的文件中。< 可以读取右侧的文件,将其输入到左侧的程序中。需要注意的是,> 默认会重置整个文件,如果想要在当前文件的后面追加一行,可以使用 >>。
bluuue% cat tmp.md
c
a
b
2
4
1
23
1
1
1
2
2
bluuue% sort tmp.md | uniq -c | sort -nk1,1
1 23
1 4
1 a
1 b
1 c
3 2
4 1
| 是管道,可以将左侧程序的输出,作为输入给到右侧的程序。在上述例子中,如果想要对文件按照词频排序,可以先使用 sort 排序。然后将排序好的结果作为输入给到 uniq 命令对相邻相同的字符进行去重,这里 -c 表示打印相同的个数。之后再将这个结果按照第一列重新排序,这里 -n 表示按数值排序而非字典序,-k1,1 表示指定排序的键,表示从第一列开始到第一列结束,即词频列。
- 条件语句
bluuue% if grep 2026 tmp.md;then echo "hello 2026";echo "happy new year";fi
2026年 07月 02日 星期四 14:25:20 CST
2026年 07月 02日 星期四 14:25:31 CST
hello 2026
happy new year
bluuue% echo hi
hi
bluuue% echo $?
0
if xxx; then xxx;fi 是 bash 的条件语句,可以使用分号实现多个语句。
需要注意的是,在 bash 中,如果程序正确退出,其返回值是
0
0 而不是
1
1,对应程序最后的 return 0。所以为了对应这个,当 if 中是
0
0 的时候才表示正确从而进入分支。这里 $? 可以返回上个程序的返回值。
bluuue% if 10 = 10 ; then echo "equal";else echo "not equal";fi
equal
bluuue% if 11 = 10 ; then echo "equal";else echo "not equal";fi
not equal
bluuue% if -f tmp.md ; then echo "file exists";fi
file exists
在 bash 中,判断也是通过程序来实现的。这里的 \[\] 就是一个程序,可以进行数值判断以及判断文件是否存在等。
- 循环语句
bluuue% while grep 2026 tmp.md; do echo "it's 2026";date > tmp.md;done
2026年 07月 02日 星期四 14:40:33 CST
it's 2026
2026年 07月 02日 星期四 14:40:33 CST
it's 2026
2026年 07月 02日 星期四 14:40:33 CST
it's 2026
2026年 07月 02日 星期四 14:40:33 CST
it's 2026
bluuue% while grep 2026 tmp.md; do echo "it's 2026";date > tmp.md;sleep 10; done
2026年 07月 02日 星期四 14:53:36 CST
it's 2026
2026年 07月 02日 星期四 14:53:39 CST
it's 2026
2026年 07月 02日 星期四 14:53:49 CST
it's 2026
while xxx; do xxx; done 就是循环语句,跟条件语句一样,还是可以用分号写多个语句。在上述例子中,就是每次查看当前时间是否是 2026,是的话就写入日志文件。如果想要间隔写入,还可以通过 sleep 10 命令让程序每 10 秒写入一次。
bluuue% for varname in a b c d; do echo "$varname";done
a
b
c
d
bluuue% for varname in (seq110);doecho"(seq 1 10); do echo "(seq110);doecho"varname";done
1
2
3
4
5
6
7
8
9
10
bluuue% for varname in (seq1210);doecho"(seq 1 2 10); do echo "(seq1210);doecho"varname";done
1
3
5
7
9
for 语句跟 python 差不多,声明变量后迭代即可,注意输出变量时需要使用双引号和美元符。数值的枚举可以使用 seq start step end 命令,同样需要用美元符将输出提取出来。
- 脚本文件
bluuue% cat bash.sh
#!/bin/zsh
for i in (seq110);doecho"(seq 1 10);do echo "(seq110);doecho"i";
done;
bluuue% ./bash.sh
1
2
3
4
5
6
7
8
9
10
首先,开头的 #!/bin/zsh 语句是 shebang。当以 #!/path 开头的文件被执行时,shell 就会从 /path 启动程序,然后将脚本内容传给它。
bluuue% ls -l bash.sh
-rw-r--r-- 1 Bluuue Bluuue 72 7月 2日 15:31 bash.sh
bluuue% chmod +x bash.sh
bluuue% ls -l bash.sh
-rwxr-xr-x 1 Bluuue Bluuue 72 7月 2日 15:31 bash.sh
可以发现,写完直接执行会显示权限不够,这是因为此时该脚本只有读和写的权限而没有执行权限。此时就需要用 chmod +x 为脚本赋予执行权限,之后就能执行了。
三、课后练习
Q01 文件权限
What does the -l flag to ls do? Run ls -l / and examine the output. What do the first 10 characters of each line mean? (Hint: man ls)
bluuue% ls -l /
总计 16777236
lrwxrwxrwx 1 root root 7 2025年10月13日 bin -> usr/bin
drwxr-xr-x 5 root root 4096 1970年 1月 1日 boot
drwxr-xr-x 21 root root 4940 7月 2日 19:33 dev
drwxr-xr-x 1 root root 3134 7月 2日 22:36 etc
drwxr-xr-x 1 root root 12 6月15日 19:36 home
lrwxrwxrwx 1 root root 7 2025年10月13日 lib -> usr/lib
lrwxrwxrwx 1 root root 7 2025年10月13日 lib64 -> usr/lib
drwxr-xr-x 1 root root 0 2025年10月13日 mnt
drwxr-xr-x 1 root root 62 6月17日 00:01 opt
dr-xr-xr-x 609 root root 0 7月 2日 19:33 proc
drwxr-x--- 1 root root 112 6月24日 20:20 root
drwxr-xr-x 29 root root 700 7月 2日 20:00 run
lrwxrwxrwx 1 root root 7 2025年10月13日 sbin -> usr/bin
drwxr-xr-x 1 root root 14 6月15日 17:37 srv
-rw------- 1 root root 17179869184 6月28日 20:29 swapfile
dr-xr-xr-x 13 root root 0 7月 2日 22:30 sys
drwxrwxrwt 19 root root 1680 7月 2日 22:40 tmp
drwxr-xr-x 1 root root 80 7月 2日 22:36 usr
drwxr-xr-x 1 root root 126 6月29日 13:18 var
对于前
10
10 个字符,第一个表示文件类型,- 表示这是普通文件,d 表示这是目录,l 表示这是个符号链接,类似 windows 里的快捷方式。后
9
9 个字符
3
3 个一组分为
3
3 组,每组的三个字符 rwx 分别表示是否可读、可写和可执行。三组分别对应所有者的权限、所属组的权限和其他人的权限。其中所有者和所属组分别对应后面的两个名字,只是在这里都是 root 而已。
Q02 通配符
In the command find ~/Downloads -type f -name "*.zip" -mtime +30, the *.zip is a "glob". What is a glob? Create a test directory with some files and experiment with patterns like ls *.txt, ls file?.txt, and ls {a,b,c}.txt. See Pattern Matching in the Bash manual.
bluuue% ls -l
总计 0
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 ab.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 a.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 b.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 c.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 file12.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 file1.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 file2.txt
-rw-r--r-- 1 Bluuue Bluuue 0 7月 3日 00:15 image.png
bluuue% ls *.txt
ab.txt a.txt b.txt c.txt file12.txt file1.txt file2.txt
bluuue% ls file?.txt
file1.txt file2.txt
bluuue% ls {a,b,c}.txt
a.txt b.txt c.txt
bluuue% ls abc.txt
a.txt b.txt c.txt
bluuue% ls a-c.txt
a.txt b.txt c.txt
bluuue% ls a-d.txt
a.txt b.txt c.txt
bluuue% ls {a,b,c,d}.txt
ls: 无法访问 'd.txt': 没有那个文件或目录
a.txt b.txt c.txt
bluuue% ls \^a-b.txt
c.txt
bluuue% ls *.(txt|png)
ab.txt a.txt b.txt c.txt file12.txt file1.txt file2.txt image.png
- 可以匹配空字符串在内的任意字符串,? 可以匹配任意一个字符。
{} 可以将内部的每一个元素拿出来单独匹配,需要注意的是,由于是单独匹配,所以当某个元素拿出来找不到时会报错。
\[\] 可以匹配在内部范围内的元素,和 {} 不同的是,当找不到时不会发生报错。由于是范围匹配,也可以写成 a-d 表示匹配 a,b,c,d 四个元素。如果想要匹配不在范围内的元素,可以使用 ^ 表示取反。
除此之外,还可以使用 | 同时匹配多种模式。
Q03 引用
What's the difference between 'single quotes', "double quotes", and 'ANSI quotes'? Write a command that echoes a string containing a literal , a !, and a newline character. See Quoting.
bluuue% echo PATH/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/siteperl:/usr/bin/vendorperl:/usr/bin/coreperlbluuuePATH /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl bluuue% echo 'PATH/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/siteperl:/usr/bin/vendorperl:/usr/bin/coreperlbluuuePATH'
PATHbluuuePATH bluuue% echo "PATHbluuuePATH"
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
bluuue% echo "hello\nworld"
hello
world
bluuue% ls ".md"
ls: 无法访问 '.md': 没有那个文件或目录
Bluuue@bluuue \~$ echo 'hello\nworld'
hello\nworld
Bluuue@bluuue \~ echo 'hello\nworld'
hello
world
bluuue% echo 'here it is PATH\nhi'
here it is $PATH
hi
首先,单引号是强引用,内部不管什么字符都会被当成字符串输出。而双引号是弱引用,会将其中的 进行变量展开和 \\n 等字符转义。而 '' 只解读内部的 \n 等字符转义,之后就会变成正常的单引号。对于通配符,不管用单引号还是双引号,其都不会被解释出来。
需要注意的是,zsh 中的 echo 会自动帮你把 \n 翻译出来,即使用的是强引用。
Q04 重定向
The shell has three standard streams: stdin (0), stdout (1), and stderr (2). Run ls /nonexistent /tmp and redirect stdout to one file and stderr to another. How would you redirect both to the same file? See Redirections.
bluuue% ls /nonexistent /tmp
ls: 无法访问 '/nonexistent': 没有那个文件或目录
/tmp:
{4894522e-c6b9-4c04-b728-9941bc1f1405}
{7B585BD5-8E73-4058-B7DF-F46EE9AB43BC}KDCSDK_RESTORE_LOCK_MUTEXb15be47aef79e9d8fcfdb5838d512108.lock
...
bluuue% ls /nonexistent /tmp 1> stdout.log 2> stderr.log
bluuue% cat stdout.log
/tmp:
{4894522e-c6b9-4c04-b728-9941bc1f1405}
{7B585BD5-8E73-4058-B7DF-F46EE9AB43BC}KDCSDK_RESTORE_LOCK_MUTEXb15be47aef79e9d8fcfdb5838d512108.lock
...
bluuue% cat stderr.log
ls: 无法访问 '/nonexistent': 没有那个文件或目录
bluuue% ls /nonexistent /tmp &> both.log
bluuue% cat both.log
ls: 无法访问 '/nonexistent': 没有那个文件或目录
/tmp:
{4894522e-c6b9-4c04-b728-9941bc1f1405}
{7B585BD5-8E73-4058-B7DF-F46EE9AB43BC}KDCSDK_RESTORE_LOCK_MUTEXb15be47aef79e9d8fcfdb5838d512108.lock
...
Shell 中,标准输入流的文件描述符是
0
0,标准输出流是
1
1,标准错误流是
2
2。标准错误流和标准输出流不同,它会输出报错结果,可以根据这个将成功的结果和报错的结果存入不同的文件。
此时,就可以通过重定向,用 1> 或 > 将标准输出流重定向到 stdout.log 中,用 2> 将标准错误流重定向到 stderr.log 里,这样就可以分别查看了。如果要将两者重定向到一个文件里,可以用 &>。
bluuue% cat << EOF
heredoc> hello
heredoc> hi
heredoc> 你好
heredoc> EOF
hello
hi
你好
<< 是 Here Documents 操作符,可以允许从这里开始输入多行信息,直到读到规定的提示符。然后在不创建新文件的情况下,一股脑将输入当作文件给出。这里的 EOF 可以是任何自己喜欢的词。需要注意的是,cat 是从文件中读取,而 echo 是输出参数,压根不会去动输入流,所以 echo < file 的写法是不行的。
Q05 逻辑运算
$? holds the exit status of the last command (0 = success). && runs the next command only if the previous succeeded; || runs it only if the previous failed. Write a one-liner that creates /tmp/mydir only if it doesn't already exist. See Exit Status.
bluuue% cd Desk && echo hello,Desktop
cd: 没有那个文件或目录: Desk
bluuue% cd Desk || echo hello,Desktop
cd: 没有那个文件或目录: Desk
hello,Desktop
bluuue% -d /tmp/mydir || mkdir /tmp/mydir
bluuue% ls /tmp | grep mydir
mydir
和正常的程序类似,&& 和 || 还是可以通过第一个程序是否正常退出,从而判断是否执行下一个程序。如果要写一句当且仅当某目录不存在时才创建,就可以写成上述代码这样,通过 || 判断是否存在,若不存在才去执行后面的创建命令。
Q06 内置命令
Why does cd have to be built into the shell itself rather than a standalone program? (Hint: think about what a child process can and cannot affect in its parent.)
bluuue% type cd
cd is a shell builtin
bluuue% type ls
ls is /usr/bin/ls
bluuue% type exit
exit is a shell builtin
可以看出,cd 是 shell 内置命令,而 ls 是外部命令。
在操作系统中,子进程不能修改父进程的当前工作目录,即进程隔离的一部分。当用户输入一条外部命令时,shell 这个进程会创建一个继承了当前父进程各种状态的子进程,然后在子进程内运行命令,之后销毁退出。那么如果 cd 是一个外部命令,就只能修改其子进程的工作目录,完全影响不到 shll 的目录。
那么也可以联想到,exit 作为退出 shell 的命令,也需要是内部的。
Q07 条件判断
Write a script that takes a filename as an argument ($1) and checks whether the file exists using test -f or -f ... . It should print different messages depending on whether the file exists. See Bash Conditional Expressions.
#!/bin/zsh
if -z "$1" ; then
echo "Arguments required"
exit 1
fi
FILE="1"if−f"1" if \[ -f "1"if\[−f"FILE" ; then
echo "File exists"
else
echo "File not exists"
fi
首先需要判断用户是否输入了第一个参数。在这里,0 表示这个程序本身,1 才表示第一个参数。那么就可以先用 -z 判断第一个参数是否为空字符串。如果不是的话,就可以再用 -f 判断该文件是否存在了。
bluuue% zsh check_file.sh
Arguments required
bluuue% zsh check_file.sh tmp.md
File exists
bluuue% zsh check_file.sh tmp.txt
File not exists
bluuue% source check_file.sh tmp.md
File exists
之后就可以用 zsh 运行了。需要注意的是,这里不能使用 source 加载脚本。因为 source 是将给出的文件加载到 shell 里执行,所以如果不提供参数的话在碰到 exit 1 的时候就直接退出 shell 了。所以加载时必须把参数提供出来,要么就是写成函数,先加载再运行函数。
bluuue% if 10 \< 5 ; then echo less;else echo greater;fi
zsh: 没有那个文件或目录: 5
greater
bluuue% if \[ 10 \< 5 ]; then echo less;else echo greater;fi
less
bluuue% if (( 10 < 5 )); then echo less;else echo greater;fi
greater
除此之外,在判断大小时,如果只写一个方括号,大于小于号会被识别成重定向,从而发生错误。这个是因为一个方括号是单目运算符,只需要一个操作数,通常用于判断文件是否存在。
正确的写法是写成两个方括号实现双目运算,但此时比较的是两个字符串的字典序。如果想要数值比较的话需要写成两个小括号。
Q08 权限赋予
Save the script from the previous exercise to a file (e.g., check.sh). Try running it with ./check.sh somefile. What happens? Now run chmod +x check.sh and try again. Why is this step necessary? (Hint: look at ls -l check.sh before and after the chmod.)
bluuue% ./check_file.sh tmp.md
zsh: 权限不够: ./check_file.sh
bluuue% ls -l ./check_file.sh
-rw-r--r-- 1 Bluuue Bluuue 159 7月 4日 00:48 ./check_file.sh
bluuue% chmod +x check_file.sh
bluuue% ls -l ./check_file.sh
-rwxr-xr-x 1 Bluuue Bluuue 159 7月 4日 00:48 ./check_file.sh
bluuue% ./check_file.sh tmp.md
File exists
用 touch 或编辑器保存的文件,默认初始只赋予读和写的权限,如果想要执行,必须使用 chmod 赋予执行权限。而在之前的操作中,zsh check_file.sh 可以运行的原因是,此时并不是执行的这个脚本,而是将这个脚本输入给了 zsh,再由 zsh 进行执行。
Q09 调试语句
What happens if you add -x to the set flags in a script? Try it with a simple script and observe the output. See The Set Builtin.
#!/bin/zsh
set -x
NAME="Bluuue"
GREETING="Hello,$NAME"
echo "$GREETING"
先执行 set -x 命令,再依次对变量进行赋值并输出。
bluuue% zsh debug.sh
+debug.sh:5> NAME=Bluuue
+debug.sh:6> GREETING=Hello,Bluuue
+debug.sh:8> echo Hello,Bluuue
Hello,Bluuue
set -x 可以开启命令追踪模式,此时就可以显示每一步的变量和内容了。需要注意的是,在这里减号是开启,加号是关闭。
#!/bin/zsh
NAME="Bluuue"
set -x
GREETING="Hello,$NAME"
set +x
echo "$GREETING"
bluuue% zsh debug.sh
+debug.sh:7> GREETING=Hello,Bluuue
+debug.sh:9> set +x
Hello,Bluuue
当然也可以用 set 调试某一部分的代码。
#!/bin/zsh
NAME="Bluuue"
GREETING="Hello,$NAME"
echo "$GREETING"
bluuue% zsh -x debug.sh
+debug.sh:3> NAME=Bluuue
+debug.sh:4> GREETING=Hello,Bluuue
+debug.sh:6> echo Hello,Bluuue
Hello,Bluuue
或者也可以通过 zsh -x 开启全局调试,也可以直接写在 shebang 里 #!/bin/zsh -x。
#!/bin/zsh
echo $#
set -- A B C
echo $1
echo $2
echo $3
echo $#
bluuue% zsh debug.sh 1 2
2
A
B
C
3
Hello,Bluuue
还可以通过 -- 暂时修改参数。可以发现,虽然传进去是两个参数,但在修改后就变成了三个参数,参数内容也随之发生了改变。
Q10 命令替换
Write a command that copies a file to a backup with today's date in the filename (e.g., notes.txt → notes_2026-01-12.txt). (Hint: $(date +%Y-%m-%d)). See Command Substitution.
bluuue% cp note.md "note_$(date +%Y-%m-%d_%H-%M-%S).md"
bluuue% find -name "note*.md" -exec ls -l {} ;
-rw-r--r-- 1 Bluuue Bluuue 25 7月 2日 15:19 ./note.md
-rw-r--r-- 1 Bluuue Bluuue 25 7月 4日 16:13 ./note_2026-07-04_16-18-43.md
在复制语句中,$() 就是命令替换,可以将命令的输出插入到当前位置。
Bluuue@bluuue \~ x=123;echo x;echo { local x=456;echo x; };echo x123456123需要注意的是,x 123 456 123 需要注意的是,x123456123需要注意的是,() 是新开一个子进程执行命令,会有较大的开销。而 {} 可以直接在当前进程下运行,但需要注意对外部变量的修改和 exit 的影响,整体特性类似匿名函数。此时可以配合 local 声明当前作用域下的变量,注意这个只在 bash 中有效,zsh 只能使用 ()。
Q11 命令参数
Modify the flaky test script from the lecture to accept the test command as an argument instead of hardcoding cargo test my_test. (Hint: 1 or @). See Special Parameters.
#!/bin/zsh
if $# -eq 0 ; then
echo "Argument required"
exit 1
fi
count=0
while true; do
count=$((count+1))
echo "Running on test $count"
eval "$@"
if [ $? -ne 0 ];then
echo "Failed"
break
fi
done
首先还是特判没输入参数的情况。之后设置一个计数器,注意需要使用 (()) 进行数值计算,不然会被解析为字符串拼接。然后通过 @ 获取所有输入的参数,通过 eval 进行二次解析并运行,直到运行失败就退出。
注意这里的 @ 可以将所有参数及其内部的空格,而 * 会对空格进行拆分。除此之外,$0 可以查看当前在使用的 shell 名字。
bluuue% zsh flaky_test.sh "shuf -i 1-10 -n 1 | grep -xq '1-9' "
Running on test 1
Running on test 2
Running on test 3
Running on test 4
Running on test 5
Running on test 6
Running on test 7
Running on test 8
Running on test 9
Running on test 10
Running on test 11
Running on test 12
Running on test 13
Running on test 14
Running on test 15
Running on test 16
Running on test 17
Running on test 18
Running on test 19
Running on test 20
Running on test 21
Running on test 22
Running on test 23
Failed
这里是用 shuf 生成随机数,再用 grep 判断是否在
1
1 到
9
9 的范围内。注意 -x 是一次判断一行,否则会因为
10
10 中也有
1
1 从而被 grep 获取到,-q 是不输出发现的结果。
还需要注意的是,通常情况下,shell 会先对给出的命令字符串进行解析再运行。在解析的过程中,shell 会先将字符串中的管道、重定向和分号等拿出来,再对变量替换、命令替换和通配符进行展开,最后再将引号去掉,然后开始执行。那么如果直接将外部命令传入的话,会导致命令中的管道和通配符被提前拦下,从而发生报错。所以将命令作为参数传入时,必须要加引号进行作为字符串传入。
所以在传给 grep 等命令带有通配符的表达式时,需要先用单引号对通配符进行保护,防止 shell 先给这个表达式解析了,再让 grep 自己去解析这个表达式。
还没结束,这样传入命令字符串后,如果直接写 @会导致在shell解析的过程中,首先没有发现管道,之后再进行展开。此时展开后,管道就不会再被解析,只会被当作普通字符,从而无法正确执行。而eval可以先将"@ 会导致在 shell 解析的过程中,首先没有发现管道,之后再进行展开。此时展开后,管道就不会再被解析,只会被当作普通字符,从而无法正确执行。而 eval 可以先将 "@会导致在shell解析的过程中,首先没有发现管道,之后再进行展开。此时展开后,管道就不会再被解析,只会被当作普通字符,从而无法正确执行。而eval可以先将"@" 替换为正确的命令字符串,再将这个字符串扔给 shell 进行解析,这样就可以完整解析命令了。
Q12 管道的应用
Use pipes to find the 5 most common file extensions in your home directory. (Hint: combine find, grep or sed or awk, sort, uniq -c, and head.)
bluuue% find ~ -type f | awk -F/ '{print NF}' \| grep '.' \| awk -F. '{print NF}' | sort | uniq -c | sort -
nk1,1 | tail -n5
5289 md
5355 jpg
8272 go
11208 png
21359 js
首先,find ~ -type f 递归地筛出所有文件,然后 awk -F/ '{print $NF}' 按照 / 进行切分,然后输出最后一列的内容,即 xxx.xxx 的形式。之后再用 grep 把有 . 的文件筛出来,去掉一些没有后缀的文件。然后再用一次 awk 按照 . 切分,输出最后一列,即后缀名。最后将这些结果排序去重再根据数值从小到大排序,输出最后五行即可。
bluuue% find ~ -type f | awk -F/ '{print NF}' \| grep '.' \| awk -F. '{print NF}' | sort | uniq -c | sort -
rnk1,1 | head -n5
21359 js
11208 png
8272 go
5360 jpg
5289 md
当然,使用 sort -r 倒序排序最后输出前五个也可以()
Q13 参数转换
xargs converts lines from stdin into command arguments. Use find and xargs together (not find -exec) to find all .sh files in a directory and count the lines in each with wc -l. Bonus: make it handle filenames with spaces. (Hint: -print0 and -0). See man xargs.
bluuue% find . -type f -name "*.sh" | wc -l
3
bluuue% find . -type f -name "*.sh" | xargs wc -l
14 ./check_file.sh
15 ./debug.sh
19 ./flaky_test.sh
48 总计
首先,wc 命令可以对给定内容进行计数,包括但不限于计数行数和单词数。在这里,-l 就是计数行数。之后,xargs 可以将管道传过来的文件名列表转换成命令参数给到 wc 去执行。在这里,如果不加 xargs,由于 find 找到了三个文件,wc 统计的就是找到的文件个数,而不是这三个文件里的内容的行数。
bluuue% find . -name "*.sh" -print0 | xargs -0 wc -l
14 ./check_file.sh
15 ./debug.sh
19 ./flaky_test.sh
0 ./space script.sh
48 总计
默认情况下,xargs 会按空格和换行符进行拆分,此时如果把文件 "space script.sh" 传过去,就会被拆分成两个文件。此时,就可以先用 -print0 让 find 不用换行符分隔,而是用空字符 \0 来分隔找到的文件。之后 -0 就是让 xargs 不要用空格和换行符分隔,而是用这个 \0 来拆分参数,这样就可以实现查找文件名带空格的文件了。
和 find -exec 不同,当 find 找到一个文件时,-exec 都会生成一个全新的子进程,这个的开销的很大的。但 xargs 可以把找到的文件打包一次性发给 wc,从而减小开销。
find . -type f -name "*.sh" | ls -l
-rwxr-xr-x 1 Bluuue Bluuue 159 7月 4日 00:48 check_file.sh
-rw-r--r-- 1 Bluuue Bluuue 123 7月 4日 16:03 debug.sh
-rwxr--r-- 1 Bluuue Bluuue 29937704 6月18日 11:01 MyPaint-v2.0.1.AppImage
-rw-r--r-- 1 Bluuue Bluuue 25 7月 4日 16:13 note_2026-07-04_16-18-43.md
-rw-r--r-- 1 Bluuue Bluuue 25 7月 4日 16:13 'note_2026-07-04.md'$'\n'
-rw-r--r-- 1 Bluuue Bluuue 25 7月 2日 15:19 note.md
-rw-r--r-- 1 Bluuue Bluuue 0 7月 5日 15:14 'space script.sh'
bluuue% find . -type f -name "*.sh" | xargs ls -l
-rwxr-xr-x 1 Bluuue Bluuue 159 7月 4日 00:48 ./check_file.sh
-rw-r--r-- 1 Bluuue Bluuue 123 7月 4日 16:03 ./debug.sh
-rw-r--r-- 1 Bluuue Bluuue 214 7月 4日 17:11 ./flaky_test.sh
这个就体现出 xargs 的重要性了。因为 ls 命令永远不会碰标准输入,所以管道传给他的文件是不会被考虑到的,此时就需要 xargs 将其转换为参数再传给 ls。
Q14 网页获取
Use curl to fetch the HTML of the course website (https://missing.csail.mit.edu/) and pipe it to grep to count how many lectures are listed. (Hint: look for a pattern that appears once per lecture; use curl -s to silence the progress output.)
bluuue% curl -s https://missing.csail.mit.edu/ | grep '/2026/a-z-+/' | wc -l
9
curl 命令可以对制定网页发送请求并获取该网页的内容,需要注意的是,如果不加 -s 静音,其就会输出下载进度条污染数据,加上之后就可以只输出网页的 HTML 源码。
传给 grep 后,考虑用正则表达式 /2026/a-z-+/ 对其进行匹配。这个表达式的意思是,先匹配路径为 /2026/,再匹配所有小写英文字母和连字符,+ 允许前面的字符至少出现一次,最后匹配结尾的 /。最后用 wc 计数行数即可。
Q15 json 操作
jq is a powerful tool for processing JSON data. Fetch the sample data at https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json with curl and use jq to extract just the names of people whose version is greater than 6. (Hint: pipe to jq . first to see the structure; then try jq '.\[\] | select(...) | .name')
bluuue% curl -s https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json | jq .
...
{
"name": "Zamokuhle Zulu",
"language": "isiZulu",
"id": "XU7BX2F8M5PVZ1EF",
"bio": "Etiam congue dignissim volutpat. Phasellus tincidunt sollicitudin posuere. Phasellus tincidunt s
ollicitudin posuere. Nam tristique feugiat est vitae mollis.",
"version": 8.39
},
{
"name": "Bhupesh Menon",
"language": "Hindi",
"id": "0CEPNRDV98KT3ORP",
"bio": "Maecenas tempus neque ut porttitor malesuada. Phasellus massa ligula, hendrerit eget efficitur e
get, tincidunt in ligula. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Maecenas qu
is nisi nunc.",
"version": 2.69
}
]
首先,json 是一种用于数据交换和配置格式的文件类型,其是由代表一个对象的大括号、代表一个列表的中括号以及若干键值组成。
bluuue% curl -s https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json | jq '.\[\] | select(.version > 6 ) | .name'
"Adeel Solangi"
"Aamir Solangi"
"Adil Eli"
"Adil Abro"
"Antía Sixirei"
...
bluuue% curl -s https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json | jq -r '.\[\] | select(.version > 6 ) | .name'
Adeel Solangi
Aamir Solangi
Adil Eli
Adil Abro
Antía Sixirei
Aygul Mutellip
Celtia Anes
...
首先在拿 curl 拿出 json 格式的文件后,先用 .\[\] 将返回的大数组拆成若干个对象,再用 select (.version > 6) 筛选出版本大于 6 的对象,最后再输出其 .name 属性。在一般情况下,由于通常需要将其输出再传给别的程序,所以最好加上 -r 参数将其输出从字符串改为纯文本,避免传给别的程序时发生干扰。
Q16 awk 应用
awk can filter lines based on column values and manipulate output. For example, awk '3 \~ /pattern/ {4=""; print}' prints only lines where the third column matches pattern, while omitting the fourth column. Write an awk command that prints only lines where the second column is greater than 100, and swaps the first and third columns. Test with: printf 'a 50 x\nb 150 y\nc 200 z\n'
bluuue% printf 'a 50 x\nb 150 y\nc 200 z\n' | awk '2 \> 100 {print 3,2,1}'
y 150 b
z 200 c
bluuue% printf 'a 50 x\nb 150 y\nc 200 z\n' | awk '2 \> 100 {print 3 2 1}'
y150b
z200c
首先,awk 的模式是 条件 { 动作 },即只对满足条件的行执行该动作,在这里就是筛掉第二列小于等于 100 的行。之后在打印的时候,对换第三列和第一列的位置即可。注意,在 print 中,逗号表示分隔,在输出时会被替换为空格,不写的话内容就会黏在一起。
Q17 远程连接
Dissect the SSH log pipeline from the lecture: what does each step do? Then build something similar to find your most-used shell commands from ~/.bash_history (or ~/.zsh_history).
missing:~$ ssh myserver 'journalctl -u sshd -b-1 | grep "Disconnected from"'
| sed -E 's/.Disconnected from . user (.) \^ + port. /\1/'
| sort | uniq -c
| sort -nk1,1 | tail -n10
| awk '{print $2}' | paste -sd,
postgres,mysql,oracle,dell,ubuntu,inspur,test,admin,user,root
首先,ssh 可以让你远程连接到另一台电脑,直接进行操纵或者单纯给其发送要执行的命令并接收运行结果。在这里,ssh myserver 就是在连接一台远程服务器。
之后,如果自己拿 ai 和 linux 系统打过交道,一定见过journalctl 命令。这个命令可以调用系统的日志管理器,-u sshd 表示只看 SSH 服务,即所有的远程连接日志,-b-1 表示只看上一次开机周期的日志。然后对于这些日志,用 grep 筛出连接断开的行,再用 sed 提取出尝试登录的用户名,之后排序去重并统计出词频最高的 10 个用户名,并且用 awk 去掉词频只保留用户名。
bluuue% cat file1.txt
1
2
bluuue% cat file2.txt
A
B
bluuue% paste -d, file1.txt file2.txt
1,A
2,B
bluuue% paste -d, file1.txt
1
2
bluuue% curl -s https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json | jq -r '.\[\] | select(.version > 6 ) | .name' | head -n5
Adeel Solangi
Aamir Solangi
Adil Eli
Adil Abro
Antía Sixirei
bluuue% curl -s https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json | jq -r '.\[\] | select(.version > 6 ) | .name' | head -n5 | paste -d,
Adeel Solangi
Aamir Solangi
Adil Eli
Adil Abro
Antía Sixirei
bluuue% curl -s https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json | jq -r '.\[\] | select(.version > 6 ) | .name' | head -n5 | paste -sd,
Adeel Solangi,Aamir Solangi,Adil Eli,Adil Abro,Antía Sixirei