-
本课程要求你使用类 Unix 的 Shell,如 Bash 或 ZSH 。若你在 Linux 或 macOS 上,无需额外设置。若你在 Windows 上,请确认你用的不是
cmd.exe或PowerShell;你可以使用 Windows Subsystem for Linux 或 Linux 虚拟机来获得 Unix 风格的命令行工具。要确认当前 Shell 是否合适,可运行echo $SHELL;若输出类似/bin/bash或/usr/bin/zsh,就说明没问题。 -
ls的-l选项(flag)作用是什么?运行ls -l /并观察输出。每一行最前面的 10 个字符分别代表什么?(提示:man ls) -
在命令
find ~/Downloads -type f -name "*.zip" -mtime +30中,*.zip是一个 「glob」。什么是 glob ?新建一个测试目录并创建一些文件,试试ls *.txt、ls file?.txt、ls {a,b,c}.txt等模式。参见 Bash 手册中的 Pattern Matching 。 -
'单引号'、"双引号"和$'ANSI 引号'有什么区别?写一条命令,输出一个同时包含字面量$、!和换行符的字符串。参见 Quoting 。 -
Shell 有三条标准流:stdin(0)、stdout(1)、stderr(2)。运行
ls /nonexistent /tmp,把 stdout 和 stderr 分别重定向到两个文件。你将如何把两者都重定向到同一个文件?参见 Redirections 。 -
$?保存上一条命令的退出状态(0 表示成功)。&&仅在前一条成功时执行后一条;||仅在前一条失败时执行后一条。写一个一行命令:仅当/tmp/mydir不存在时才创建它。参见 Exit Status 。 -
为什么
cd必须是 Shell 内建命令,而不能是独立程序?(提示:想想子进程能影响和不能影响父进程的哪些状态。) -
写一个脚本,接收文件名参数(
$1),用test -f或[ -f ... ]检查该文件是否存在,并根据结果输出不同提示。参见 Bash Conditional Expressions 。 -
把上一题完成的脚本保存为文件(如
check.sh)。先运行./check.sh somefile,会发生什么?然后执行chmod +x check.sh再试一次。为什么这一步是必须的?(提示:比较chmod前后的ls -l check.sh输出) -
在脚本的
set选项(flag)里加入-x会发生什么?写个简单脚本试试并观察输出。参见 The Set Builtin 。 -
写一条命令,把文件复制为带当天日期的备份文件名(例如
notes.txt→notes_2026-01-12.txt)。(提示:$(date +%Y-%m-%d))参见 Command Substitution 。 -
修改讲义中的「复现偶尔才会失败的测试」脚本(flaky test),使它能够从命令行参数接收测试命令,而不是在脚本中写死
cargo test my_test。(提示:$1或$@)参见 Special Parameters 。 -
使用管道找出你「home 目录」中最常见的 5 种文件扩展名。(提示:组合
find、grep/sed/awk、sort、uniq -c以及head) -
xargs会把 stdin 的每一行转换为命令参数。结合find和xargs(不要用find -exec),找出目录中所有.sh文件,并用wc -l统计每个文件行数。加分项:正确处理文件名中的空格。(提示:-print0和-0)参见man xargs。 -
使用
curl获取 课程网站 的 HTML,并通过grep统计列出了多少讲。(提示:找出每讲课程名称在那份 HTML 中的共性;用curl -s关闭进度输出。) -
jq是处理 JSON 的强大工具。用 curl 获取示例数据 https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json,再用 jq 提取 version 大于 6 的人员姓名。(提示:先jq. 看结构;再试jq '.[] | select(...) | .name') -
awk可以按列值过滤行并改写输出。例如,awk '$3 ~ /pattern/ {$4=""; print}'会只输出第三列匹配pattern的行,并省略第四列。请写一个awk命令:只输出第二列大于 100 的行,并交换第一列和第三列。可用这条命令测试:printf 'a 50 x\nb 150 y\nc 200 z\n' -
拆解讲义中的 SSH 日志处理管道:每一步分别做了什么?然后仿照它构建一个管道,从
~/.bash_history(或~/.zsh_history)中找出你最常使用的 Shell 命令。
-
你可能见过像
cmd --flag -- --notaflag这样的命令。这里的--是一个特殊参数,它告诉程序后面不要再继续解析选项(flag)了。也就是说,--后面的所有内容都会被当作位置参数(positional argument)。这有什么用?试着运行touch -- -myfile,然后在不使用--的情况下把它删掉。 -
阅读
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 .. -
进程替换
<(command)可以让你把一个命令的输出当成文件来用。试着配合diff和进程替换,比较printenv与export的输出。它们为什么不一样?(提示:可以试试diff <(printenv | sort) <(export | sort))
- 写两个 bash 函数
marco和polo,行为如下:每次执行marco时,都要以某种方式保存当前工作目录;之后无论你切到哪个目录,只要执行polo,它都应该把你cd回执行marco时所在的目录。为了方便调试,你可以把代码写进marco.sh,然后通过执行source marco.sh把这些定义重新加载到当前 shell。
-
假设你有一个很少失败的命令。为了调试它,你需要把它的输出保存下来,但等到一次失败运行可能会很耗时。写一个 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"
-
在终端里启动一个
sleep 10000任务,用Ctrl-Z把它挂起,再用bg让它继续在后台运行。然后使用pgrep找到它的 pid,再用pkill把它杀掉,整个过程都不要手动输入这个 pid。(提示:试试-af参数) -
假设你不想在某个进程完成之前启动另一个进程,你会怎么做?在这个练习里,限制条件始终是
sleep 60 &。一种实现方式是使用wait命令。试着启动这个sleep,然后让一个ls等到后台进程结束后再执行。不过,如果我们是在另一个 bash 会话里启动的,这个策略就失效了,因为
wait只对当前 shell 的子进程有效。讲义里还没提到的一点是:kill命令成功时退出状态为 0,失败时则非 0。kill -0不会真的发送信号,但如果进程不存在,它会返回非 0。写一个叫pidwait的 bash 函数,接收一个 pid,并一直等待到该进程结束。你应该用sleep来避免空转浪费 CPU。
- (进阶)写一个命令或脚本,递归找出某个目录中最近修改过的文件。更一般一点,你能不能按"最近修改时间"列出所有文件?
-
创建一个 alias
dc,让你把cd打错时也能正常工作。 -
运行
history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10,找出你最常用的 10 条命令,并考虑给它们写更短的 alias。注意:这条命令适用于 Bash;如果你用的是 ZSH,把history换成history 1。 -
为你的 dotfiles 建一个目录,并把它纳入版本控制。
-
至少为一个程序加入配置,比如你的 shell,并做一些定制(刚开始时,哪怕只是通过设置
$PS1来调整 shell prompt 也可以)。 -
配置一种方法,让你能在新机器上快速、自动地安装 dotfiles。这可以简单到只是写一个 shell 脚本,对每个文件执行
ln -s;也可以使用一个 专门的工具。 -
在一台全新的虚拟机上测试你的安装脚本。
-
把你当前所有工具的配置都迁移到 dotfiles 仓库里。
-
把你的 dotfiles 发布到 GitHub。
-
进入
~/.ssh/,检查你是否已经有一对 SSH 密钥。如果没有,就用ssh-keygen -a 100 -t ed25519生成一对。建议给密钥设置密码,并配合ssh-agent使用,更多信息见这里。 -
编辑
.ssh/config,加入下面这样的配置项:Host vm User username_goes_here HostName ip_goes_here IdentityFile ~/.ssh/id_ed25519 LocalForward 9999 localhost:8888 -
使用
ssh-copy-id vm把你的 SSH 公钥复制到服务器上。 -
在虚拟机里执行
python -m http.server 8888启动一个 Web 服务器。然后在你自己的机器上访问http://localhost:9999,确认自己能访问虚拟机里的这个服务。 -
运行
sudo vim /etc/ssh/sshd_config编辑 SSH 服务端配置,把PasswordAuthentication改成禁用密码认证,再把PermitRootLogin改成禁用 root 登录。然后执行sudo service sshd restart重启ssh服务,再试着重新 SSH 登录。 -
(挑战)在虚拟机里安装
mosh,并建立连接。然后断开服务器/虚拟机的网卡。mosh能不能正确恢复连接? -
(挑战)查一下
ssh里的-N和-f参数分别是什么意思,然后写出一条能够实现后台端口转发的命令。
- 在你使用的所有支持 Vim mode 的软件中启用 Vim 模式,例如编辑器和 Shell,并在接下来一个月里把所有文本编辑都用 Vim 模式完成。每当你觉得哪里低效,或者想到"肯定有更好的办法"时,就去 Google 一下,通常真的有更好的办法。
- 完成一道 VimGolf 挑战。
- 为你正在做的一个项目配置 IDE 扩展和语言服务器。确保所有预期功能都能正常工作,例如对库依赖进行跳转到定义。如果你手头没有可用于这个练习的代码,也可以从 GitHub 上找一个开源项目来用(例如这个)。
- 浏览一份 IDE 扩展列表,安装一个看起来对你有用的扩展。
-
调试排序算法:以下伪代码实现了归并排序但包含一个 bug。用你选择的语言实现它,然后使用调试器(gdb、lldb、pdb 或 IDE 的调试器)找到并修复 bug。
function merge_sort(arr): if length(arr) <= 1: return arr mid = length(arr) / 2 left = merge_sort(arr[0..mid]) right = merge_sort(arr[mid..end]) return merge(left, right) function merge(left, right): result = [] i = 0, j = 0 while i < length(left) AND j < length(right): if left[i] <= right[j]: append result, left[i] i = i + 1 else: append result, right[i] j = j + 1 append remaining elements from left and right return result测试向量:
merge_sort([3, 1, 4, 1, 5, 9, 2, 6])应返回[1, 1, 2, 3, 4, 5, 6, 9]。使用断点和单步执行 merge 函数来找到错误选择元素的位置。 -
安装
rr并使用反向调试找到一个损坏 bug。将此程序保存为corruption.c:#include <stdio.h> typedef struct { int id; int scores[3]; } Student; Student students[2]; void init() { students[0].id = 1001; students[0].scores[0] = 85; students[0].scores[1] = 92; students[0].scores[2] = 78; students[1].id = 1002; students[1].scores[0] = 90; students[1].scores[1] = 88; students[1].scores[2] = 95; } void curve_scores(int student_idx, int curve) { for (int i = 0; i < 4; i++) { students[student_idx].scores[i] += curve; } } int main() { init(); printf("=== Initial state ===\n"); printf("Student 0: id=%d\n", students[0].id); printf("Student 1: id=%d\n", students[1].id); curve_scores(0, 5); printf("\n=== After curving ===\n"); printf("Student 0: id=%d\n", students[0].id); printf("Student 1: id=%d\n", students[1].id); if (students[1].id != 1002) { printf("\nERROR: Student 1's ID was corrupted! Expected 1002, got %d\n", students[1].id); return 1; } return 0; }用
gcc -g corruption.c -o corruption编译并运行它。学生 1 的 ID 被损坏了,但损坏发生在只修改学生 0 的函数中。使用rr record ./corruption和rr replay找到罪魁祸首。在students[1].id上设置监视点,并在损坏后使用reverse-continue找到覆盖它的具体代码行。 -
使用 AddressSanitizer 调试内存错误。将此保存为
uaf.c:#include <stdlib.h> #include <string.h> #include <stdio.h> int main() { char *greeting = malloc(32); strcpy(greeting, "Hello, world!"); printf("%s\n", greeting); free(greeting); greeting[0] = 'J'; printf("%s\n", greeting); return 0; }首先不使用 sanitizer 编译并运行:
gcc uaf.c -o uaf && ./uaf。它可能看起来正常工作。现在用 AddressSanitizer 编译:gcc -fsanitize=address -g uaf.c -o uaf && ./uaf。阅读错误报告。ASan 找到了什么 bug?修复它识别的问题。 -
使用
strace(Linux)或dtruss(macOS)跟踪ls -l等命令的系统调用。它做了哪些系统调用?尝试跟踪更复杂的程序,看看它打开了哪些文件。 -
使用 LLM 帮助调试晦涩的错误消息。尝试复制编译器错误(特别是来自 C++ 模板或 Rust 的错误)并请求解释和修复。尝试将
strace或地址清理器的一些输出放入其中。
-
使用
perf stat获取你选择的程序的基本性能统计信息。不同的计数器意味着什么? -
使用
perf record进行性能分析。将此保存为slow.c:#include <math.h> #include <stdio.h> double slow_computation(int n) { double result = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < 1000; j++) { result += sin(i * j) * cos(i + j); } } return result; } int main() { double r = 0; for (int i = 0; i < 100; i++) { r += slow_computation(1000); } printf("Result: %f\n", r); return 0; }用调试符号编译:
gcc -g -O2 slow.c -o slow -lm。运行perf record -g ./slow,然后perf report查看时间花在哪里。尝试使用 flamegraph 脚本生成火焰图。 -
使用
hyperfine对同一任务的两种不同实现进行基准测试(例如find与fd,grep与ripgrep,或你自己代码的两个版本)。 -
在运行资源密集型程序时使用
htop监控系统。尝试使用taskset限制进程可以使用的 CPU:taskset --cpu-list 0,2 stress -c 3。为什么stress不使用三个 CPU? -
一个常见问题是你想要监听的端口已被另一个进程占用。学习如何发现该进程:首先执行
python -m http.server 4444在端口 4444 上启动一个最小的 Web 服务器。在单独的终端上运行ss -tlnp | grep 4444查找该进程。用kill <PID>终止它。
- 如果你之前没有使用 Git 的经验,可以尝试阅读 Pro Git 的前几章,或者完成 Learn Git Branching 等教程。在学习过程中,将 Git 命令与数据模型联系起来。
- 克隆课程网站的仓库。
- 通过将其可视化为图形来探索版本历史。
- 谁是最后修改
README.md的人?(提示:使用带参数的git log)。 - 最后一次修改
_config.yml中collections:行的提交消息是什么?(提示:使用git blame和git show)。
- 学习 Git 时的一个常见错误是提交不应由 Git 管理的大文件或添加敏感信息。尝试将文件添加到仓库中,进行一些提交,然后从历史记录中删除该文件(不仅仅是最新提交)。你可能想看看这个。
- 从 GitHub 克隆某个仓库,并修改其现有文件。执行
git stash时会发生什么?运行git log --all --oneline时你会看到什么?运行git stash pop撤销你用git stash所做的操作。在什么情况下这会有用? - 像许多命令行工具一样,Git 提供了一个配置文件(或 dotfile)
~/.gitconfig。在~/.gitconfig中创建一个别名,使你运行git graph时,获得git log --all --graph --decorate --oneline的输出。你可以通过直接编辑~/.gitconfig文件来完成,也可以使用git config命令添加别名。有关 git 别名的信息可以在这里找到。 - 你可以在
~/.gitignore_global中定义全局忽略模式,运行git config --global core.excludesfile ~/.gitignore_global。这设置了 Git 将使用的全局忽略文件的位置,但你仍需要在该路径手动创建文件。设置你的全局 gitignore 文件以忽略操作系统特定或编辑器特定的临时文件,如.DS_Store。 - Fork 课程网站的仓库,找到一个错别字或其他可以改进的地方,并在 GitHub 上提交一个 pull request(你可能想看看这个)。请只提交有用的 PR(不要给我们发垃圾邮件!)。如果你找不到可以改进的地方,可以跳过这个练习。
- 通过模拟协作场景来练习解决合并冲突:
- 用
git init创建一个新仓库,并创建一个名为recipe.txt的文件,包含几行(例如一个简单的食谱)。 - 提交它,然后创建两个分支:
git branch salty和git branch sweet。 - 在
salty分支中,修改一行(例如将 "1 cup sugar" 改为 "1 cup salt")并提交。 - 在
sweet分支中,以不同方式修改同一行(例如将 "1 cup sugar" 改为 "2 cups sugar")并提交。 - 现在切换到
master并尝试git merge salty,然后git merge sweet。会发生什么?查看recipe.txt的内容 -<<<<<<<、=======和>>>>>>>标记是什么意思? - 通过编辑文件保留你想要的内容、删除冲突标记,并用
git add和git commit(或git merge --continue)完成合并来解决冲突。或者,尝试使用git mergetool使用图形或基于终端的合并工具解决冲突。 - 使用
git log --graph --oneline可视化你刚刚创建的合并历史。
- 用
- 使用
printenv将你的环境保存到文件中,创建一个 venv,激活它,将printenv保存到另一个文件并diff before.txt after.txt。环境中发生了什么变化?为什么 shell 更喜欢 venv?(提示:查看激活前后的$PATH。)运行which deactivate并推断 deactivate bash 函数在做什么。 - 创建一个带有
pyproject.toml的 Python 包,并在虚拟环境中安装它。创建一个 lockfile 并检查它。 - 安装 Docker 并使用它在本地使用 docker compose 构建 Missing Semester 课程网站。
- 为一个简单的 Python 应用程序编写一个 Dockerfile。然后编写一个
docker-compose.yml,让你的应用程序与 Redis 缓存一起运行。 - 将 Python 包发布到 TestPyPI(除非值得分享,否则不要发布到真正的 PyPI!)。然后使用该包构建一个 Docker 镜像并推送到
ghcr.io。 - 使用 GitHub Pages 制作一个网站。额外(非)学分:用自定义域名配置它。
- 将同一编程任务分别用手动编码、AI 自动补全、内联聊天和智能体各做一次,比较体验差异。最佳选择是你正在开发的项目中的一个小功能。如果没有合适的,可以考虑在 GitHub 上做"good first issue"类型的任务,或者做 Advent of Code 或 LeetCode 上的题目。
- 用 AI 编程智能体浏览一个陌生的代码库。最好在调试或为你在意的项目添加新功能的背景下进行。如果想不出合适的,可以试试用 AI 智能体理解 opencode 中安全相关功能的工作原理。
- 从零开始氛围编程一个小应用。不要手动编写一行代码。
- 为你选择的编程智能体创建并测试一个
AGENTS.md(或等效文件,如CLAUDE.md)、一个 Skill(如 Claude Code 的 Skill 或 Codex 的 Skill),以及一个子智能体(如 Claude Code 的子智能体)。思考什么情况下会选择用其中某一种而不是另一种。注意,你选的智能体可能不支持其中某些功能;可以跳过或换一个支持的智能体试试。 - 用编程智能体完成与代码质量一讲中 Markdown 无序列表正则表达式练习相同的目标。它是通过直接编辑文件来完成的吗?智能体直接编辑文件有什么缺点和局限?想办法提示智能体不通过直接编辑文件来完成。提示:让智能体使用第一讲中提到的某个命令行工具。
- 大多数编程智能体都支持某种"YOLO 模式"(例如,在 Claude Code 中是
--dangerously-skip-permissions)。直接使用此模式并不安全,但在虚拟机或容器等隔离环境中运行编程智能体然后启用自主操作可能是可以接受的。在你的机器上搭建这样的环境。Claude Code devcontainers 或 Docker Sandboxes / Claude Code 等文档可能会派上用场。搭建方式不止一种。
-
浏览一个知名项目(例如 Redis 或 curl)的源码。找出讲义中提到的几种注释类型:有用的 TODO、指向外部文档的引用、解释为何避开某种做法的"why not"注释、或血泪教训。如果这些注释不存在,会失去什么?
-
选一个你感兴趣的开源项目,查看最近的提交历史(
git log)。找到一个能解释"为什么要改"的好提交信息,再找一个只描述"改了什么"的弱提交信息。对于后者,查看 diff(git show <hash>),尝试按照"问题 → 解决方案 → 影响"结构写一个更好的提交信息。体会一下:事后补全背景信息是多么费劲! -
对比三个 1000+ star 的 GitHub 项目的 README。它们都同样有用吗?找出哪些内容在你看来主要是噪音,为你将来写 README 提供借鉴。
-
在一个你使用的项目中找一个未解决 issue(如果有的话,可以看"good first issue"或"help wanted"标签)。根据讲义中的标准评估它:它是否尊重维护者的时间、包含所有必要信息,还是你预期维护者需要和提交者来回追问多轮才能找到根源?
-
想想你在使用的软件里遇到的一个 bug(或者在 issue tracker 中找一个)。练习创建一个最小可复现示例:剥离所有与 bug 无关的东西,只留下仍能展示问题的最小案例。写下去掉了什么以及为什么。
-
找一个你熟悉项目里已经合并的 PR,其中包含实质性的评审意见(不只是"LGTM")。通读整个评审。所有评论同样高效吗?如果你是 PR 作者,收到这些评论的体验会如何?
-
打开 Stack Overflow,找一个你熟悉技术领域内获得高票回答的问题。再找一个被关闭或大量被点踩的问题。按照讲义的建议对比它们:能否预见哪个问题更可能得到好答案?
- 为你正在进行的一个项目配置格式化器、linter 和 pre-commit 钩子。如果你有很多错误:自动格式化应当能够处理格式错误。对于 linter 错误,试着使用一个 AI agent 来修复所有 linter 错误。确保这个 AI agent 能运行 linter 并观察结果,这样它就可以在迭代循环中修复所有问题。仔细检查结果,确保 AI 没有破坏你的代码!
- 学习一种你熟悉语言的测试库,并为你正在进行的一个项目编写一个单元测试。运行代码覆盖率工具,生成 HTML 格式的覆盖率报告,并观察结果。你能找到哪些行被覆盖了吗?你的代码覆盖率很可能会很低。尝试手动编写一些测试来改进它。也试着使用一个 AI agent 来提高覆盖率;确保这个 coding agent 能以带覆盖率的方式运行测试,并生成逐行覆盖率报告,这样它才知道该聚焦在哪里。AI 生成的测试真的好吗?
- 为你正在进行的一个项目设置持续集成,并让它在每次 push 时运行。让 CI 运行格式化、lint 和测试。故意把你的代码弄坏(例如,引入一条 linter 违规),并确认 CI 能抓到它。
- 试着写一个正则表达式,并使用
grep命令行工具在你的代码中查找subprocess.Popen(..., shell=True)的出现位置。然后,试着"破坏"这个 regex 模式。semgrep 是否仍然能成功匹配到那些会让你的 grep 调用失效的危险代码? - 在你的 IDE 或文本编辑器里练习 regex 查找替换,把这些讲义中的
-Markdown 项目符号标记替换为*项目符号标记。注意,直接替换文件中所有的-字符是不正确的,因为该字符在文件中还有很多并非项目符号标记的用途。 - 写一个 regex,从形如
{"name": "Alyssa P. Hacker", "college": "MIT"}的 JSON 结构中捕获 name(例如本例中的Alyssa P. Hacker)。提示:在第一次尝试时,你可能最终会写出一个提取Alyssa P. Hacker", "college": "MIT的 regex;读一读 Python regex 文档中关于贪婪量词的内容,弄清楚如何修复它。- 让这个 regex 模式在 name 中包含
"字符时也能工作(在 JSON 中,双引号可以用\"转义)。 - 在实践中,我们 不 推荐用正则表达式解决复杂解析问题。弄清楚如何用你所使用编程语言的 JSON 解析器来完成这个任务。写一个命令行程序,从标准输入读取上述形式的 JSON 结构,并把 name 输出到标准输出。你应该只需要几行代码就能完成。在 Python 中,除了
import json之外,一行代码就可以轻松做到。
- 让这个 regex 模式在 name 中包含