Missing Semester 计算机教育中缺失的一课 Lecture 02 Command-line Environment
前言
这课从练习题里学到的比视频多得多……
一、命令行
1. 标志
bluuue% ls -l answer.docx
-rw-r--r-- 1 Bluuue Bluuue 34178 7月 5日 18:44 answer.docx
bluuue% touch answer.docx
bluuue% ls -l answer.docx
-rw-r--r-- 1 Bluuue Bluuue 34178 7月 7日 10:30 answer.docx
bluuue% ls answer.docx
answer.docx
bluuue% ls arguments.sh
arguments.sh
bluuue% ls answer.docx arguments.sh
answer.docx arguments.sh
首先,touch 命令可以更新文件的修改时间,如果文件不存在就会创建这个文件。
在上一节也了解过,诸如 -l 和 --help 这种参数叫 “标志”,其可以改变程序的运行方式等属性。一般单横杠 - 表示简写,同时允许将 -l 和 -a 合并写成 -la。而双横杠 -- 后一般是全称,如 --help。
在脚本中,$0 表示此程序本身的名字,之后 $[1-9] 表示之后的若干个参数。$# 为当前传入的参数个数,$@ 表示参数列表。对于允许接受多个参数的程序,可以将多个参数写在一行。
#!/bin/zsh
if [ $# -eq 0 ]; then
echo "No arguments"
else
for argu in "$@"; do
echo $argu
done
fi
bluuue% zsh arguments.sh *.pdf
2022ICPCXian_statement.pdf
2024ICPCNanjing_statement.pdf
2026Jiangsu_solution.pdf
2026Jiangsu_statement.pdf
xcpc模板(至圣 && Nrtusea && Hiraethsoul).pdf
bluuue% ls **/*.md | grep "CF.*\.md"
Algo/note/Algorithm/6月/2026.6.9/CF1264C Beautiful Mirrors with queries.md
Algo/note/Algorithm/6月/2026.6.9/CF2040E Control of Randomness.md
Algo/note/Algorithm/6月/2026.6.9/CF912D Fishes.md
...
对于通配符,在上节的练习题中也有提到,其会先被 shell 解析展开,再作为参数传给程序。在 zsh 中,还可以使用 ** 表示任意路径。
2. 流
import time
for i in range(10):
print(i)
time.sleep(1)
bluuue% python slow.py
0
1
2
3
4
5
6
7
8
9
bluuue% python -u slow.py | grep -P '1|3|5|7|9'
1
3
5
7
9
将程序运行后可以发现,通过管道连接的若干个程序并不是顺序执行的,而是并行执行的,即当 python 输出一个数字后立刻给到 grep 进行判断并输出。需要注意的是,python 的输出默认带有缓冲区,需要用 -u参数取消。
bluuue% cat data.md
B
C
A
1
3
2
5
6
4
1
1
1
3
bluuue% ( sleep 5 && cat data.md ) | grep -P '\d' | sort | uniq &
[1] 99431 99433 99434 99435
bluuue% ps
PID TTY TIME CMD
9353 pts/1 00:00:09 zsh
99431 pts/1 00:00:00 zsh
99432 pts/1 00:00:00 sleep
99433 pts/1 00:00:00 grep
99434 pts/1 00:00:00 sort
99435 pts/1 00:00:00 uniq
99439 pts/1 00:00:00 ps
bluuue% 1
2
3
4
5
6
[1] + done ( sleep 5 && cat data.md; ) | grep -P '\d' | sort | uniq
在这里,可以在程序后加上 & 让其到后台运行,此时再用 ps 命令查看所有进程,可以发现一开始所有程序都在执行,除了 cat 命令需要等到 sleep 结束后才开始执行。
bluuue% ls answer.docx > output.txt
bluuue% cat output.txt
answer.docx
bluuue% ls answer.pptx > output.txt
ls: 无法访问 'answer.pptx': 没有那个文件或目录
bluuue% cat output.txt
bluuue% ls answer.pptx 2> output.txt
bluuue% cat output.txt
ls: 无法访问 'answer.pptx': 没有那个文件或目录
bluuue% ls answer.pptx 2> /dev/null
就像上节课的题目里说的,标准输出和标准错误是不同的流,重定向时需要分类讨论。除此之外,如果想丢弃某个流的内容,可以直接将其输出到 \dev\null 中。
bluuue% ls ./Algo | fzf
bluuue% cat ~/.bash_history | fzf
fzf 命令可以交互式地列出传入的内容。
3. 环境变量
bluuue% foo=bar
bluuue% echo $foo
bar
在命令行中,可以使用 foo=bar 的语法声明变量,之后可以使用 $ 对变量进行展开。注意此时不能有空格,否则会被判定为命令。
bluuue% zsh -c 'echo $foo'
bluuue% foo=bar zsh -c 'echo $foo'
bar
bluuue% foo=car zsh -c 'echo $foo'
car
bluuue% foo=car zsh -c "echo $foo"
bar
如果现在使用 bash -c 'echo $foo',会发现什么都没输出。这是因为 bash 命令会创建一个子进程,然后在子进程里执行命令。而子进程只会继承父进程的环境变量,之前创建的是普通变量,无法继承。此时,可以通过在命令前重新声明一遍的方法临时创建环境变量,这样子进程就可以正常解析了。
除此之外,因为双引号是弱引用,其会先对内部的变量进行解析,所以如果用双引号的话解析出来的还是之前声明的临时变量。而单引号作为强引用,其不会提前对变量进行解析,而是将 echo $foo 这个命令完整给到子进程。
bluuue% date
2026年 07月 07日 星期二 11:57:45 CST
bluuue% TZ=Asia/Tokyo date
2026年 07月 07日 星期二 12:58:00 JST
对于 shell 的默认环境变量,也可以通过临时环境变量方法进行临时修改。比如上面例子,date 会检查环境变量 TZ 的内容,然后根据其显示的时区列出当前时间。所以就可以临时修改这个环境变量,让其输出别的时区的时间。
bluuue% x=123
bluuue% echo $x
123
bluuue% zsh -c 'echo $x'
bluuue% export x=456
bluuue% zsh -c 'echo $x'
456
bluuue% echo $x
456
除了临时变量声明,还可以用 export 直接声明其为环境变量,这样子进程就可以正常继承了。
4. 返回值
bluuue% grep -q 1 data.md && echo "Found"
Found
bluuue% grep -q 10 data.md || echo "Not found"
Not found
如上节练习题里说的,程序正常结束时的返回值为 0,否则就说明程序发生了错误。那么此时就可以通过 && 和 || 逻辑运算符,根据左侧程序是否正常退出,来控制何时执行后续的程序。
while read line; do
echo "$line"
done < data.md
read 命令可以从标准输入读取内容,然后赋值给变量。那么就可以使用循环语句,将是否读到下一行作为循环条件,从而实现遍历整个文件的每一行。
5. 信号
bluuue% sleep 10
^C
bluuue%
如果想要强制结束一个程序,可以按 Ctrl+C,这就是信号 (signal)。此时 shell 会识别到这个特殊的按键组合,然后向正在执行的程序发送 SIGINT 信号,即 signal interrupt。程序在接受到这个信号后,会中断当前的执行,然后默认退出。
import signal, time
def handler(signum, time):
print("\nI got a SIGINT, but I am not stopping")
signal.signal(signal.SIGINT, handler)
i = 0
while True:
time.sleep(.1)
print("\r{}".format(i), end="")
i += 1
bluuue% python ignore.py
10^C
I got a SIGINT, but I am not stopping
23^C
I got a SIGINT, but I am not stopping
38^C
I got a SIGINT, but I am not stopping
68^\zsh: quit (core dumped) python ignore.py
如果自行设置了一个 SIGINT 信号的处理方法,那么此时再按 Ctrl+C 就不会退出程序了。此时如果还想强制退出,可以考虑按 Ctrl+\ 表示 SIGQUIT 强制退出。
bluuue% sleep 20
^Z
zsh: suspended sleep 20
bluuue% jobs
[1] + suspended sleep 20
Ctrl+Z 可以发送信号 SIGTSTP,表示暂停一个程序,此时用 jobs 就可以看到当前终端里正在执行的程序。如果想要恢复执行,可以用 fg 表示抓回前台继续,bg 表示在后台默默执行。
bluuue% sleep 30
^Z
zsh: suspended sleep 30
bluuue% sleep 60
^Z
zsh: suspended sleep 60
bluuue% jobs
[1] - suspended sleep 30
[2] + suspended sleep 60
bluuue% kill -SIGINT %2
[2] + interrupt sleep 60
bluuue% jobs
[1] + suspended sleep 30
如果想要强制杀死某个进程,可以使用 kill 命令向其发送中断信号 SIGINT。此时,可以通过 %1 指代 jobs 列出的第一个进程。
bluuue% sleep 60
^Z
zsh: suspended sleep 60
bluuue% jobs
[1] + suspended sleep 60
bluuue% ps
PID TTY TIME CMD
6416 pts/1 00:00:02 zsh
92146 pts/1 00:00:00 sleep
92155 pts/1 00:00:00 ps
bluuue% kill -SIGCONT 92146
bluuue% jobs
[1] + running sleep 60
注意,kill 命令并不代表杀死一个进程,而是给进程发送一个信号!!如果想要恢复一个进程,可以发送 SIGCONT 信号。此外,如果想要给某个特定进程发送信号,kill 命令接受的是进程的 pid,这个可以通过 ps 命令查看。
bluuue% sleep 100
^Z
zsh: suspended sleep 100
bluuue% nohup sleep 200 &
[2] 98159
nohup: 忽略输入并把输出追加到 'nohup.out'
bluuue% jobs
[1] + suspended sleep 100
[2] - running nohup sleep 200
bluuue% kill -SIGHUP %1
[1] + hangup sleep 100
bluuue% jobs
[2] + running nohup sleep 200
bluuue% kill -SIGHUP %2
bluuue% jobs
[2] + running nohup sleep 200
bluuue% kill %2
[2] + terminated nohup sleep 200
需要注意的是,在后台执行的所有任务,都是该终端的子进程。如果关闭当前终端,这些子进程都会收到 SIGHUP 的信号然后被一起杀死。防范措施是,在程序启动时在前面加上 nohup,这样该程序就是自动忽略 SIGHUP 信号了。此时仍然可以使用 kill %2 默认发送 SIGTERM 强行终止。
bluuue% ( sleep 10 && echo Done )
^Z
zsh: suspended ( sleep 10 && echo Done; )
bluuue% bg
[1] + continued ( sleep 10 && echo Done; )
bluuue% disown
bluuue% jobs
bluuue% Done
如果在启动时忘了加 nohup,可以先 Ctrl+Z 暂时,然后 bg 让其到后台运行,再输入 disown 切断该子进程和父进程的关系。此时再用 jobs 就看不到该进程了,但最终仍然能完成执行。默认情况下,disown 指向的是最近被放到后台的作业,等效于 disown %+。如果想要让所有子进程都和父进程切割,可以使用 -a 参数。
注意,如果单纯写成 sleep 10 && echo Done,在按 Ctrl+Z 暂停时,此时暂停的是正在运行的 sleep 进程。而由于 sleep 程序并没有完成执行并返回 0,所以后面的 echo 自然也就不会执行了。即使后续恢复执行 sleep,刚刚的 echo 也早就被忽略了。解决方法是,通过给整体加括号的方式,强行将这条语句放入一个全新的子进程,此时按 Ctrl+Z 暂停的就是这一个整体了。
#!/bin/zsh
cleanup() {
echo "Cleaning up temporary files..."
}
trap cleanup EXIT # Run cleanup when script exits
trap cleanup SIGINT SIGTERM # Also on Ctrl-C or kill
sleep 10
bluuue% zsh clean.sh
^CCleaning up temporary files...
Cleaning up temporary files...
注意,如果程序在运行时被强行终止,可能会留下一堆垃圾文件。此时可以用 trap 命令当脚本正常退出或遇到强行终止时去调用清理程序。注意,trap 需要写在所有可能发生终止的命令前!然后,这里清理了两次,正常情况下只能清理一次。这个可以通过只让 EXIT 清理,发生终止时让执行 exit xxx,顺便调用清理即可。
二、远程连接
bluuue% sudo systemctl start sshd
bluuue% systemctl status sshd
● sshd.service - OpenSSH Daemon
Loaded: loaded (/usr/lib/systemd/system/sshd.service; disabled; preset: disabled)
Active: active (running) since Wed 2026-07-08 23:41:39 CST; 1min 53s ago
Invocation: f6327cbdc9014f549065dd93fe1ba490
Docs: man:sshd(8)
man:sshd_config(5)
Main PID: 164666 (sshd)
Tasks: 1 (limit: 18418)
Memory: 1.5M (peak: 5.7M)
CPU: 35ms
CGroup: /system.slice/sshd.service
└─164666 "sshd: /usr/bin/sshd -D [listener] 0 of 10-100 startups"
7月 08 23:41:39 bluuue systemd[1]: Starting OpenSSH Daemon...
7月 08 23:41:39 bluuue sshd[164666]: Server listening on 0.0.0.0 port 22.
7月 08 23:41:39 bluuue sshd[164666]: Server listening on :: port 22.
7月 08 23:41:39 bluuue systemd[1]: Started OpenSSH Daemon.
7月 08 23:43:24 bluuue sshd-session[164715]: Connection closed by ::1 port 49186 [preauth]
首先,ssh,即 Secure Shell,可以用于向别的服务器发起连接。而 sshd 用于接受别人的连接,这里的 d 代表 daemon,是一个在后台运行、等待请求的守护进程。
bluuue% ssh Bluuue@localhost
The authenticity of host 'localhost (::1)' can't be established.
ED25519 key fingerprint is: SHA256:TngJQJxe9DlPAJ2tTc3C5xd+Z4Az3ZxlCjT7BB+/svY
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
Bluuue@localhost's password:
[Bluuue@bluuue ~]$ ls
Desktop Documents Downloads go Music Pictures Projects Public Templates Videos
[Bluuue@bluuue ~]$ exit
logout
Connection to localhost closed.
在开启 sshd 后,就可以通过 ssh localhost 来连接自己电脑的这个服务器了。虽然没什么用,但可以看出 ssh 的语法是 ssh usrname@hostname。如果 hostname 输入的是 github.com,之后 DNS 就会将其解析为真正的 IP 地址。
bluuue% ssh-keygen -a 100 -t ed25519 -f ~/.ssh/id_ed25519
Generating public/private ed25519 key pair.
Enter passphrase for "/home/Bluuue/.ssh/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/Bluuue/.ssh/id_ed25519
Your public key has been saved in /home/Bluuue/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:6wqtQxVD1H57Xk+tYil8f/p5MimgWUQ3ZMTBYXA5L2I Bluuue@bluuue
The key's randomart image is:
+--[ED25519 256]--+
| oo. .*O+ |
| o . .+B |
| + . . + |
| . . E . . |
| . S+ o . .|
| .. .+ . . o|
| .. . .= + o = |
| .o .o o B = =|
| ..... + +oBo|
+----[SHA256]-----+
bluuue% ls ~/.ssh
id_ed25519 id_ed25519.pub known_hosts known_hosts.old
bluuue% cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEkjc1CxgEo9cO2iQrAbM3hSMhsCufuXb1GVpmEpsJZn Bluuue@bluuue
如果不想每次登录都输入一遍密码,可以生成一对密钥。只要这对密钥对得上,就可以不用密码直接登录。在这里,ssh-keygen -a 100 -t ed25519 -f ~/.ssh/id_ed25519 密钥生成器可以生成公钥 public key 和私钥 private key 一对密钥,其后面的参数分别表示生成的算法、登录时计算的次数和生成到的位置。
bluuue% ls ~ -a
Desktop .ssh Templates .zshrc
...
这里,在 linux 中,以 . 开头的文件默认隐藏,可以通过 ls -a 查看。
bluuue% ssh-copy-id -i ~/.ssh/id_ed25519 Bluuue@localhost
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/Bluuue/.ssh/id_ed25519.pub"
The authenticity of host 'bluuue (127.0.1.1)' can't be established.
ED25519 key fingerprint is: SHA256:TngJQJxe9DlPAJ2tTc3C5xd+Z4Az3ZxlCjT7BB+/svY
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:1: localhost
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already inst
alled
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the n
ew keys
Bluuue@bluuue's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh -i /home/Bluuue/.ssh/id_ed25519 'Bluuue'"
and check to make sure that only the key(s) you wanted were added.
bluuue% ssh Bluuue@localhost
[Bluuue@bluuue ~]$
bluuue% cat ~/.ssh/authorized_keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEkjc1CxgEo9cO2iQrAbM3hSMhsCufuXb1GVpmEpsJZn Bluuue@bluuue
在将公钥发给服务器后,每次在登录时服务器就会检查你发的公钥和你的私钥是否对的上,对的上就可以直接登录。
bluuue% ssh Bluuue@localhost ls
Desktop
Documents
Downloads
Music
Pictures
Projects
Public
Templates
Videos
go
bluuue% ssh Bluuue@localhost ls | wc -l
10
bluuue% ssh Bluuue@localhost 'ls | wc -l'
10
当然,在设置好密钥之后,可以直接通过 ssh 远程让服务器执行命令。需要注意的是,在语句 ssh Bluuue@localhost ls | wc -l 中,此时 ls 是在服务器上执行的,而 wc 是在本地执行的。如果想要一起在服务器上执行,可以用单引号直接将其作为字符串发过去。
bluuue% scp ignore.py Bluuue@localhost:~/Desktop/Algo/
ignore.py 100% 224 188.7KB/s 00:00
bluuue% ssh Bluuue@localhost
[Bluuue@bluuue ~]$ ls ~/Desktop/Algo/*.py
/home/Bluuue/Desktop/Algo/ignore.py
bluuue% scp Bluuue@localhost:~/Desktop/clean.sh Algo/
clean.sh 100% 184 247.1KB/s 00:00
bluuue% ls Algo/*.sh
Algo/clean.sh
bluuue% rsync ignore.py Bluuue@localhost:~/Desktop/Algo/
bluuue% ssh Bluuue@localhost
[Bluuue@bluuue ~]$ ls -l ~/Desktop/Algo/*.py
-rw-r--r-- 1 Bluuue Bluuue 224 Jul 9 00:31 /home/Bluuue/Desktop/Algo/ignore.py
ssh 最强大的不只是登录,其还可以实现文件传输和同步等操作。这里,scp 就是文件传输命令,注意特定语法 usrname@hostname:/path。
除此之外,如果对于一个 5GB 的文件,我只修改了其中 5KB 的内容,此时如果用 scp 的话每次都是重新传输一遍。而对于 rsync 命令,其不仅可以完全平替 scp,而且在这种情况下其只会修改部分内容,所以速度非常快。不仅如此,如果一个大文件传到一半网断了,此时就可以在一开始加上 --partical 参数,这样在断网时也会保留第一次传输的部分内容了。
Host me
User Bluuue
Hostname localhost
Identityfile ~/.ssh/id_ed25519
Host *.com
User usrname
bluuue% vim ~/.ssh/config
bluuue% ssh me
[Bluuue@bluuue ~]$
~/.ssh/config 是 ssh 的配置文件,如果不写的话,每次登录就得写 ssh usrname@hostname -i ~/.ssh/xxxx -p xxxx 非常麻烦。此时就可以去配置文件中给每个服务器起名字,这样之后就可以直接用这个很短的名称连接了。除此之外,在这里也可以使用通配符,表示对于任何 .com 地址都使用 usrname 作为用户名。
三、终端复用器
bluuue% tmux
bluuue% tmux ls
0: 2 windows (created Thu Jul 9 14:29:35 2026)
bluuue% tmux new -s python_session
[detached (from session python_session)]
bluuue% tmux ls
python_session: 1 windows (created Thu Jul 9 14:53:09 2026)
tmux 可以支持同时打开多个窗口。在打开后,就可以创建多个窗口,一边跑程序一边编辑了。在 tmux 里,可以使用 Ctrl+B 使用特殊按键。在按下 Ctrl+B 后松开,再按 C 是新建窗口,按数字表示去往该窗口,按 , 就是对该窗口重命名,按 w 可以列出所有窗口。除此之外,按 % 就是将两个窗口并排展示," 就是上下展示,之后再按上下左右键就可以切换了。
除此之外,也可以使用 tmux ls 查看当前所有会话。也可以用 tmux new -s name 新建指定名称的会话。
bluuue% tmux
[detached (from session 0)]
bluuue% tmux attach
bluuue% tmux attach -t python_session
[detached (from session python_session)]
在按下 Ctrl+B 后按 d 表示暂时退出,之后可以用 tmux attach 重连。这样的优点在于,如果在远程服务器上运行程序,此时断连的话远程程序会收到 SIGHUP 的信号被强制终止。此时就可以用 tmux 暂时退出,此时就可以自由和服务器断连,之后重新连接后 attach 就可以恢复之前的会话了。在自定义名称后,可以指定重连哪个会话。
四、定制终端
在之前也提到过,.zshrc 就是隐藏的 zsh 配置文件,可以通过编辑里面的内容自己配置自己的终端。比如对于一些常用的命令,可以用 alias 起别名,之后直接输入简短的别名即可。除此之外,还可以给 zsh 加一些插件,比如自动补全和错误提醒等。
五、AI in Shell
在当下,肯定可以用 ai 来加快开发和学习速度。那么与其每次将代码复制粘贴到网页里,可以直接配置 ai shell,这样就可以用命令直接操纵 ai 了。
bluuue% sudo pacman -S node npm
bluuue% sudo npm install -g @openai/codex
bluuue% codex
可以考虑用 npm 安装 code CLI,之后只需要 cd 到项目目录下,然后直接提问,codex 就能自动检查、执行甚至修改你的项目了。
六、课后练习
Arguments and Globs
Q01
You might see commands like cmd --flag -- --notaflag. The -- is a special argument that tells the program to stop parsing flags. Everything after -- is treated as a positional argument. Why might this be useful? Try running touch -- -myfile and then removing it without --.
bluuue% touch -- -data.md
bluuue% ls *.md
ls: 无效的选项 -- .
bluuue% find -maxdepth 1 -name "*.md" | xargs ls -l
-rw-r--r-- 1 Bluuue Bluuue 0 7月10日 00:03 ./-data.md
-rw-r--r-- 1 Bluuue Bluuue 26 7月 7日 11:16 ./data.md
bluuue% touch -data.md
touch: 无效的日期格式 “ata.md”
bluuue% rm -- -data.md
bluuue% find -maxdepth 1 -name "*.md" | xargs ls -l
-rw-r--r-- 1 Bluuue Bluuue 26 7月 7日 11:16 ./data.md
一般情况下,如果想要创建名称开头为 - 的文件,会被命令识别为标志 flag 从而报错。而 -- 参数可以让命令将后续的一切内容都当作参数传入,这样就可以命名以 - 开始的文件了。
需要注意的是,如果此时用通配符的话,shell 会首先将其展开。此时以 - 开头的文件就会被认错而导致报错,所以最好的方法还是不要这么命令()
Q02
**Read man ls and write an ls command that lists files in the following manner:
- **Includes all files, including hidden files
- **Sizes are listed in human readable format (e.g. 454M instead of 454279954)
- **Files are ordered by recency
- **Output is colorized
**A sample output would look like this:
-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 -l -a -h -t --color=auto
总计 316M
drwx------ 1 Bluuue Bluuue 806 7月11日 00:31 ..
drwxr-xr-x 1 Bluuue Bluuue 400 7月10日 22:49 Algo
drwxr-xr-x 1 Bluuue Bluuue 870 7月10日 21:22 .
drwxr-xr-x 1 Bluuue Bluuue 1.1K 7月10日 21:22 省赛邀请赛
drwxr-xr-x 1 Bluuue Bluuue 110 6月 1日 19:05 区域赛
-rw-r--r-- 1 Bluuue Bluuue 184 7月 8日 17:53 clean.sh
-rw-r--r-- 1 Bluuue Bluuue 224 7月 8日 16:13 ignore.py
-rw-r--r-- 1 Bluuue Bluuue 60 7月 7日 11:23 output.txt
-rw-r--r-- 1 Bluuue Bluuue 26 7月 7日 11:16 data.md
-rw-r--r-- 1 Bluuue Bluuue 63 7月 7日 11:03 slow.py
-rwxr--r-- 1 Bluuue Bluuue 124 7月 7日 10:24 arguments.sh
...
-rw-r--r-- 1 Bluuue Bluuue 50 6月15日 19:58 .directory
-h 参数可以以人类可读的大小输出,-t 参数可以按时间从新到旧排序。
Q03
Process substitution <(command) lets you use a command’s output as if it were a file. Use diff with process substitution to compare the output of printenv and export. Why are they different? (Hint: try diff <(printenv | sort) <(export | sort)).
❯ diff <( printenv | sort ) <( export | sort)
3c3
< COLORFGBG=15;0
---
> COLORFGBG='15;0'
5,6c5,6
< DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
< DEBUGINFOD_URLS=https://debuginfod.archlinux.org
---
> DBUS_SESSION_BUS_ADDRESS='unix:path=/run/user/1000/bus'
> DEBUGINFOD_URLS='https://debuginfod.archlinux.org '
...
❯ echo <(printenv)
/proc/self/fd/13
进程替换 process substitution 就是把命令的输出伪装成一个文件。对于像 diff 这样需要接受文件作为参数的命令,如果每次都是先把程序输入到一个临时文件中,用完后再删除就太麻烦了。此时 echo 就可以看到被临时写入这个文件,diff 就是接受的这个文件然后进行比较。
这里 diff 的看法就是上方诸如的 3c3 的行,表示在左侧文件的第三行和右侧文件的第三行发生了改变 change,也有可能是 a 表示增加 add 或 d 表示删除 delete。
❯ x=10
❯ printenv | grep '^x='
❯ export | grep '^x='
❯ export x
❯ printenv | grep '^x='
x=10
❯ export | grep '^x='
x=10
对于环境变量,可以发现在 export 前环境变量里是没有 x 的,只有 export 后才有。
Environment Variables
Q01
Write bash functions marco and polo that do the following: whenever you execute marco the current working directory should be saved in some manner, then when you execute polo, no matter what directory you are in, polo should cd you back to the directory where you executed marco. For ease of debugging you can write the code in a file marco.sh and (re)load the definitions to your shell by executing source marco.sh.
#!/bin/zsh
marco(){
saved_path=$(pwd)
}
polo(){
cd '$saved_path'
}
❯ source marco.sh
~/Desktop ❯ marco
❯ cd ~
❯ polo
~/Desktop ❯
需要注意的是,如果直接声明变量的话,需要用 ./march.sh 运行。此时就是在创建的子进程里声明了,压根不会带到外面。所以这里需要用函数的方法,先将函数 source 到 shell 里,然后直接执行 marco 命令,这样就是在当前进程里声明变量了。类似的,由于子进程不能更改父进程的目录,所以 polo 也需要写成函数。
Return Codes
Q01
Say you have a command that fails rarely. In order to debug it you need to capture its output but it can be time consuming to get a failure run. Write a bash script that runs the following script until it fails and captures its standard output and error streams to files and prints everything at the end. Bonus points if you can also report how many runs it took for the script to fail.
#!/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"
以下是解答:
#!/bin/zsh
count=1
while ./program.sh 1> stdout.log 2> stderr.log; do
(( count++ ))
done
echo "Program executed $count times."
echo "stdout.log: "
cat stdout.log
echo "stderr.log: "
cat stderr.log
❯ ./test.sh
Program executed 47 times.
stdout.log:
Something went wrong
stderr.log:
The error was using magic numbers
需要注意的是,判断语句 [] 内部的前后必须要有空格!
Signals and Job Control
Q01
Start a sleep 10000 job in a terminal, background it with Ctrl-Z and continue its execution with bg. Now use pgrep to find its pid and pkill to kill it without ever typing the pid itself. (Hint: use the -lf flags).
❯ sleep 100
^Z
[1] + 217028 suspended sleep 100
❯ bg
[1] + 217028 continued sleep 100
❯ pgrep -lf sleep
217028 sleep
❯ pkill sleep
[1] + 217028 terminated sleep 100
相比于先用 ps 查看进程编号再用 kill 杀死那个编号的进程,可以直接用 pgrep 查看当前正在执行的指定进程的编号,也可以用 pkill 杀死所有指定的进程。
Q02
Say you don’t want to start a process until another completes. How would you go about it? In this exercise, our limiting process will always be sleep 60 &. One way to achieve this is to use the wait command. Try launching the sleep command and having an ls wait until the background process finishes.
❯ sleep 20 &
[1] 222877
❯ wait;ls
❯ sleep 10 &
[1] 231700
❯ wait &
[2] 231707
[2] + 231707 done wait
❯ jobs
[1] + running sleep 10
wait 命令可以等待在后台运行的进程结束再执行后续的程序。需要注意的是,wait 只会等待调用其的 shell 创建的孩子进程。如果是 wait & 也让其到后台执行,由于 shell 的后台执行是默认新开一个子进程,所以这个 wait 和 sleep 就不是父子而是兄弟关系了,也就不会进行等待了。
❯ (sleep 20 && echo "sleep 20s")&
[1] 223112
❯ (sleep 30 && echo "sleep 30s")&
[2] 223133
❯ (sleep 40 && echo "sleep 40s")&
[3] 223140
❯ wait;echo hello
sleep 20s
[1] 223112 done ( sleep 20 && echo "slept 20s"; )
sleep 30s
[2] - 223133 done ( sleep 30 && echo "slept 30s"; )
sleep 40s
[3] + 223140 done ( sleep 40 && echo "slept 40s"; )
hello
在不加参数的时候,wait 默认等待所有进程结束。
❯ (sleep 10 && echo "sleep 10s")&
[1] 225141
❯ (sleep 5 && echo "sleep 5s")&
[2] 225152
❯ wait $! ; echo hello
sleep 5s
[2] + 225152 done ( sleep 5 && echo "sleep 5s"; )
hello
~/Desktop ❯ sleep 10s
[1] + 225141 done ( sleep 10 && echo "sleep 10s"; )
❯ ( sleep 10 && false )&
[1] 232759
❯ wait $!
[1] + 232759 exit 1 ( sleep 10 && false; )
❯ echo $?
1
在加了参数等待指定进程结束后,此时 wait 执行完的返回值就不是这条命令自己是否成功,而是等待的进程的返回值。
Q03
However, this strategy will fail if we start in a different bash session, since wait only works for child processes. One feature we did not discuss in the notes is that the kill command’s exit status will be zero on success and nonzero otherwise. kill -0 does not send a signal but will give a nonzero exit status if the process does not exist. Write a bash function called pidwait that takes a pid and waits until the given process completes. You should use sleep to avoid wasting CPU unnecessarily.
❯ sleep 20 &
[1] 233042
❯ kill -0 %1
❯ echo $?
0
❯ jobs
[1] + running sleep 20
如果给 kill 加上 -0 的参数,此时就不会发送任何信号,而是检查这个进程是否存在。
#!/bin/zsh
pidwait()
{
if [ $# -eq 0 ]; then
echo "Argument required."
return 1
fi
local pid=$1
while kill -0 "$pid" 2> /dev/null; do
echo "Checking if process $PID exists."
sleep 1
done
}
❯ sleep 10 &
[1] 236137
❯ polling $!
Checking if process 236137 exists.
Checking if process 236137 exists.
Checking if process 236137 exists.
Checking if process 236137 exists.
Checking if process 236137 exists.
[1] + 236137 done sleep 10
在判断完无参数的情况,可以用 local 声明局部变量,防止污染全局环境,之后直接循环即可。
Files and Permissions
Q01
(Advanced) Write a command or script to recursively find the most recently modified file in a directory. More generally, can you list all files by recency?
❯ find . -type f -printf "%TY-%Tm-%Td %TH:%TM:%TS %p\n" | sort -nr | head -n1
2026-07-14 01:46:37.0583799700 ./Algo/note/Missing_Semester/Lecture_02_Command-line Environment.md
这里可以使用 -printf 参数输出指定格式的时间,之后排序输出即可。
当然,对于这种比较复杂的代码,可以直接 codex 解决()
Terminal Multiplexers
Q01
Follow this tmux tutorial and then learn how to do some basic customizations following these steps.
# 开启鼠标支持
set -g mouse on
# 历史记录保存更多行
set -g history-limit 10000
# 从 1 开始编号 window
set -g base-index 1
# pane 从 1 开始编号
set -g pane-base-index 1
# 改前缀键为 Ctrl-a
unbind C-b
set -g prefix C-a
bind C-a send-prefix
# 设置 shell 为 zsh
set-option -g default-shell /bin/zsh
可以改成这样。
Aliases and Dotfiles
Q01
Create an alias dc that resolves to cd for when you type it wrong.
alias dc='cd'
在 ~/.zshrc 里加入这句即可,之后就可以 dc Desktop 了。
Q02
Run history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10 to get your top 10 most used commands and consider writing shorter aliases for them. Note: this works for Bash; if you’re using ZSH, use history 1 instead of just history.
❯ history 1 | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10
...
对于常用的命令,可以在查看后直接 alias。
Q03
Create a folder for your dotfiles and set up version control.
❯ mkdir ~/dotfiles
❯ cd ~/dotfiles
❯ pwd
/home/Bluuue/dotfiles
❯ git init
在 /home/Bluuue/dotfiles/.git/ 初始化空的 Git 仓库
❯ git status
位于分支 master
尚无提交
无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪)
❯ ls -a
. .. .git
首先,在创建一个文件夹后,在当前文件夹内使用 git init 初始化,之后每次可以用 git status 查看当前状态。
❯ cp ~/.zshrc .
❯ cp ~/.tmux.conf .
❯ ls -la
总计 8
drwxr-xr-x 1 Bluuue Bluuue 40 7月15日 02:28 .
drwx------ 1 Bluuue Bluuue 862 7月15日 02:28 ..
drwxr-xr-x 1 Bluuue Bluuue 82 7月15日 02:24 .git
-rw-r--r-- 1 Bluuue Bluuue 334 7月15日 02:28 .tmux.conf
-rw-r--r-- 1 Bluuue Bluuue 4007 7月15日 02:28 .zshrc
❯ git status
位于分支 master
尚无提交
未跟踪的文件:
(使用 "git add <文件>..." 以包含要提交的内容)
.tmux.conf
.zshrc
提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
❯ git add .
❯ git status
位于分支 master
尚无提交
要提交的变更:
(使用 "git rm --cached <文件>..." 以取消暂存)
新文件: .tmux.conf
新文件: .zshrc
❯ git commit -m "Initialize dotfiles"
[master(根提交) 59e7bba] Initialize dotfiles
5 files changed, 1879 insertions(+)
create mode 100644 .p10k.zsh
create mode 100644 .tmux.conf
create mode 100644 .vimrc
create mode 100644 .zprofile
create mode 100644 .zshrc
❯ git log
commit 59e7bbac1ec07800532e58657a397b77e15d060a (HEAD -> master)
Author: Bluuue <3531918730@qq.com>
Date: Wed Jul 15 02:34:05 2026 +0800
Initialize dotfiles
此时就可以将配置文件复制过来,然后使用 git add . 让 git 管理这些文件。之后使用 git commit -m "xxx" 就可以提交并保存这个版本了,-m 参数后是这次提交的说明,使用 git log 就可以查看。
Q04
Add a configuration for at least one program, e.g. your shell, with some customization (to start off, it can be something as simple as customizing your shell prompt by setting $PS1).
❯ vim ~/.vimrc
❯ cp ~/.vimrc ~/dotfiles/
❯ cd ~/dotfiles/
❯ git status
位于分支 master
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: .vimrc
修改尚未加入提交(使用 "git add" 和/或 "git commit -a")
❯ git diff
diff --git a/.vimrc b/.vimrc
index a6faff9..3be26e2 100644
--- a/.vimrc
+++ b/.vimrc
@@ -5,3 +5,20 @@ Plug 'vim-airline/vim-airline-themes'
let g:airline_theme='ayu_dark'
call plug#end()
+
+" 显示行号
+set number
+
+" 高亮语法
+syntax on
+
+" 搜索时忽略大小写
+set ignorecase
+
+" 如果搜索内容有大写,则区分大小写
+set smartcase
+
+" Tab 显示为 4 个空格
+set tabstop=4
+set shiftwidth=4
+set expandtab
❯ git add .vimrc
❯ git commit -m "Update vim configuration"
[master 4d72910] Update vim configuration
1 file changed, 17 insertions(+)
在修改完配置后,可以直接 cp 复制到仓库中,此时 git status 显示发生修改。之后在提交前先用 git diff 查看修改的部分,然后正常提交即可。
Q05
Set up a method to install your dotfiles quickly (and without manual effort) on a new machine. This can be as simple as a shell script that calls ln -s for each file, or you could use a specialized utility.
❯ ln -sf ~/dotfiles/.vimrc ~/.vimrc
❯ ls -l ~/.vimrc
lrwxrwxrwx 1 Bluuue Bluuue 28 7月15日 15:40 /home/Bluuue/.vimrc -> /home/Bluuue/dotfiles/.vimrc
如果不想每次修改完都手动复制,可以考虑使用软链接。在使用 ln -sf source file 创建一个指向 source 的 file 后,file 就相当于一个快捷方式。此时每次修改 file 时,source 也会跟着发生变化。
❯ vim ~/.vimrc
❯ cd ~/dotfiles
❯ git diff
diff --git a/.vimrc b/.vimrc
index 3be26e2..bdfc821 100644
--- a/.vimrc
+++ b/.vimrc
@@ -22,3 +22,5 @@ set smartcase
set tabstop=4
set shiftwidth=4
set expandtab
+
+
在编辑完后可以发现仓库中直接就发生了变化。
#!/bin/zsh
ln -sf ~/dotfiles/.zshrc ~/.zshrc
ln -sf ~/dotfiles/.vimrc ~/.vimrc
ln -sf ~/dotfiles/.tmux.conf ~/.tmux.conf
ln -sf ~/dotfiles/.p10k.zsh ~/.p10k.zsh
ln -sf ~/dotfiles/.zprofile ~/.zprofile
ln -sf ~/dotfiles/.gitconfig ~/.gitconfig
在创建 install.sh 后,之后每次在复制完整个仓库后就可以直接运行了。
Q06
Test your installation script on a fresh virtual machine. Migrate all of your current tool configurations to your dotfiles repository. Publish your dotfiles on GitHub.
在配好 https 或者 ssh 后,直接 git push 即可上传 github。之后在虚拟机上直接 git clone github,然后运行 install 即可。
总结
最后一部分不想搞虚拟机了就没做()

Lecture 02 Command-line Environment
浙公网安备 33010602011771号