Overview

本视频(Missing Semester 课程的第二讲)的核心论题是,Shell(特别是 Bash)远不止是一个简单的命令执行器,它本身就是一个功能完备且异常强大的编程环境。讲座的结论是,通过掌握 Shell 脚本的核心概念(如变量、控制流、函数)以及学会使用一系列高效的命令行工具(用于查找、搜索和导航),开发者可以将大量重复性的手动任务自动化,从而极大地提升工作效率和能力。

按照主题来梳理

第一节:Shell 脚本编程——释放 Bash 的真正力量

大多数开发者将 Shell 视为执行单个命令的地方,但它的真正潜力在于其“脚本”能力。本节深入探讨了将 Shell (Bash) 作为一种编程语言来使用的核心概念,这是实现自动化的基石。

  • 变量(Variables)

    • 在 Bash 中定义变量非常直接,使用 foo=bar 这样的语法 [01:07]。但这里有一个至关重要的“怪癖”:等号两边绝对不能有空格。

    • foo = bar [01:33](注意空格)在 Bash 中不会被解释为变量赋值。相反,Shell 会尝试执行一个名为 foo 的程序,并将其余部分(=bar)作为参数传递给它。这通常会导致“command not found”的错误。

    • 这个特性的原因是 Shell 的设计哲学:空格是用来分隔参数的。这个规则是理解 Shell 行为的基础。

    • 要访问变量的值,你需要使用美元符号 $,例如 echo $foo 将会输出 bar

  • 字符串(Strings)与引号(Quoting)

    • 在 Shell 中处理包含空格的字符串或特殊字符时,引号至关重要。Bash 主要使用两种引号:双引号 (") 和单引号 (') [02:13]。

    • 双引号 ("):这是“弱”引用。它会保留字符串中的大部分字面意义,但进行变量扩展。例如,echo "value is $foo" [02:33] 会输出 value is bar

    • 单引号 ('):这是“强”引用。它会阻止任何形式的扩展。echo 'value is $foo' [02:50] 会严格按照字面意义输出 value is $foo

    • 理解这两种引号的区别对于编写健壮的脚本至关重要,尤其是当文件名或参数本身包含空格或 $ 符号时。

  • 函数与参数(Functions & Arguments)

    • Bash 允许你定义自己的函数,这对于重用代码块非常有用。一个贯穿讲座的例子是 mcd (Make and Change Directory) 函数 [03:26],它实现了一个常见的模式:创建一个新目录,然后立刻 cd 进去。

    • 函数体可以访问传入的参数。在 Bash 中,这些参数是通过一系列特殊的“美元变量”来访问的:

      • $1, $2, $3…:分别代表第一个、第二个、第三个参数 [04:02]。在 mcd 示例中,mkdir $1cd $1 使用了第一个参数(即你希望创建的目录名)。

      • $0:代表脚本本身的名称 [05:27]。

      • $#:代表传递给脚本或函数的参数总数 [12:55]。

      • $@:代表所有参数的列表 [13:14],通常用于 for 循环中,以便迭代所有传入的参数。

      • $$:代表当前 Shell 脚本的进程 ID (Process ID) [13:01]。

      • $_:一个非常方便的变量,代表前一个命令的最后一个参数 [05:49]。例如,你可以运行 mkdir my_new_dir,然后运行 cd $_,它会自动扩展为 cd my_new_dir

  • 退出码与控制流(Exit Codes & Control Flow)

    • 这是 Shell 编程的核心心智模型。在 UNIX 哲学中,每个程序在退出时都会返回一个“退出码”(Exit Code)[07:28],这是一个整数。

    • 0 (零) 代表成功 [07:44]。

    • 任何非零值(通常是 1)代表失败或错误 [08:07]。

    • 你可以通过特殊变量 $? [05:40] 来获取前一个命令的退出码。例如,运行 grep "fubar" mcd.sh,如果找到了字符串,$? 会是 0;如果没找到,$? 会是 1 [08:07]。

    • 这个机制是 Shell 中所有控制流的基础。Bash 使用两个主要的逻辑运算符来利用退出码:&& (AND) 和 || (OR)。

    • command1 && command2 [09:32]:command2 仅在 command1 成功(即退出码为 0)时才会执行。

    • command1 || command2 [08:46]:command2 仅在 command1 失败(即退出码为非 0)时才会执行。

    • 这提供了一种极其强大和简洁的逻辑控制方式。例如,cd some_directory || exit 1 [26:05] 是一个非常健壮的模式,意思是“尝试进入那个目录,如果失败了,就立刻退出脚本”,这可以防止脚本在错误的目录中继续执行。

    • 视频中的 example.sh 脚本 [12:17] 演示了更复杂的 if 语句,它同样是基于退出码工作的:if [ $? -ne 0 ] [15:24] 就是在检查“上一个命令是否失败了?”

  • 输入/输出重定向 (I/O Redirection)

    • Shell 脚本经常需要处理命令的输出。有时你关心输出,有时你不关心。

    • 视频中的 example.sh 脚本使用 grep fubar $file > /dev/null 2>&1 [14:34] (或类似语法) 来运行一个命令,但完全不关心它的屏幕输出

    • > /dev/null 的意思是将标准输出(Standard Output)重定向到 /dev/null(一个“黑洞”设备,会丢弃所有写入的数据)。

    • 2>&1 的意思是将标准错误(Standard Error,文件描述符为 2)也重定向到标准输出(文件描述符为 1)去的地方——也就是 /dev/null

    • 这样做的唯一目的是为了获取 grep 命令的退出码($? [14:21],以便知道 fubar 是否存在于文件中,而不会让 grep 的任何输出污染屏幕。

  • 命令替换与进程替换 (Substitution)

    • 命令替换 (Command Substitution)$(...) [10:16](或反引号 ``)。这允许你将一个命令的输出(一个字符串)“嵌入”到另一个命令中,或赋值给一个变量。

      • foo=$(pwd) [10:24]:运行 pwd 命令,将其输出(如 /home/user/docs)赋值给变量 foo

      • echo "We are in $(pwd)" [10:51]:直接在字符串中嵌入命令的输出。

    • 进程替换 (Process Substitution)< (...) [11:13]。这是一个更高级但也极其有用的功能。它不会替换为字符串,而是替换为一个临时文件路径(技术上是一个文件描述符),该文件的内容是括号内命令的输出。

    • 这对于那些期望接收文件路径作为参数、而不是从标准输入接收数据的命令(如 diff)非常有用。

    • 示例:diff <(ls foo) <(ls bar) [21:53]。这个命令会分别执行 ls fools bar,将它们的输出存入两个临时文件,然后 diff 会比较这两个文件的内容,告诉你 foo 目录和 bar 目录的文件列表有何不同。

  • Globbing (文件名扩展)

    • 这是 Shell 提供的一种模式匹配,用于快速选择文件。它不是正则表达式(Regex),但功能相似。

    • * (星号):匹配任意零个或多个字符 [18:29]。ls *.txt 会列出所有 .txt 文件。

    • ? (问号):匹配任意一个字符 [19:09]。ls project_?.md 会匹配 project_1.mdproject_a.md,但不会匹配 project_10.md

    • {} (花括号扩展):这是最强大的功能之一。它会创建出所有可能的组合。

      • mv image.{png,jpg} [20:01] 会被 Shell 扩展为 mv image.png image.jpg

      • touch {foo,bar}/{a,b,c}.txt [20:49](视频中有类似演示)会创建 foo/a.txt, foo/b.txt, foo/c.txt, bar/a.txt, bar/b.txt, bar/c.txt,这是一个笛卡尔积。

  • Shebang (#!) 与脚本可移植性

    • 当你编写一个脚本(无论是 Bash、Python 还是其他语言)并希望它能像普通命令一样被执行时,你需要在文件的第一行添加一个 “Shebang”。

    • 例如 #!/bin/python [23:23]。这行告诉操作系统:“不要用 Shell 来执行这个文件,请用 /bin/python 这个解释器来执行它”。

    • 然而,不同系统上的 Python 路径可能不同(可能是 /usr/bin/python/usr/local/bin/python)。

    • 更健壮、可移植性更高的方法是使用 #!/usr/bin/env python [24:16]。env 是一个标准程序,它会根据你的 PATH 环境变量 [24:40] 自动去查找 python 解释器所在的位置。这使得你的脚本在几乎所有 UNIX 类系统上都能正确运行。

  • 脚本检查 (Linting)

    • Bash 语法非常古怪且容易出错(比如空格问题)。

    • 讲座推荐了一个名为 shellcheck [25:29] 的工具。它是一个静态分析器(linter),可以检查你的 Bash 脚本,找出常见的语法错误、逻辑陷阱和不良实践,并给出修改建议。

第二节:Shell 工具箱——高效开发者的利器

掌握了脚本编程后,讲座的第二部分转向了日常工作中用于提高效率的“工具”。这些工具的核心思想是:你永远不应该手动去做计算机可以为你做的重复性工作

  • 查找帮助 (Getting Help)

    • man (Manual Pages) [28:27]:这是 UNIX 系统的内置手册。man ls 会显示 ls 命令的所有选项和详细说明。这是最权威、最完整的文档,但往往非常冗长。

    • tldr (Too Long; Didn’t Read) [29:51]:这是一个社区驱动的工具(需要单独安装),它为命令提供了“简明扼要的”使用示例。当你忘记 tar 命令如何解压,或者 ffmpeg 如何转换视频时 [29:43],tldr tar 会直接给你几个最常用的例子,非常实用。

  • 查找文件(按名称、路径或元数据)

    • find [31:16]:这是最经典、功能最强大的文件查找工具。它会递归地遍历目录结构。

      • find . -name "src" -type d [31:36]:在当前目录(.)下,查找所有名称为 src 且类型为目录(d)的文件/文件夹。

      • find . -path "*test*.py" [32:05]:按完整路径匹配模式(例如,查找所有路径中包含 test.py 文件)。

      • find . -mtime -1 [32:41]:查找在过去 24 小时内(-1)被修改过的文件。

    • find 最强大的地方在于它的 -exec 动作 [33:18]:find . -name "*.tmp" -exec rm {} \;。这个命令会找到所有 .tmp 文件,并对每一个找到的结果({})执行 rm 命令。这是将“查找”和“操作”结合起来的典范。

    • fd [34:25]:一个现代的 find 替代品。它通常更快,语法更简洁(例如 fd "src"),并且默认会尊重 .gitignore 规则,这在代码库中查找时非常方便。

    • locate [35:23]:它不直接搜索磁盘,而是搜索一个预先建立的“索引”数据库。因此 locate 的速度极快,但它可能找不到你刚刚创建的文件,因为它依赖于 updatedb [36:05] 进程(通常每晚运行)来更新索引。

  • 查找内容(在文件中搜索文本)

    • grep (Global Regular Expression Print) [36:24]:UNIX 的标准文本搜索工具。

    • grep -R "fubar" . [36:43]:-R 标志让 grep 递归地(Recursive)搜索当前目录(.)下的所有文件,查找包含 “fubar” 字符串的行。

    • ripgrep (rg) [37:52]:一个现代的 grep 替代品。它被认为是目前最快的文本搜索工具之一。

    • rg 的优点包括:默认递归、默认尊重 .gitignore [38:08]、彩色高亮输出、更快的速度。

    • rg 有很多有用的标志,例如 -C 5--context=5)[38:26] 会显示匹配行以及它“上下的 5 行”上下文,这在阅读代码时非常有用。

    • 它甚至可以做反向搜索,例如 rg -L "shebang" [39:28](-L 对应 --files-without-match)会列出所有包含 “shebang” 字符串的文件。

  • 查找历史(复用过去的命令)

    • 上箭头 (Up-Arrow) [41:21]:最基本的方式,效率低下。

    • history [41:37]:显示你所有的历史命令。一个常见的用法是 history | grep "docker" [42:00],用 grep 来搜索你运行过的所有 docker 命令。

    • Ctrl+R [42:22]:(reverse-i-search) 这是一个内置的、交互式的“反向搜索”功能。按下 Ctrl+R 后,你可以开始输入命令的任何部分(例如 convert),它会立刻从历史记录中找到最近的匹配项。

    • fzf (Fuzzy Finder) [42:52]:一个改变游戏规则的工具。它可以与 Ctrl+R 绑定 [43:20],将标准的 Ctrl+R 替换为一个极其强大的“模糊查找”界面。你只需要输入几个零散的字符(例如 cvrt icon),它就能“模糊”地匹配到你想要的命令(如 convert image.png icon.ico),即使你输入的字符是无序的 [43:47]。

    • 历史子字符串搜索 (History Substring Search) [44:08]:许多 Shell(如 Zsh)支持的另一个功能。你只需输入命令的前几个字符(例如 git c),然后按“上箭头”,Shell 会自动只在你输入的前缀(git c)匹配的历史记录中循环,(例如 git commit, git checkout)。

  • 查找目录(高效导航)

    • tree [45:32]:以树状结构清晰地显示目录层级。

    • broot [45:55] / ranger:基于终端的文件管理器。它们提供了一个交互式界面,允许你使用箭头键在目录中“漫游”,预览文件,并执行操作 [46:46]。

    • autojump [48:01](或 zfasd 等工具):这是一种“智能”的 cd。它会学习你最常最近(“frecency”)访问的目录。你不再需要输入 cd ~/Documents/Projects/my-app/src/main,而只需输入 j my-appjautojump 的命令),它就会自动“跳转”到那个你最常去的 my-app 目录。


框架 & 心智模型 (Framework & Mindset)

框架一:将 Shell 视为一个组合式的编程环境

视频中展示的第一个核心心智模型是停止将 Shell 视为孤立命令的执行者,而是将其视为一个用于“组合”的编程环境。UNIX 哲学的核心是“做一件事并把它做好”(Do One Thing and Do It Well)。grep 只负责搜索,find 只负责查找,sort 只负责排序。Shell 的魔力在于它提供了将这些简单工具“粘合”在一起的机制,以创建出极其复杂的自动化工作流。

这个框架的粘合剂包括:

  1. 管道 (|):这是最主要的粘合剂。它将一个命令的“标准输出”连接到下一个命令的“标准输入”。history | grep "docker" | wc -l 是一个完美示例:history 的完整列表被“导入” grepgrep 过滤后的结果被“导入” wc -l(字数统计)以计算行数。你动态地创建了一个“统计 docker 命令数量”的新程序。

  2. 退出码 ($?) 与逻辑运算符 (&&, ||):这提供了“逻辑”和“控制”。它们是 Shell 的 if-then-else。你不必总是编写一个完整的 .sh 脚本文件。git pull && npm install [09:32](一个常见的例子)就是一个微型脚本,它在命令行上定义了逻辑:“只有在 git pull 成功后,才运行 npm install”。cd my-dir || echo "Directory not found" [08:46] 也是如此:“尝试 cd,如果失败了,就打印一条错误消息”。

  3. 变量与替换 ($foo, $(...)):这提供了“状态”和“数据传递”。通过 foo=$(pwd) [10:24],你将一个命令的动态输出(当前目录)捕获到了一个“内存”(变量 foo)中,以便稍后在脚本的其他地方(例如 echo "Building in $foo")重用它。

当你内化了这种思维,你就不会再害怕复杂的命令行。相反,你会开始主动地将你的任务分解为“我需要什么数据(find)?”、“我需要如何处理它(grep, sed)?”、“我需要如何组合它们(|)?”以及“我需要什么逻辑(&&)?”。

框架二:高效开发者的“查找与自动化”心智模型

视频的第二部分(关于工具)提供了一个隐含的框架,即如何解决“我正在手动做一件重复的事”。这个心智模型的目标是:将你的时间从“执行”任务转变为“自动化”任务

这个框架可以被看作一个分层递进的查找策略:

  1. 查找“如何做”:mantldr

    • 当你在任务开始时,不确定使用什么命令,或者不确定命令的正确语法时(例如“我如何解压这个 .tar.gz 文件?”),你的第一反应不应该是打开浏览器搜索。

    • 第一层:tldr tar [29:51]。在 5 秒内,你将看到最常见的 3-5 个用例(例如 tar -xzvf ...)并解决你的问题。

    • 第二层:man tar [28:27]。如果你需要一个非常规的选项(例如“排除特定目录”),man 手册是最终的、最完整的答案。

  2. 查找“在哪里”:find, fd, locate

    • 当你面对一个庞大的代码库或文件系统,需要找到特定的“某个东西”时(例如“那个 config.xml 文件到底在哪?”)。

    • 第一层:locate config.xml [35:23]。如果它是一个你没有刚创建的文件,locate 会在 1 秒内给你答案。

    • 第二层:fd config.xml [34:25]。在代码库中,fd 会快速、智能地(忽略 node_modules)找到它。

    • 第三层:find . -name "config.xml" [31:36]。find 是最强大的,它允许你添加更复杂的逻辑,如按大小(-size)、修改时间(-mtime)[32:33] 或权限来过滤。

  3. 查找“它说了什么”:greprg

    • 当你找到了文件(或目录),但你需要知道“哪个文件包含了特定的 API 密钥”或“这个函数在哪里被调用了?”

    • 第一层:rg "functionName" [37:52]。ripgrep 会快速(默认递归、并行)搜索所有文件,并以高亮、带上下文 [38:26] 和行号的方式显示所有匹配项。

    • 第二层:grep -R "functionName" . [36:43]。greprg 的通用(但较慢)版本,在所有系统上都可用。

  4. 查找“我以前做过”:Ctrl+Rfzf

    • 当你意识到“我上周刚运行过那个复杂的 docker build 命令,但我忘了参数……”

    • 第一层:Ctrl+R [42:22]。输入 dockerbuild,它会立即调出最近的匹配项。

    • 第二层:fzf [42:52]。如果你有 fzf,你可以更“模糊”地搜索(dckr bld)[43:47],并从一个可滚动的列表中交互式地选择你想要的那个命令。

最终的自动化(The Final Step)

这个框架的最后一步是,当你发现自己组合了上述工具来完成一个任务(例如,find 某些文件,然后 grep 它们的内容,最后用一个 sed 命令来修改它们)——并且你发现自己第二次在做这件事时——你就应该将这个命令流封装到一个可重用的 Shell 脚本(使用第一节中的技术)中,并将其放入你的 PATH。

这就是从一个“Shell 用户”转变为一个“Shell 程序员”的完整闭环。