Learning Hub
These courses were created by
RayZh,
TOmorrowArc1,
PhantomPhoenix, and
me, 
        while serving as TAs for Programming 2025.
        All exercises and materials are open-sourced at
github.com/acm-dojo.
The curriculum includes beginner-friendly crash courses on shell and git. Every lesson and challenge is built from scratch by our team to help you learn by doing.
See available course materials below:
Course Selection
Git Learning
5 modules · 10 lessons
You have completed the Shell training, and I believe you now have a deeper understanding of the command line! Now, we are going to learn the version control system Git.
You might ask: Isn’t a version control system just a piece of software? Why do we need to go to great lengths to learn it? In fact, version control systems play a crucial role in modern software development. By learning Git, you will master how to effectively manage codebases, handle conflicts, and collaborate with team members.
We will only cover the basics of Git. In fact, the content we teach is basically sufficient to cover all your needs from now until your sophomore year; however, Git is a very powerful and complex tool, far beyond this, and you will encounter more usage scenarios in future development. We hope that Git Dojo will allow you not to panic when you encounter problems in the future, understand how Git works, and know how to consult documentation and find solutions.
There are 5 modules in this course, click the buttons below to navigate between them.
欢迎来到 Git 学习之旅的第一站!在深入学习 Git 的强大功能之前,我们需要先为你的开发环境进行基础配置。我已经让你们在课前安装好了 Git,你们在课上也略微了解了一些 Git 的基本概念。
在这个模块中,你将学习如何配置 Git 的基本设置,包括设置你的用户名和邮箱地址,这样每次提交代码时,Git 都能准确记录是谁做出了这些更改。
在这个教程中我假定你已经安装好了 Git。如果你没有安装,我推荐 Windows 用户在 WSL 中安装(以避免经典的CRLF 和 LF 换行符问题),使用你们在上一个 Shell 教程中学到的 sudo apt install git。如果是 Mac 用户,可以先安装 Homebrew 然后使用 brew install git。
现在,它的目的主要是来解决你们课上在运行 git commit -m 时它报的错误,你在当时可能看到它是这样说的
Author identity unknown
*** Please tell me who you are.
Run
  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"
to set your account's default identity.
Omit --global to set the identity only in this repository.显然,对于一些学习能力强的人来说他们看到报错可能已经去问过AI来解决问题了,不过我在这里还是想再讲一下:它到底干了什么,然后顺便再提一些 git 中别的配置。
在这里,我想强调的是,Git 的配置是非常重要的,尤其是用户信息的配置。通过 git config 命令,你可以设置许多选项,包括用户的姓名和电子邮件地址。这些信息将会被 Git 用来标识每个提交的作者。
例如,运行我课上讲的 git log 命令,你可能会看到如下类似的东西
commit 9fceb02c0ee4b1a5c0e4b1a5c0ee4b1a5c0ee4b1a5
Author: Your Name <you@example.com>
Date:   Mon Sep 28 12:00:00 2021 +0800
    Your commit message这里的 Author 字段就是从你通过 git config 设置的信息中获取的。如果你没有正确配置这些信息,Git 将无法识别你的身份,这可能会导致提交记录中缺少作者信息,影响团队协作和代码追踪。怎么配置?很简单,Git 在上面都教你了
git config --global user.name "Your Name"
git config --global user.email "you@example.com"--global 选项表示这些配置将应用于你系统上的所有 Git 仓库。如果你想为某个特定的仓库设置不同的用户信息,可以在该仓库目录下运行相同的命令,但不加 --global 选项。记得一定不要粘贴上面的命令中的 Your Name 和 you@example.com,而是要替换成你自己的信息。
除了用户信息,git config 还允许你设置其他许多选项。例如,你可以配置 Git 的颜色输出,使其在终端中更易读
git config --global color.ui auto此外,Git 还支持配置别名,还记得我之前教的 Bash 别名吗?Git 也有类似的功能。刘祎禹学长在他的博客 ↗中提到了一些有用的 Git 别名,例如
- 
unstage:相当于git reset HEAD --,用于取消暂存区的更改
 bashgit config --global alias.unstage 'reset HEAD --'
- 
full-pull:相当于git pull --recurse-submodules,用于拉取包含子模块的仓库
 bashgit config --global alias.full-pull 'pull --recurse-submodules'
… 诸如此类的别名可以极大地提高你的工作效率,建议根据自己的习惯进行配置。
最后,所有这些配置都会被存储在一个名为 .gitconfig 的文件中,位于你的用户主目录下。你可以直接编辑这个文件来查看或修改你的配置。
你可以使用以下命令来查看当前的 Git 配置
git config --list这将显示所有当前的配置选项及其值。如果你只配置了用户名和邮箱地址,你应该会看到类似如下的输出
[user]
    name = Your Name
    email = you@example.com本章不设置挑战。作为阅读章节即可。如果你真能通过某些手段在本关正确拿到 flag 并提交,第一位正确的提交者将会获得一杯奶茶的奖励。
既然你已经完成了 Git 的基础配置,现在是时候学习如何在本地环境中创建和管理你的第一个 Git 仓库了。这个模块将带你深入了解 Git 的核心工作流程,从创建仓库到追踪和提交更改。
在这里,你将学会如何将一个普通的文件夹转变为受 Git 版本控制的仓库,理解什么是暂存区以及它在 Git 工作流程中的重要作用。你将掌握如何精确地选择要提交的更改,如何编写有意义的提交信息,以及如何查看项目的当前状态。
这些本地操作技能是所有 Git 工作的基础。无论你将来是独自开发项目还是与团队协作,理解如何在本地环境中有效管理代码更改都是必不可少的。通过这个模块的学习,你将建立起对 Git 工作原理的直观理解,为后续学习远程仓库协作打下坚实的基础。
在开始使用 Git 之前,你需要先初始化一个 Git 仓库。
让我们从最基础的命令开始:git init!
什么是 Git 仓库?#
Git 仓库(repository)是一个包含项目所有文件和版本历史的目录。它就像一个时光机,可以让你:
- 追踪文件的每一次修改
- 回到任何历史版本
- 与其他开发者协作
- 管理不同的开发分支
每个 Git 仓库都有一个隐藏的 .git 目录,这里存储着所有的版本信息和配置。还记得吗?你可以用 ls -la 来查看隐藏文件。
git init 命令#
git init 是创建新 Git 仓库的命令。它会在当前目录下创建一个新的 Git 仓库。
基本用法#
# 在当前目录初始化 Git 仓库
git init
# 创建新目录并初始化 Git 仓库
git init my-projectTIP 运行
git init后,你会看到一条消息:“Initialized empty Git repository in /path/to/your/directory/.git/” 不要慌张,这只是告诉你当前目录是空的,并且已经成功初始化了一个 Git 仓库。
验证仓库初始化#
初始化仓库后,让我们验证一下是否成功:
# 查看隐藏的 .git 目录
ls -la
# 检查 Git 状态
git statusls -la 命令会显示所有文件,包括隐藏文件。你应该能看到一个 .git 目录。
我们后续会详细介绍 git status 命令的输出内容,但现在你只需要知道它可以告诉你当前仓库的状态。在新初始化的仓库中,你会看到类似这样的输出:
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track).git 目录是 Git 的核心,包含了:
- HEAD: 指向当前分支的指针
- config: 仓库的配置信息
- objects/: 存储所有的 Git 对象(commits, trees, blobs)
- refs/: 存储分支和标签的引用
⚠️ 警告: 永远不要手动修改
.git目录中的文件!这可能会损坏你的仓库。
让我们创建一个简单的项目并初始化 Git 仓库:
# 创建新的项目目录
mkdir my-first-repo
cd my-first-repo
# 初始化 Git 仓库
git init
# 创建一个简单的文件
echo "# My First Git Repository" > README.md
# 查看状态
git status运行 git status 会显示 README.md 是一个未跟踪的文件:
On branch main
No commits yet
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        README.md
nothing added to commit but untracked files present (use "git add" to track)初始化选项#
git init 命令还有一些有用的选项:
# 指定初始分支名称
git init --initial-branch=main
# 或简写为
git init -b mainTIP 从 Git 2.28 开始,你可以配置默认分支名称,避免每次都指定。
你可能会问…#
Q: 我可以在已有文件的目录中运行 git init 吗?
A: 当然可以!git init 不会删除任何现有文件,它只会创建 .git 目录。
Q: 如果我在错误的目录运行了 git init 怎么办?
A: 你可以简单地删除 .git 目录:rm -rf .git
RECAP#
在本章中,你学习了 Git 版本控制的第一步:初始化仓库。
核心概念#
- Git 仓库: 包含项目文件和版本历史的目录
- .git目录: 存储所有 Git 数据的隐藏目录
- 分布式版本控制: 每个仓库都是完整的项目副本
关键命令#
- git init: 在当前目录初始化新的 Git 仓库
- git init <目录名>: 创建新目录并初始化 Git 仓库
- git init -b <分支名>: 初始化仓库并指定初始分支名称
命令速查表#
- git init: 初始化当前目录为 Git 仓库
- git init my-project: 创建并初始化新项目
- ls -la: 查看包括- .git在内的所有文件
一些 Tips#
- 初始化仓库后会创建 .git目录
- 不要手动修改 .git目录中的文件,会变得不幸!
任务卡#
现在轮到你亲手初始化第一个仓库了。我们已经把环境收拾干净,请按顺序完成以下步骤:
- 在 Home 目录下创建目录 dojo-init,并初始化其为 Git 仓库,让默认分支名就是我们习惯使用的 main。
如果你已经忘了 Home 目录是什么,请你看一下你学过的 Shell Dojo,或者问一问 AI。
- 创建一个 README.md,首行写上# dojo-init
- 用 git status验证一下仓库里只有一个未跟踪文件。
- 上述都准备好后运行 /challenge/submit,验证你的仓库配置是否正确。
做到这里,你就已经完成了 Git 学习旅程中的第一小步!
将修改后的代码提交到 Git 仓库需要几步?
使用 git add 和 git commit让  Git 跟踪你的所有修改。
理解 Git 的三个区域#
在学习 git add 和 git commit 之前,必须先理解 Git 管理文件的三个核心区域:
- 工作区 (Working Directory): 用户在电脑上能直接看到和编辑的项目文件夹。
- 暂存区 (Staging Area / Index): 一个临时的缓冲区,记录下一次准备提交的文件快照。
- 版本库 (Repository): .git目录,永久存储了项目所有版本历史的地方。
git add 将工作区的更改放入暂存区,而 git commit 则将暂存区的内容打包存入版本库。
git add 命令#
git add 命令将工作区中某个文件或目录的当前更改放入暂存区,包含在下一次提交中,让用户能够精确控制每次提交的内容。
使用场景#
- 跟踪新文件: 在 Git 工作区中新建一个文件时,其状态为 Untracked,只有使用git add <file>后 Git 才会开始追踪并管理该文件。
 bash# 创建一个新文件 echo "Hello, Git!" > README.md # 查看状态,会提示 README.md 是未跟踪文件(Untracked files) git status # 使用 git add 开始跟踪它 git add README.md # 再次查看状态,会提示 README.md 已被暂存,准备提交(Changes to be committed) git status
- 暂存已修改文件: 修改已被 Git 跟踪的文件后,使用 git add <filename>将修改添加到暂存区。
 bash# 修改 README.md echo "Add a new line." >> README.md # 查看状态,会提示 README.md 有未暂存的修改(Changes not staged for commit) git status # 将修改添加到暂存区 git add README.md # 再次查看状态,提示修改已暂存 git status
基本用法#
# 添加指定文件:
git add <file>
# 添加指定目录下的所有更改:
git add src/
# 添加当前目录所有更改:
git add .
# 交互式添加: 使用 `-p` 或 `--patch` 标志,Git 会逐一展示文件中的每一处修改(称为 "hunk"),让你决定是否要暂存它。
git add -p
git commit 命令#
将暂存区中的内容打包,生成一个永久的版本记录并存入版本库,同时通过commit message为本次提交附加说明。简言之,每次提交都相当于项目历史中的一个“存档点”。
基本用法#
# 使用 `-m` (message) 标志:通过命令行直接提供信息
git commit -m "feat: Add README.md"
# 打开文本编辑器编写信息:如果不带参数,Git 会自动打开配置的默认文本编辑器(如 Nano、Vim),让用户编写更详细的提交信息。
git commit
commit message#
一次好的提交应只处理一个问题,而一个好的提交信息应该清晰地说明本次提交“做了什么”以及“为什么这么做”。
良好实践 (Conventional Commits 规范):
一个有所简化的标准的提交信息格式如下:
<类型>: <简短描述>
[可选的正文]- 类型 (Type): 如 feat(新功能),fix(修复bug),docs(文档),style(格式),refactor(重构),test(测试),chore(构建或杂务)。
- 简短描述: 50个字符以内,清晰概括本次提交。
- 正文 (Body): 可选,正文必须起始于描述字段结束的一个空行后,每行不超过72个字符,并可以使用空行分隔不同段落。提交的正文内容自由编写。
例子:
git commit -m "fix: Remove null pointer dereference in user login"更具体的规范可见: Conventional Commits 规范 ↗
RECAP#
核心概念#
工作区 -> 暂存区 -> 版本库: 这是 Git 提交的核心流程。
git add: 将选定的工作区中修改放进暂存区。
git commit: 将暂存区中所有修改打包并附上信息,永久地存入版本库历史中。
关键命令#
- git add <file>: 将指定文件的更改添加到暂存区。
- git add <directory/>: 将指定目录下的所有更改添加到暂存区。
- git add .: 将当前目录下所有更改添加到暂存区。
- git add -p: 交互式地选择要暂存的修改部分。
- git commit -m "message": 将暂存区内容附上简短信息,并提交到版本库。
- git commit: 打开编辑器编写详细的提交信息。
任务卡#
进入仓库~/dojo-add-commit,仓库已初始化,文件结构如下:
include/
  book.hpp
  user.hpp
src/
  main.cpp
  book/
    book1.cpp
    book2.cpp
  user/
    user.cpp
CMakeLists.txt请使用 git add 命令将工作目录下所有修改过的文件加入暂存区,并提交以下文件:
- include/book.hpp
- src/book/book1.cpp
- src/book/book2.cpp
只允许进行一次提交,且其它文件不能出现本次提交中。提交完成后运行 /challenge/submit 检查你的结果是否符合要求。
项目里有100个文件,忘了自己修改过哪些应该怎么办?
可以使用 git status 与 git diff 查看项目情况。
git status 命令#
git status 能快速输出当前项目的状态:有哪些文件被修改了?有哪些文件已暂存?位于哪个分支?本地分支与远程分支是否同步?(分支相关内容详情见branch一节)
基本用法#
# 该命令会向输出流中打印项目状态
git status输出解读#
一个典型的 git status 输出包含关于当前分支用户需要的所有信息:
# 1. 项目当前所在分支
On branch main
# 2. 本地分支与远程分支的关系
# "Your branch is up to date with 'origin/main'." -> 本地与远程同步,一切正常。
# "Your branch is ahead of 'origin/main' by X commits." -> 本地有X个新提交尚未推送到远程。
# "Your branch is behind 'origin/main' by Y commits." -> 远程有Y个新提交需要拉取到本地。
Your branch is up to date with 'origin/main'.
# 3. 已暂存的更改(位于Staging Area)
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   user.cpp
# 4. 已修改但未暂存的更改
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   book.cpp
                book.hpp
# 5. 未被 Git 跟踪的新文件
Untracked files:
  (use "git add <file>..." to include in what will be committed)
            logger.hppgit diff 命令#
git status显示状态信息的最小单位是文件,而 git diff 可以精确地输出文件具体哪一行、哪个字符发生了变化。
基本用法#
git diff 是一个多功能的比较工具,使用时需要指定比较的对象。
- 
比较“工作区”与“暂存区”: 这是 git diff最常见的用法。它会显示所有已修改但未暂存的改动。
 bash# 查看所有未暂存的修改 git diff # 只看特定文件的未暂存修改 git diff book.hpp
- 
比较“暂存区”与“版本库”: 使用 --staged标志,可以查看已暂存但未提交的改动。
 bashgit diff --staged
- 
比较“工作区”与“版本库”: 比较你当前工作区的所有修改(无论是否暂存)与最近一次提交的版本有何不同。 
 bashgit diff HEAD
- 
其他: 事实上,只要一个项目状态可以被 Git引用(无论是通过分支名、标签名、提交哈希,还是通过工作区/暂存区这些特殊位置),git diff就能计算出它与另一个可引用状态之间的差异。同时通过选项可以将git diff比较的粒度调整到字符等更细的层次。
TIP:
diff本身就是一个命令,可以在本地通过该命令比较两个文件的不同,实际上git diff是对diff命令的封装。
输出解读#
diff --git a/book.cpp b/book.cpp
index 6b9195f..8b832a2 100644
--- a/book.cpp
+++ b/book.cpp
@@ -1,4 +1,4 @@
-  #include<bits/stdc++.h>;
+  #include<stdint.h>;
;- --- a/book.cpp: 代表修改前的源文件 (- a文件)。
- +++ b/book.cpp: 代表修改后的目标文件 (- b文件)。
- @@ ... @@: 指明修改发生在文件的哪几行。
- -(减号): 表示这一行从源文件中被删除了。
- +(加号): 表示这一行在目标文件中是新增的。
TIPS:使用
q键退出diff查看界面。
RECAP#
核心概念#
- git status: 输出项目的整体状态。
- git diff: 输出文件的具体变化。
- git diff比较不同的区域:- git diff通过调整flag可以灵活比较 Git 三大区域(工作区、暂存区、版本库)之间的差异。
关键命令#
- git status: 查看项目当前状态。
- git diff: 查看未暂存的修改。
- git diff --staged: 查看已暂存、待提交的修改。
- git diff HEAD: 查看所有未提交的修改(包含已暂存和未暂存)。
任务卡#
进入仓库~/dojo-status,调整仓库到如果使用 git status 命令会输出如下结果(忽略日期和提示,只需保证每个文件在对应的git分区内即可):
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/main.cpp
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   include/user.hpp
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        CMakeLists.txt调整后运行 /challenge/submit 检测仓库状态。
掌握了本地仓库的管理技能后,现在你将步入 Git 最强大的领域之一:远程协作。这个模块将教会你如何与托管在 GitHub、GitLab 等平台上的远程仓库进行交互,真正体验现代软件开发的协作方式。
你将学习如何从远程位置克隆现有的项目到本地,这是参与开源项目或加入新团队时最常见的第一步操作。更重要的是,你将掌握本地仓库与远程仓库之间的同步机制,学会如何将你的本地更改推送到远程仓库与他人分享,以及如何从远程仓库拉取其他开发者的更新。
> **注意**:本模块的两个教程是有先后顺序的, 请务必按照顺序完成. 如果你遇到了问题, 重启不会清除你在 `/home/hacker` 的操作. 你可以 `rm -rf` 重新来过.
你有没有尝试过去部署别人的项目? 开源世界有着相当多优质的项目; 而在本机上使用它们的第一步往往就是使用 git clone 命令将它们克隆到本地.
在本挑战中, 我们将学习如何使用 git clone 命令来克隆远程仓库.
Git clone 的基本语法是 git clone <repository-url>, 其中 <repository-url> 填写你想要克隆的远程仓库的 URL.
如果你想要克隆的仓库在 GitHub 上, 你可以在该仓库的主页上找到绿色的 “Code” 按钮, 点击它会显示出仓库的 URL. 你可以选择使用 HTTPS 或 SSH 的 URL.
git clone https://github.com/RayZh-hs/neofrp.git执行上述命令后, 你会在当前目录下看到一个名为 neofrp 的文件夹, 里面包含了该仓库的所有内容.
如果你想要使用 SSH, 你需要确保你的本地机器已经配置了 SSH key 并将其添加到你的 GitHub 账户中. 如果你不知道怎么做, 可以参阅 GitHub 的官方文档 ↗. 这一块本教程不做要求.
git clone 命令提供了一些可选参数. 常用的有:
- git clone <remote-url> <new_path>可以指定克隆到的目录名, 而不是使用默认的仓库名.
- git clone --recursive如果仓库中包含子模块 (git 仓库中的 git 子仓库), 你可以使用这个选项来同时克隆所有子模块.
- git clone -b <branch-name> <remote-url>如果你只想克隆某个特定分支 (branch), 可以使用这个选项. (分支在后面会提到)
在这个挑战中, 你需要克隆我们准备的一个远程仓库 (点击下面的链接访问 GitHub 页面):
将它克隆在你的主目录 /home/hacker 下, 不要重命名, 然后运行 /challenge/submit 命令提交你的结果.
在使用 Git 进行多人合作的时候, 许多时候我们需要将自己的进度和他人共享, 或者获取他人的新进展. 这时候, 我们就需要使用 git fetch 和 git pull 等命令来实现本地仓库与远程仓库的同步.
git fetch 命令用于从远程仓库获取最新的更新. 它会将远程仓库的数据下载到你的本地, 但不会自动合并或修改你当前的工作.
# 从名为 origin 的远程仓库获取更新
$ git fetch origin如果命令行没有任何输出, 说明你的本地仓库已经是最新的. 否则, 你可能会看到类似下面的输出:
From https://github.com/acm-dojo/sudo
    44bc5d6..3737415  main       -> origin/main这表示远程仓库 origin 的 main 分支有了新的提交. origin/main 是一个远程跟踪分支, 它反映了远程仓库 main 分支在你上次 fetch 时的状态.
要将这些获取到的更新合并到你当前的本地分支, 你需要接着运行 git merge:
# 将 origin/main 分支合并到当前分支
git merge origin/main在 Git 中, 远程和你的本地的操作都以分支 (branch) 形式存储. 你可以使用
git merge来合并它们. 这在后续的教程中会详细说明.
为了简化这个两步过程, Git 提供了 git pull 命令. 它相当于 git fetch 和 git merge 的组合:
# 等同于 git fetch origin && git merge origin/main
git pull origin main如果你当前的分支已经设置了上游分支 (upstream branch), 你可以省略远程仓库和分支名称, 直接运行 git pull.
你克隆下的仓库默认会有一个名为
origin的远程仓库, 以及一个名为main的主分支. 因此, 你可以省略参数, 直接运行git pull.
有 git pull 获取远程的更新, 当然也就有 git push 将你的本地更新推送到远程仓库. 它和 git pull 是相反的过程.
git push origin main同理, 在有上游分支的情况下, 你也可以省略远程仓库和分支名称.
上一个挑战中, 你已经克隆了一个远程仓库 ~/git-sample-repo. 现在, 这个远程仓库有了新的更新. 你的任务是用将这些更新同步到你的本地仓库.
在更新后的仓库中, README.md 文件的内容已经被修改. 请根据它的提示, 完成本次挑战.
提示: 在做任何操作前, 请先确认你的工作目录在
~/git-sample-repo下.
在掌握了本地仓库管理和远程协作的基础技能后,现在是时候学习一些能够让你成为更专业开发者的 Git 高级技巧了。这个模块将教会你三个在实际项目中不可或缺的重要概念,它们将极大提升你的开发效率和代码质量。
你将学习如何使用 `.gitignore` 文件来精确控制哪些文件应该被版本控制系统追踪,避免将临时文件、构建产物或敏感信息意外提交到仓库中。接下来,你将掌握分支的强大功能,学会如何同时开发多个功能特性而不相互干扰,这是现代软件开发团队必备的工作方式。
最后,你将学会如何利用 Git 作为"时光机"的能力,通过重置和回退操作来灵活地管理项目历史。这些技能建立在你之前学到的提交、推送、拉取等操作基础之上。
在讲解了 Git 的基本配置后,我们接下来要讨论的是分支管理。分支是 Git 中一个非常强大的特性,它允许你在不影响主干代码的情况下进行开发。类比的说,如果仓库是“时光机”,那么分支就是“平行宇宙的时间线”。你可以在一个分支上开发新功能,同时主分支保持稳定,等新功能开发完成并经过测试后,再将其合并回主分支。
在 Git 中,创建和管理分支非常简单。你可以使用以下命令来创建一个新分支:
git branch <branch-name>然后,切换到新分支:
git checkout <branch-name>有没有“只做一次”的脚本来切换分支呢?有的兄弟,有的:
git checkout -b <branch-name>这条命令会同时创建并切换到新分支。你可以在新分支上进行开发,提交更改,就像在主分支上一样。所有之前你学到的命令,git add、git commit、git status,都可以在分支上使用。
当你完成了在分支上的开发,并且确认代码没有问题后,你可以将分支合并回主分支。合并回主分支这件事你既可以在本地完成,也可以在远程仓库中通过 Pull Request(PR)来完成。这里我们先介绍本地合并的方式:
git checkout main  # 切换回主分支
git merge <branch-name>  # 将指定分支合并到主分支有时候,合并分支可能会遇到冲突(conflicts)。如果你同时在两个分支上修改了同一份代码(这在多人合作中事实上很常见!),那么到底是保留你的更改,还是保留另外一个人的更改?这时候需要你手动解决冲突,我们把这个话题留到 “Oh Shit, Git” 模块中详细讲解,毕竟它较为头疼。
我们再来提一嘴分支的命名规范。和 commit message 一样,为了让团队协作更加顺畅,建议大家在创建分支时遵循一定的命名规范。例如,可以使用以下格式:
feature/<feature-name>  # 新功能
bugfix/<bug-description>  # Bug 修复
hotfix/<hotfix-description>  # 紧急修复我之前还说了 “你可以在 GitHub 上创建 Pull Request(PR)来合并分支”。通过 PR 来合并分支事实上它不止能干“合并”的事,它更重要的作用是一种协作方式,它允许你在合并代码之前,先对代码进行审查和讨论。通过 PR,团队成员可以查看你的更改,提出意见,甚至直接在你的分支上进行修改。如果你要贡献代码到一些大项目中,你一般来说只能通过 PR 来贡献代码,因为你没有他们代码库的写入权限。PR 在 Merge 意义上其实 Nothing Special。你在创建 PR 的状态栏上应该能看到类似这样的东西:

左边是你要合并的分支,右边是目标分支。这与你本地的操作是类似的。
最后,我还要介绍一下 git stash 命令。当然,你们现在不用掌握它,不过知道一下总归是好的。当你在运行 git checkout 切换分支时,如果工作区有未提交的更改,Git 会阻止你切换分支。这时你可以使用 git stash 命令将未提交的更改暂存起来,等切换到目标分支后再恢复。在你们看到这里的时候,大概率助教已经给你们讲了 栈 (Stack) 这个数据结构~~(好吧可能他也只讲了讲STL的栈怎么用吧)~~。事实上,git stash 就是利用栈的特性来保存和恢复工作区的状态。具体而言:
git stash push -m "some unsaved work" # 缓存这些未提交的更改
git checkout some_new_branch # 切换到新分支
...
# 当你新分支上的工作完成后
git checkout previous_branch # 切换回之前的分支
git stash pop # 看!这就是栈!push 进去的东西 pop 出来RECAP#
在本章中,你学习了如何利用分支隔离工作并安全地合并回主干。
核心概念#
- 分支可以理解成平行时间线:在不打扰 main的情况下开发新功能或修复问题。
- 分支命名规范:用 feature/、bugfix/等前缀,类似于 commit message 的规范。
- 临时存储工作区:git stash用栈的方式保存未提交的改动,方便切换分支。
关键命令#
- git branch <branch-name>:创建新分支
- git checkout <branch-name>:切换到指定分支
- git checkout -b <branch-name>:创建并切换分支
- git merge <branch-name>:把目标分支合并进当前分支
- git stash push -m "<note>"/- git stash pop:暂存并恢复未提交更改
命令速查表#
- git branch feature/ui:新建- feature/ui分支
- git checkout -b feature/ui:新建并切换到- feature/ui
- git checkout feature/ui:进入该分支继续开发
- git checkout main && git merge feature/ui:返回- main并完成合并
- git stash push -m "WIP logs":保存未完成的工作以便切换
- git stash list/- git stash pop:查看并取回暂存的更改
一些 Tips#
- 合并前记得回到目标分支(通常是 main)。
- 分支上的提交和主分支完全一样,放心使用 git add/git commit。
任务卡#
现在轮到你来实践一次分支协作流程了!我们已经在 ~/dojo-branch 中准备好一个仓库,其中 main 分支上有一份日常进度日志,但第三天的记录还在草稿状态,且目前保持在未提交的工作区里。
请按照下面的步骤完成练习:
进入目录:cd ~/dojo-branch,创建并切换到新分支,名字叫 feature/progress-log。
打开 progress.md,新增一行 Day 3: learned branch.。使用 git add 和 git commit 提交更改,提交信息为 branch: ACM Dojo。
运行 /challenge/submit 检查结果。
人非圣贤, 孰能无过? 版本控制的很大一个用处就是: 在你 (或者你的队友) 犯错时, 帮助你的项目回到正确的状态.
在这节课程中, 你将学习使用 git reset 和 git revert 来撤销错误的提交.
Git Reset 的使用方法#
让我们回忆一下 Git 的三大区域:
- 工作区 (Working Directory): 你正在编辑的文件所在的地方, 就是你的文件系统管理的目录.
- 暂存区 (Staging Area): 你准备提交的文件所在区域, 这是 Git 管理的.
- 本地仓库 (Local Repository): 你本地的 Git 仓库, 存储所有的提交历史. 其中 HEAD 指向当前分支的最新提交.
git reset 的操作, 就是基于之前的历史记录调整这些区域的信息.
git reset 有三个常用的模式: --soft, --mixed (默认), 和 --hard. 它们的共同特点是接受一个 commit 作为参数, 并将当前分支的 HEAD 指向该 commit. 比如:
git reset d1977cfba62fadd89e2f2f292256866d4da286a5
git reset HEAD (回退到 HEAD 自身, 也就是当前 commit)
git reset HEAD~1 (回退到 HEAD 的上一个提交, 也就是倒数第二个 commit)其中, 参数的缺省值是 HEAD, 也就是默认对于 HEAD 指针不改变.
三种模式的区别在于它们对暂存区和工作区的影响.
Soft 模式#
--soft 是 “温柔的” reset 模式. 它只会改变 HEAD 指针, 而对于暂存区和工作区都不做任何修改. 也就是说, 你回退到某个 commit 后, 该 commit 之后的所有更改都会被保留在暂存区和工作区中.
一个常见的用例是当你发现自己误打了 commit, 于是想回到 “执行 commit 之前一瞬间” 的状态, 你就可以使用:
git reset --soft HEAD~1这会将 HEAD 回退一个 commit, 然后之后的所有操作都会被保留下来移动到暂存区 (因为你 commit 的是暂存区的内容). 你当前工作区的操作也不会被抹除.
Mixed 模式#
--mixed 是 git reset 的默认模式. 它会改变 HEAD 指针, 并且将暂存区的内容重置为指定 commit 的状态, 但是不会修改工作区. 也就是说, 你回退到某个 commit 后, 该 commit 之后的所有更改会被保留在工作区中, 但是不再保留在暂存区.
如果你执行了
git reset HEAD~1那么你也会回退到上一个 commit, 但是该 commit 之后的更改会被保留在工作区中, 不再存储在暂存区.
利用这个性质, 我们可以使用这个模式来 “unstage” 一些或全部文件:
git reset    # 相当于 git reset --mixed HEAD: 将暂存区所有文件都会移到工作区
git reset file1 file2  # 只将指定的文件从暂存区移到工作区Hard 模式#
--hard 是 “强硬的” reset 模式. 它会改变 HEAD 指针, 并且将暂存区和工作区都重置为指定 commit 的状态. 也就是说, 你回退到某个 commit 后, 该 commit 之后的所有更改都会被抹除, 无论是在暂存区还是工作区.
⚠️ 警告: 使用 --hard 模式会丢失所有未提交的更改, 包括工作区和暂存区的更改, 并且无法恢复. 请谨慎使用.
通常, 如果你想丢弃你从上一个 commit 以来你在工作区和暂存区的所有更改, 你可以使用:
git reset --hard HEAD如果你想不保留痕迹地回到某个 commit, 你可以使用:
git reset --hard <commit-hash>Git Revert: 以进为退的安全手段#
在单人项目中, git reset 是一个非常有用的工具, 但是在多人协作的项目中, 它可能会引起混乱. 因为 git reset 会改变提交历史, 这会导致其他协作者的仓库与远程仓库不一致.
例如, 如果你做了
git reset --hard HEAD~1, 你将不能将这个操作 push 到 remote 仓库. 如果你强制使用git push --force这么做, 就会导致其他协作者的仓库出现问题.
为了避免这种情况, 我们可以使用 git revert 来撤销错误的提交. git revert 不会改变提交历史, 而是创建一个新的 commit 来撤销指定的 commit. 这样, 其他协作者的仓库不会受到影响.
git revert 同样接受一个参数 <commit>, 用来指示要撤销到的 commit, 使用方法和 git reset 类似:
git revert <commit-hash>
git revert HEAD~1任务卡#
我们在 /home/hacker 目录下创建了一个 git-reset 仓库, 请你完成以下任务:
- 回退到倒数第二个 commit 的状态, 但请不要清空该状态之后的更改. 我们把这个 commit 记为 x;
- 新建一个 commit, 消息任意: 提交在 x之后对于文件grimm.md的更改, 并丢弃对于其他文件的更改.
- 运行 /challenge/submit来获取/flag.
在实际开发中,即使是最有经验的开发者也会遇到各种意外情况。这个最终模块将教会你如何优雅地处理那些让人头疼的 Git 问题,让你在面对"Oh Shit"的时刻能够冷静应对。
你们如果是因为标题吸引进来的,跳过了前面的介绍部分,建议先回去看看前面的内容。这里我要讲一些黑魔法,来减少你们 “删掉仓库重开” 的频率。
在这一章节我主要介绍:如果你在 Commit 之后发现问题(比如你不小心把密钥提交上去,又或者说你想要修改刚刚的 Commit 信息),你该怎么办?你显然不太希望别人看到你的错误,新建一个 Commit 是掩耳盗铃,毕竟 Git 是时光机。
修改最后一次提交#
如果你刚刚提交了代码,但发现了一些小问题,比如拼写错误、忘记添加文件,或者想要修改提交信息,你可以使用:
git commit --amend这个命令会打开文本编辑器让你修改提交信息,取决于你的配置,默认应该是 Vim。
如果你还想添加一些文件(或者修改一些文件)到这次提交中,你可以在运行 git commit --amend 之前先将文件添加到暂存区:
git add forgotten_file.txt
git commit --amend这样,forgotten_file.txt 会被添加到上一次的提交中。
以下内容不需要掌握,但是了解一下总没有坏处。你事实上有可能在以后用到它们。
撤销提交但保留更改#
如果你想撤销最后一次提交,但保留文件的修改:
git reset --soft HEAD~1这样提交就被撤销了,但你的更改还在暂存区中。
完全撤销提交和更改#
如果你想完全回到上一次提交的状态:
git reset --hard HEAD~1⚠️ 警告:这会永久删除你的更改,请谨慎使用!
修改历史提交信息#
如果你需要修改更早的提交信息,可以使用交互式 rebase:
git rebase -i HEAD~n其中 n 是你想要回溯的提交数量。这会打开一个文本编辑器,列出最近的 n 次提交。你可以将 pick 改为 reword 来修改提交信息,或者 edit 来修改提交内容。
在最后我还是要提一嘴,如果你已经把错误的提交推送到远程仓库,并且其他人可能已经基于这些提交进行了工作,修改历史会导致问题。 因为你事实上不是简单的“修改了提交内容”,而是“创建了新的提交替换了旧的提交”,还记得 Git 的提交总有 “PARENT” 这回事吗?如果其他人基于错误的 PARENT 进行了工作,你修改了 PARENT 之后,他们的工作就会变得不可用。
RECAP#
在本章中,你学习了如何在不丢失工作成果的情况下修补提交历史。
核心概念#
- git commit --amend:重写最新一次提交的内容和信息。
- git reset变体:- --soft/- --hard分别回滚提交但保留或丢弃工作区改动。
- 历史重写的影响:每次重写都会生成新的提交 ID,需要与团队同步!改改自己的历史就算了,千万不能改别人的!
命令速查表#
- git commit --amend:修正刚刚的提交信息或内容。
- git add README.md && git commit --amend:把漏掉的文件(这里是README.md)补进上一条提交。
- git reset --soft HEAD~1:撤销提交但保留暂存区。
- git reset --hard HEAD~1:彻底回滚到上一版本。
一些 Tips#
- 先把修正内容 git add再--amend,否则不会被包含。
- git reset --hard会删除工作区改动,务必确认无关键信息遗失。
- 修改历史会改变提交哈希,推送前确保没有人基于旧提交继续开发。这真的很重要,重要的事情说三遍!
任务卡#
我们在 ~/dojo-amend 中准备好了一个只有一条提交的小仓库。那次提交的信息写得太含糊,而且 README 也不好。请按照下面的步骤修复它:
打开 README.md,把内容改成 # ACM Dojo。并且把提交信息改成 feat: I love dojo。
确保仓库依旧只有一条提交、工作区干干净净,然后运行 /challenge/submit 验证结果。
在上一章节的 “Branch” 里,我们学习了如何创建和切换分支,以及如何合并分支。不知道各位还是否记得,合并分支可能会遇到冲突(conflicts)?如果你同时在两个分支上修改了同一份代码(这在多人合作中事实上很常见!),那么到底是保留你的更改,还是保留另外一个人的更改?这时候需要你手动解决冲突。
什么是合并冲突?#
Git 足够智能,可以自动合并很多情况(比如你在文件 A 的第 10 行做了修改,而另一个分支在文件 B 的第 50 行做了修改)。但当两个分支都对文件 A 的第 10 行进行修改时,Git 不知道该听谁的,它就会停下来,把决定权交给你。
当 Git 无法自动合并两个分支的更改时,就会发生合并冲突。这通常发生在:
- 同上面所说,两个分支修改了同一个文件的同一行
- 一个分支删除了文件,而另一个分支修改了该文件
识别冲突#
当发生冲突时,Git 会在文件中插入冲突标记:
<<<<<<< HEAD
你当前分支的代码
=======
另一个分支的代码
>>>>>>> branch-name事实上,冲突解决并不复杂,各位应该对它祛魅,解决过一次就会发现它其实并没有那么可怕。
各位可以在自己的终端里面随便新建一个文件夹试一试,我把命令贴在下面,逐行运行,你们现在应该已经能看懂这些命令了:
mkdir conflict-demo && cd conflict-demo
git init
echo "version=1" > app.txt
git add . && git commit -m "init"
# 在 test 分支修改同一行
git switch -c test
echo "version=2-from-test" > app.txt
git commit -am "test: update version"
# 回到 main,做不同修改
git switch main
echo "version=2-from-main" > app.txt
git commit -am "main: update version"
# 合并产生冲突
git merge test你们应该会看到类似下面的输出:
Auto-merging app.txt
CONFLICT (content): Merge conflict in app.txt
Automatic merge failed; fix conflicts and then commit the result.当你打开 app.txt 文件时,你会看到:
<<<<<<< HEAD
version=2-from-main
=======
version=2-from-test
>>>>>>> test解决冲突的步骤#
- 
查看冲突文件 
 bashgit status
- 
编辑冲突文件 - 删除冲突标记(<<<<<<<,=======,>>>>>>>)
- 保留你想要的代码
- 或者合并两个版本的代码
 
- 删除冲突标记(
- 
标记冲突已解决 
 bashgit add <文件名>
- 
完成合并 
 bashgit commit
还有一些其他「小技巧」:
「走为上计」:如果你觉得冲突太复杂,想要放弃合并,可以使用
git merge --abort来取消合并操作,回到合并前的状态。「言听计从」:如果你决定保留别人的更改,可以使用
git merge -X theirs ...;这里-X theirs表示在冲突时倾向于对方分支的改动,...是你要合并的分支名。「固执己见」:如果你决定保留自己的更改,可以使用
git merge -X ours ...;这里-X ours表示在冲突时倾向于当前分支的改动,...是你要合并的分支名。以上内容来源于 刘祎禹学长的博客 ↗,他取的这三个名字实在是太好了,所以我也搬过来了🤣。
RECAP#
在本章中,你学习了如何识别并解决合并冲突,让两条时间线重新汇合。
核心概念#
- 当 Git 无法自动决定同一位置的最终内容时,它会请求你介入。
- 冲突标记:<<<<<<<、=======、>>>>>>>展示当前分支与目标分支的差异。
- 遇到冲突不要慌张:检查文件、删除标记、保留正确内容、重新提交。
- 必要时可以中止合并,或使用 ours/theirs 等策略快速决策。
命令速查表#
- git status:在有冲突时,可以列出冲突文件清单
- git diff --merge:查看冲突双方的差异
- git merge --abort:出现混乱时快速撤退 🏳️
一些 Tips#
- 打开冲突文件时先读懂标记,明确哪部分来自 HEAD、哪部分来自对方。
- 完成编辑后务必删除所有冲突标记,避免把 <<<<<<<之类提交进历史。
- 记得提交你已经解决冲突后的文件哦!并且,提交前可以再次运行 git status,确认没有未解决的冲突或遗留文件。
以下内容不需要掌握,作为阅读材料即可。
你们在合并两个分支时,你还有可能看到 Git 如下的报错,然后说什么 --ff-only, --no-ff,--squash 之类的选项。别慌,以防你碰巧碰到了它们,我在这里也解释一下。你们可能看到这样的东西:
$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.这个提示会在以下情况出现:
- 
你本地的当前分支(比如 main)有了一些新的提交。 
- 
与此同时,远程仓库的同一个分支 (origin/main) 也有了别人推送的、你本地没有的新提交。 
- 
这时,你的本地分支和远程分支就“分叉(Diverged)”了。 
Git 上面提到的 --ff-only, --no-ff,--squash 是“合并策略”。这些策略决定的是历史记录的结构,而不是代码内容的合并方式。代码内容的合并方式就是我们讲的那些东西。
- fast-forward:历史线性,但看不见合并点。
- —no-ff:保留合并节点,便于回溯特性分支。
- squash merge:把分支压成一次提交,整洁但丢失中间历史。
- rebase vs merge:
- rebase:历史更线性;不要对已共享分支重写历史。
- merge:保留自然历史;可能更“分叉”,但安全。
 
任务卡#
我们已经在 ~/dojo-conflict 中准备了一份“正在撰写的小说”。main 分支和 feature/story 分支分别对第三章做了不同修改。当你尝试把特性分支合并回 main 时,一定会遇到冲突。
请进入练习仓库:cd ~/dojo-conflict,执行 git merge feature/story,观察 Git 报出的冲突信息。
打开 story.txt,消除冲突标记,把内容整理为:
    Chapter 1: Arrival     Chapter 2: Tension builds     Chapter 3: Main timeline update.     Chapter 3 (feature): Alternate timeline reveal.     Chapter 4: Cliffhanger    
使用 git add story.txt 标记冲突已解决,并通过 git commit 创建合并提交(默认的 Merge 信息即可)。
确认工作区干净、没有残留的冲突标记后,运行 /challenge/submit 验证结果。
Shell Learning
6 modules · 14 lessons
Welcome to the Shell training ground!
In today’s world of beautiful graphical interfaces, you might wonder why we still need to learn the command line. The answer is simple: the shell is one of the most powerful tools a developer can master. It allows you to automate tasks, manage files efficiently, and access tools that don’t have graphical interfaces.
This course is designed to bridge the gap in shell usage and help you become a more efficient and confident developer. We’ll cover the fundamentals that will serve you throughout your programming journey, from basic navigation to advanced piping and package management.
We’ll only cover the basics of shell usage - but these basics are enough to cover most of your needs from now through your sophomore year. The shell is a very powerful and complex tool with much more to explore. More importantly, we hope Shell Dojo will help you stay calm when encountering problems in the future, understand how the shell works, and know how to consult documentation and find solutions.
There are 6 modules in this course, click the buttons below to navigate between them.
Welcome to Shell
Welcome to the Shell learning journey! Before diving into the powerful features of the command line, we'll introduce you to what the shell is and why it matters.
In this module, you will learn the fundamental concepts of the shell, understand why command-line interfaces are still relevant in the modern era, and get familiar with the basic structure of how we'll be learning throughout this course.
在图形界面尚不普及的年代, 人们通过我们称之为 命令行(command line) 的文本界面与计算机交互. 用户输入命令, 这些命令会先由 shell 解释, 然后再转发给底层操作系统.
你可能会好奇: 在 GUI (图形化界面)已经如此普及的今天, 我们为什么还需要学习 shell?
首先, 作为计算机专业的学生, 我们经常需要使用只能通过命令行访问的底层工具. 其次, GUI 往往在批量、自动化任务上不如 CLI 高效, 这就是为什么资深用户更偏爱 CLI(命令行界面)工具的原因.
对许多开发者来说, shell 是一门艺术. 掌握 shell 的使用将显著提升你的效率, 并加深你对计算机系统的理解. 这正是我们开发这门速成课程的原因:
弥合 shell 使用上的差距, 帮助你成为一名更高效更自信的开发者.
在本教程中, 你将学到:
- 文件的读写及权限管理
- 命令行的基本操作
- 管道与相关操作
- 软件包及其管理
当然, shell 的极限远不止这些,更多的操作空间留给诸位自己探索.
本教程将会以关卡的形式展现,当你学习新的 shell 使用技巧或者 CLI 工具后,你需要运用所学的内容去解决挑战,它可能是打开某个已知的文件,亦或是正确地执行某些命令.最后,你会获得 flag, 它长这个样子:
flag{...}将它复制到浏览器下方的 “标志” 位置以解锁下一关.
有些关卡有全屏的教程 (就像你现在在阅读的这个); 它会在你登录时自动打开. 如果你不小心退出了, 可以通过运行 /challenge/tutorial 命令重新进入.
在这个入门关卡里, 你的 flag 是:
[flag]祝一切顺利, 愿 shell 常伴你左右.
终端的快捷键很多, 虽然你不需要掌握一切快捷键, 但适当使用它们能让你的操作更加顺滑. 在这一个挑战中, 我们会列举终端中常见的快捷键和杂项指令, 帮助你在 Shell Dojo 中获得更好的体验.
⚠️ 由于技术限制, 我们在网站上无法复刻所有终端里的操作. 在不一样的地方我们也会标出, 以免你感到困惑.
🚨 因此, 我们更建议你打开一个 WSL 试试看.
网页上的复制粘贴#
在网页上, 当我们左键拖拽选中一段文字时, 这段文字会自动复制到剪贴板. 你可以用 Ctrl+Shift+V 粘贴. 注意不要使用 Ctrl+V, 因为它会在终端里输入一个特殊字符.
清屏#
- clear: 当你的终端被输出刷屏时, 你可以输入- clear清屏. 🚨 这个功能希望大家掌握!
在 Windows CMD 中, 这个指令叫做
cls(Clear Screen).
常用编辑/导航快捷键(Readline, bash/zsh 通用)#
- Ctrl+U: 删除光标到行首
- Ctrl+W: 删除光标前一个词
- Ctrl+L: 清屏 (和 clear不一样的是, 终端的历史还在. 你可以向上滚动滚轮查看)
- Alt+B / Alt+F: 以“词”为单位后退/前进 (很多终端里可以用 Ctrl + ←/→实现)
- Ctrl+Shift+C/V: 复制/粘贴 (很多时候右键也可以复制粘贴, 但是这个因终端而异)
⚠️ 由于技术限制, 网页上无法使用这些指令. 请在 WSL 中尝试.
历史搜索与自动补全#
- 上/下方向键: 历史命令上一条/下一条
- Ctrl+R: 反向增量搜索历史 (重复按继续匹配)
- Ctrl+G: 退出历史搜索
- Tab: 自动补全 (文件名, 命令名等) 🚨 这个功能非常重要! 希望大家多使用.
进程与控制#
- Ctrl+C: 发送 SIGINT, 也就是强制中断当前运行的进程
🚨 这个指令非常重要! 你可以用它来中断一个运行时间过长的命令, 或者一个进入死循环的程序.
以上就是常见的快捷键和杂项指令. 希望你能多加练习, 以提升你的效率!
[flag]Snooping Around
Now that you understand what the shell is, it's time to learn how to navigate through your file system. This module will teach you the fundamental skills of moving around directories and manipulating files and folders from the command line.
You'll learn how to read and explore your file system, how to create and organize directories, and how to create and modify files. These navigation and file manipulation skills form the foundation of all command-line work.
在 Linux 中有一个很基本的观点: 一切皆文件 (Everything is a file). 掌握了文件系统的操作, 在 Shell 中就有了立足之地.
因此大部分教授 Shell 技巧的教程都会从浏览文件系统开始, 本教程也不例外.
作为文件系统的第一节, 本节会介绍 pwd, ls, cd 命令.
pwd: 显示当前工作目录#
每时每刻, 某个 Shell 中的用户都有一个位置. 我们将其称为当前工作目录 (CWD, Current Working Directory), 表示 “你现在在哪里”. 我们可以用 pwd (Print Working Directory) 命令显示它.
在终端输入:
pwd输出: /home/hacker, 这意味着你现在的工作目录是 /home/hacker.
值得一提的是, 这里的路径通常以 /... 的形态出现. 在 Linux 中, / 是一个特殊的目录, 它是所有文件和文件夹的根目录 (Root Directory).
我们从根目录开始, 先进入 home 文件夹, 再进入 hacker 文件夹, 这样就到达了 /home/hacker. 这样的路径被称为绝对路径.
你可以将绝对路径想象成 Windows 资源管理器上方显示的那一串路径. 和 Windows 不同的是, Linux 的路径分隔符是 /, 而不是 \.
ls: 列出目录内容#
命令 ls (List) 可以用来告诉你 “某个目录里都有什么东西”. 它接受一个路径参数, 输出该路径对应文件夹内的内容. 比如, 你如果想要看根目录下的所有文件和文件夹, 你可以输入:
ls /这个路径参数是可选的. 如果不写路径, ls 会默认列出当前工作目录的内容.
ls 这个命令有很多参数, 这里列举几个常用的:
- -a: 显示所有文件, 包括隐藏文件 (以- .开头的文件)
- -l: 以长列表的形式显示文件的详细信息 (权限, 所有者, 大小, 修改时间等)
打开 wsl, 试试运行 ls -al. 你看到了什么?
你会发现, 在所有文件夹里有两个特殊的 “文件” . 和 .. 出现在列表中. 它们分别表示当前目录和上一级目录.
例如, 你可以使用 ls .. 来查看上一级目录的内容, 用 ./main 来指示当前目录下的 main 文件.
事实上, 作为路径, 不加任何前缀的
main很多时候也能指示当前目录的main文件. 但是在某些情况下, 比如你想要执行当前目录下的main文件时, 你必须要在终端中输入./main才能成功执行. 在大多数 shell 的默认设置中,PATH不包含当前目录.; 因此执行当前目录下的可执行文件需要显式写./main, 否则会得到command not found.
cd: 切换目录#
如果我们需要改变自己的当前位置, 我们需要 cd (Change Directory) 命令.
cd 命令接受一个路径参数. 如果该路径存在, 那么你的当前工作目录就会被切换到你输入的路径. 如果你直接输入 cd 而不带任何参数, 那么你的工作目录会被切换到你的主目录 (Home Directory).
主目录通常位于 /home/用户名 下, 代表着某个用户的个人空间. 你可以把它想象成 Windows 系统中的 “C:\Users\用户名”. 一般我们会将所有自己的文件都放在主目录下.
你在终端左侧提示栏中看到的 ~ 符号, 就是主目录的简写. cd、cd ~ 和 cd /home/用户名 是等价的.
~ $ ls
file-1 folder-1 folder-2
~ $ cd folder-1
~/folder-1 $ pwd
/home/用户名/folder-1
~/folder-1 $ cd ../folder-2
~/folder-2 $ pwd
/home/用户名/folder-2在上一个任务中, 你学习了如何在文件系统中导航. 从这个挑战开始, 你会学到如何对于文件系统作出改变.
touch: 创建空文件#
~ $ touch fish
touch: cannot touch 'fish': Permission denied在 Linux 中, touch 命令用于创建空文件. 因此, touch fish 不是摸鱼, 而是在当前目录创建一个名为 fish 的空文件. 当然, 若文件已经存在, 它并不会被复写, 而是更新已有文件的修改时间戳.
你可以用 ls 来验证文件是否创建成功.
~ $ ls
some-file
~ $ touch fish
~ $ ls
fish  some-filenano: 命令行中的文本编辑器#
touch 很好, 但是除了在脚本中我们并不怎么用到它. 为什么呢? 因为我们创建文件通常是想在文件中写点东西, 而使用重定向等手段 (之后会讲到) 来写文件并不方便.
- 
有没有什么软件可以在命令行中方便地编辑文本呢? 
- 
有的, 你可以使用 nano.
nano 是一款以命令行界面为基础的文本编辑器. 它非常轻量, 易于上手, 是 Linux 系统中最常见的文本编辑器之一. 你的 WSL 系统中也自带了 nano.
使用 nano 只需要在终端输入 nano <filename> 即可. 如果 <filename> 对应的文件不存在, nano 会自动帮你创建一个新文件. 那你可能会想, 既然 nano 可以创建文件, 那 touch 还有什么用呢?
touch并不鸡肋——在脚本里批量创建空文件、确保文件存在或仅更新时间戳时非常实用; 编辑器“新建文件”的行为需你手动保存且遇到只读目录会失败.
nano 有不少快捷键; 你可以在屏幕的底部看到它们. ^ 代表 Ctrl 键. 常用操作有:
- Ctrl + O: 保存文件 (O 代表 Output)
- Ctrl + X: 退出- nano(X 代表 eXit)
在上一关卡中,你学会了如何利用 pwd, ls , cd 浏览文件系统. 你也学会了使用 touch 和 nano 创建和编辑文件.
那么在本关卡中,你将会学习到如何利用 shell 对文件系统进行编辑.
mkdir 命令可以让你创建一个文件夹,或者说是创建新的目录.
你在终端输入:
mkdir my_folder
mkdir -p parent_folder/child_folder通过 -p 参数的不同, 你可以选择创建单个文件夹或者创建多级目录.
cp 命令可以复制你的文件或目录,帮你轻松搞定备份和分发任务.
更重要的是, 源文件在命令后依旧存在.
你在终端输入:
cp file.txt backup.txt这会把 file.txt 复制成 backup.txt.
复制整个文件夹? 加个 -r 参数就行:
cp -r my_folder my_folder_copyTIP
-r代表递归 (recursive), 也就是复制文件夹内的所有内容, 包括子文件夹和文件.
与复制不同, mv 的移动命令表现的更像剪切, 它可以把你的文件从源文件移到目标位置.
改名如何呢? 自然是可以的. 这相当于 Windows 系统中的重命名.
mv file.txt file_only.txt也可以将文件移到某个指定路径.
mv file_only.txt /home/user/target_dir这个操作等价于: mv file_only.txt /home/user/target_dir/file_only.txt(当目标是已存在的目录时).
有复制和移动, 也就有删除命令.
rm 命令可以删除你的文件或目录.
在终端输入:
rm file_backup.txt
rm -r my_folder_backup这些一般就可以实现文件和文件夹的删除, 但是如果遇到一些只读权限或者比较难以删除的文件夹, 可以采用更为暴力的强制删除:
rm -rf temp/⚠️ 警告:
rm -rf具有不可逆风险; 务必确认路径无误.
Commanding Commands
With navigation skills under your belt, it's time to dive deeper into understanding how commands actually work. This module will teach you about command structure, options, arguments, and how to get help when you need it.
You'll learn the anatomy of shell commands, discover how to find and use more advanced commands, and understand the shell environment itself including variables and configuration. This knowledge will make you much more effective at using any command-line tool.
在上一关卡中, 我们逐步试图贯彻的观念是: 文件构成了 linux 的系统的核心. 你学会了如何查看文件系统, 如何在文件系统中执行读写操作; 在本关卡中, 我们将会展示 linux 中最为精华的部分之一: 命令行与指令. 我们希望通过这一关卡, 你能够理解 Shell 中何为指令, 何为环境变量, 并借此掌握 command 的调用以及如何定制你的 Shell 环境.
前排提醒: 这一整个章节可能会略微有点困难😈. 如果遇到问题, 请不要气馁. 如果有你认为没有全部掌握的内容, 阅读后续章节, 增强对于 Shell 的理解, 然后再回过头来看看.
调用指令#
在 Shell 中, 你可以通过输入指令来操作系统, 就像你在上一个章节使用 ls 命令来列出文件和目录一样.
command-name arg1 arg2 ...在上面的语法中, command-name 是你想要执行的指令的名称, 后面的 arg1, arg2 等是传递给该指令的参数.
如果参数中含有空格, 你可以使用引号将其括起来, 例如 "arg with spaces", 或者使用反斜杠 \ 来转义空格 arg\ with\ spaces. 在有特殊字符(如 *, ?, $)时, 引号也能避免被 shell 提前解释.
举一个例子: 在 Shell 中, 你可以使用 echo 指令来打印文本到终端. 例如:
echo "Hello, World!"这条指令会输出 Hello, World! 到终端. 其中, echo 是指令名称, "Hello, World!" 是传递给 echo 指令的参数.
很多指令都有自己的选项, 这些选项通常以 - 或 -- 开头, 用于修改指令的行为. 大家已经看到, ls 指令的 -l 选项用于以长格式显示文件和目录的信息:
ls -l有时, 选项可以接纳参数, 例如 head -n 10 用于显示文件的前 10 行.
-h 或者 --help 选项通常用于获取指令的帮助信息. 例如, 你可以运行以下命令来查看 ls 指令的帮助信息:
ls --help面对一个你不熟悉的指令, 你总是可以尝试运行 指令名 --help 来查看该指令的用法和可用选项. 通常, 这会打印出该指令的用法说明, 包括可用的选项和参数.
有些指令会有手册页, 你可以使用 man 指令名 来查看该指令的手册页, 获取更详细的信息. 同样的, 你可以用 man ls 来查看 ls 指令的手册页.
Better yet, 你可以使用更加现代化的 tldr 指令名 来获取该指令的简明用法示例. 这往往比 man 更加易读和实用.
tldr指令是一个社区驱动的项目, 需要你手动进行安装:sudo apt install tldr && tldr --update.apt的使用会在后续章节中介绍.
指令和可执行文件 (Executable)#
你是否在 Linux 上编译过代码? 它的输出是一个二进制文件, 在这里我们称之为程序 (Binary) 或可执行文件 (Executable). 你可以通过在终端中输入程序的名称来运行它. 例如, 如果你编译了一个名为 hello 的程序, 你可以通过输入其相对或绝对路径, 如 ./hello 来运行它.
$ g++ hello.cpp -o hello
$ ls
hello.cpp hello
$ ./hello
Hello, World!事实上: 在 Linux 中, 许多常用的命令行工具 (如 ls, cat, echo 等) 都是可执行文件. 你可以通过 which 命令来查看它们的路径:
$ which cat
/usr/bin/cat于是, 你也可以通过输入完整路径来运行它们: /usr/bin/cat file 和直接输入 cat file 是一样的.
还有一个指令
whereis. 它基于索引数据库搜索更多位置(可执行、源码、man 手册), 并返回一系列相关文件; 当你希望寻找多版本或定位 man 源时有用. 与which不同,which仅报告按PATH解析的第一个命令路径.
PATH 环境变量#
于是, 你满怀期待地输入 hello 来运行你的程序, 但是 shell 却告诉你 command not found. 这是为什么呢?
在 Linux 中, 当你输入一个命令时, shell 会在一系列预定义的目录中查找该命令对应的可执行文件. 这些目录存储在一个名为 PATH 的环境变量中. 你可以通过以下命令查看当前的 PATH:
echo $PATHPATH 中的每一个目录都是用冒号 : 分隔的.
需要注意的是, 这里的 PATH 是全大写的. Linux 中的变量名 区分大小写. 根据惯例, 所有环境变量都使用大写字母.
命令里 PATH 前的
$符号表示你想要获取该变量的值. 在 Linux 中, 你可以用VAR=value来设置变量, 用$VAR来获取变量的值.
如果你的程序所在的目录不在 PATH 中, shell 就找不到它, 因此会提示 command not found. 你可以通过在当前 shell 中临时修改 PATH 来解决这个问题(仅对当前会话有效).
export PATH=/path/to/your/program:$PATH关于 export 关键词, 其他环境变量以及其作用, 我们会在后续挑战中做更详细的介绍.
到目前为止, 你已经学会了 shell 的基础使用. 现在, 是时候让它成为真正属于你的了.
程序员都喜欢可定制化的东西, 就像你可能会花几个小时来选择 VS Code 的颜色主题和图标, 或者用 Wallpaper Engine 寻找完美的壁纸. shell 也不例外.
你有没有见过开发者那炫酷多彩的终端? 或者你是否好奇过他们是如何输入两个字母的命令就能神奇地运行一长串命令的? 这不是魔法, 你也可以做到!
在这个挑战中, 你将学会:
- 
如何创建节省时间的快捷方式, 它叫做别名(aliases) 
- 
理解你的 shell 如何使用环境变量来”记住”设置 
- 
最重要的是, 将你的终端转变为一个个性化的工作空间. 
什么是 Shell?#
你正在交互的程序叫做 shell. 它是一个命令行解释器, 将你的命令转换为操作系统可以执行的操作. 有许多不同的 shell, 每个都有自己的特色.
你最常遇到的有:
- 
bash: 经典的 shell, 也是大多数 Linux 系统的默认 shell. 
- 
zsh: 现代化、高度可扩展的 shell. 现在是 macOS 的默认 shell. 
- 
fish: 专注于从一开始就用户友好的 shell. 它具有语法高亮和”自动建议”等功能, 无需配置. 
要找出你正在使用哪个 shell, 运行以下命令. 它读取一个特殊变量, 该变量存储了你的 shell 程序路径.
echo $SHELL你可能会疑惑什么是 echo, 什么是 SHELL?别担心,我们很快就会讲到‘SHELL部分. 现在只需要知道echo` 是一个在终端打印文本的命令.
Shell 的记忆#
在我们进入定制化部分之前, 我们需要理解 shell 是如何在每次你打开新终端时记住你的设置. 并且, 在哪里可以找到这些设置.
它从位于你的主目录(~/)中的一个特殊脚本读取. 对于 bash, ~/.bashrc; 对于 zsh, 读取 ~/.zshrc.
这些只是纯文本文件, 每次启动新的 shell 会话时都会运行. 你可以用任何文本编辑器编辑它们来添加你自己的选项.
Recap: 以点(.)开头的文件是隐藏的. 要查看它们, 你必须在 ls 命令中使用
-a(all)标志:ls -a ~
你通常会在 .bashrc 或 .zshrc 文件中看到大约 100 行代码. 不要被文件大小吓到. 其中大部分是注释(以 # 开头的行), 解释每个部分的作用.
在这个课程中, 我们将重点关注这两个主要概念:
- 
别名(Aliases): 这是你的个人快捷方式. 
- 
环境变量: 这是你的 shell 的记忆. 
别名(Aliases)#
别名是你给较长命令起的快捷方式或昵称. 语法是 alias name="command".
这里有一些极其有用的例子:
alias ll="ls -laH"
alias ..="cd .."要创建别名, 你只需在 shell 中输入它. 但是, 它只在当前会话中存在. 请你务必记住: 要让某个设置永久生效, 你必须将命令添加到你的 shell 配置文件(~/.bashrc 或 ~/.zshrc)中. 只需用文本编辑器打开它, 在底部添加你的别名.
编辑配置文件后, 你需要告诉 shell 重新读取它. 你可以通过关闭并重新打开终端, 或者使用 source 命令来做到这一点:
# 对于 bash 用户
source ~/.bashrc
# 对于 zsh 用户
source ~/.zshrc环境变量#
环境变量是一个命名的值, 可以影响运行进程的行为方式. 把它们想象成程序可以读取以获取信息的系统级设置. 按照惯例, 它们的名称都是大写的.
这里有一些最重要的:
- 
$SHELL: 你当前 shell 程序的路径.
- 
$HOME: 你主目录的绝对路径.
- 
$USER: 你当前的用户名.
- 
$PATH: 一个用 ’:’ 分隔的目录列表, shell 在这些目录中按顺序查找可执行程序(从左到右). 例如, 如果你的 PATH 中有/usr/local/bin, 当你在终端中输入python时, shell 会在/usr/local/bin中查找名为python的可执行文件.
btw, Windows 也有环境变量, 你是否设置过 %PATH% 变量(在系统属性 -> 环境变量中编辑的那个)?
要查看所有环境变量, 使用 env 命令.
env要查看特定变量的值, 在变量名前加上美元符号 $ 使用 echo. $ 告诉 shell “给我这个变量的值”.
# 查看 HOME 变量
echo $HOME
# 查看你的用户名
echo $USER现在你可以理解为什么之前 echo $SHELL 是什么了.
你可以使用 export 命令创建自己的环境变量. 这使得变量对你从该 shell 运行的任何其他程序都可用.
export GREETING="Hello, World!"
echo $GREETING就像 alias 一样, 如果你希望你的变量是永久的, 你必须将 export 行添加到你的 ~/.bashrc 或 ~/.zshrc 文件中.
当然, 你也可以不加 export, 那么有什么区别呢? export 告诉 shell 这个变量对从该 shell 启动的任何子进程都是可用的. 如果你不使用 export, 变量只在当前 shell 会话中可用.
定制化#
好的, 让我们回到激动人心的部分: 定制化!
shell 可以通过许多方式定制, 从改变颜色到添加功能. 对我来说, 如果你想复制一个炫酷终端的外观, 最简单的方法是使用像 Oh My Zsh(当然, 你需要先安装zsh, 我们简称为 omz)这样的框架, 或者使用开箱即用就可定制的 fish.
有很多很棒的 omz 主题和插件可供选择. 你可以在 Oh My Zsh Themes ↗ 页面找到它们.
我个人也在使用 omz 配合 powerlevel10k 主题. 对于插件, 我使用 zsh-autosuggestions 和 zsh-syntax-highlighting, 以及 git 和 copyfile 插件.
去 Google 一下, 自己设置一下吧! 这能让你每次打开终端时都感到兴奋.
那么定制化是如何工作的? 这是魔法吗? 完全不是! 这完全建立在你刚学到的东西之上: 你的 .zshrc 文件和环境变量.
当你安装像 Oh My Zsh 这样的框架时, 它本质上接管了你的 ~/.zshrc 文件. 它用一个新的文件替换它, 这个新文件充当所有功能的控制面板. 让我们看看使用 Oh My Zsh 时你的 ~/.zshrc 文件是什么样的(删减过):
export ZSH="/Users/your_username/.oh-my-zsh"
ZSH_THEME="powerlevel10k/powerlevel10k"
plugins=(git copyfile)
source $ZSH/oh-my-zsh.sh现在你都可以理解它们了! export 命令设置环境变量, 告诉 shell 这个 omz 在哪里. ZSH_THEME 变量指定要使用的主题, plugins 数组列出要加载的插件. 最后, source 命令运行主要的 Oh My Zsh 脚本, 它读取所有这些设置并应用它们.
A Piper's Dream
One of the most powerful features of the shell is the ability to chain commands together and redirect data streams. This module will teach you how to harness this power to create sophisticated command pipelines.
You'll learn about input and output streams, how to redirect data to and from files, and how to pipe the output of one command into another. These techniques are what make the command line such a powerful tool for automation and data processing.
在这个章节中, 我们会简单接触 Shell 中最有用的一项技术: 管道 (pipe) 技术.
而在学习管道之前, 我们需要了解一些常用的命令, 让我们可以做简单的文件/输入输出转发和文本处理.
cat#
之前我们讲到了可以使用文本编辑器打开一个文件, 并查看它的内容. 然而呢, 如果仅仅只是要看一眼文件的内容, 其实不必大动干戈, 而且有很多命令可以帮我们完成这件事情.
打印文件内容, 无非就是将文件输出到终端上; 最常用的命令是 cat.
cat 不是猫, cat 是 “concatenate files and print on the standard output” 的缩写, 中文翻译“将文件拼接并输出到标准输出”; 不过我们暂时可以把它理解成“把文件内容打印到屏幕上”.
你可以这么使用:
# 查看单个文件内容
cat file1.txt
# 拼接文件内容
cat file1.txt file2.txt > combined.txtgrep#
当文件较短的时候, 直接用 cat 就能看清楚文件内容了. 但是如果文件很长, 比如系统日志, 你可能就需要一些更高级的工具来帮你分页、过滤和查找你想要的信息.
查看超长文件时, 建议先用
less file分页查看, 或者head/tail抽样; 直接cat可能会刷屏. 我们不要求你掌握他们, 留个印象知道他们的存在即可.
我们可以使用 nano 这种文本编辑器来打开文件, 然后用搜索功能查找关键词, 但是这种办法是交互式的, 也就是说没有办法“自动化”. 所以我们就需要一个可脚本化的关键词查询命令, 也就是 grep.
grep 的名字来源于上古时期的文本编辑器 ed 中的命令 g/re/p (globally search a regular expression and print). 所以它的工作就是查找匹配所有满足条件的行并把它们打印出来. 语法是先加待搜索的单词, 再跟上文件名, 比如:
$ cat file.txt
Hello World!
Hello grep!
Hello cat!
$ grep "grep" file.txt
Hello grep!grep 还有一些常用的选项, 比如:
# 查找文件中包含 "error" 的行
grep "error" logfile.txt
# 忽略大小写匹配
grep -i "error" logfile.txt
# 显示匹配的次数
grep -c "error" logfile.txt其实 cat, grep 本身与重定向没有太大的关系; 但是它的模式匹配与筛选作用是无可替代的. 等我们学习了重定向和管道之后, 它们的作用就能发挥得淋漓尽致了.
在字符串搜索上, Shell 有个更加强大的工具
awk和sed, 但是使用方式比较复杂. 日常使用不妨使用python(
在 Linux 中, “一切皆文件”的哲学意味着命令的输入和输出被视为可以重定向的流. 这一强大的概念使你能控制命令从哪里获取输入, 以及将输出发送到哪里.
让我用清晰的示例来告诉你探索重定向的工作原理.
输出重定向 (>)#
> 符号将标准输出重定向到文件. 输出不会显示在屏幕上, 而是保存到文件中.
还记得
pwd命令吗?它显示当前目录.
pwd                    # 在屏幕上显示当前目录
pwd > my_location.txt  # 将输出保存到文件
cat my_location.txt    # 查看文件内容⚠️ 警告: 使用
>会覆盖任何现有的文件内容!请小心.
追加输出 (>>)#
要向文件中添加内容而不覆盖它, 请使用 >>:
echo "First Line" > notes.txt      # 创建/覆盖文件
echo "Second Line" >> notes.txt    # 向文件添加新行
cat notes.txt                      # 显示两行内容思考: 上方
notes.txt文件中现在有几行?是什么?答案是两行, 分别是 “First Line” 和 “Second Line”.
输入重定向 (<)#
就像可以将输出重定向到文件一样, 你也可以使用文件作为命令的输入, 还是拿上面我们已经创建的 notes.txt 文件为例:
# 使用该文件作为 cat 命令的输入
cat < notes.txt这个命令会显示 notes.txt 的内容, 就像直接运行 cat notes.txt 一样.
理解流类型#
这一个章节我们不作要求, 但是了解这些内容会帮助你更好地理解重定向. 也会让你的代码 debug 过程更轻松.
Linux 有三个标准流:
- 标准输入 (stdin) - 流描述符 0
- 标准输出 (stdout) - 流描述符 1
- 标准错误 (stderr) - 流描述符 2
你们可能还没有听说过这三个流, 但是你们事实上肯定已经在使用 stdin 和 stdout 了. cin 就是从 stdin 读取输入, 而 cout 则将输出发送到 stdout. 更多关于流的知识, 请期待翁阿姨这个学期讲解输入输出的内容.
你可以分别重定向每个流:
# 仅重定向标准输出
ls -l > file_list.txt
# 仅重定向 stderr 消息
./bookstore 2> errors.txt
# 将标准输出和错误消息分别重定向到不同的文件
./bookstore > output.txt 2> errors.txt高级重定向#
这一章节我们也不作要求.
要将标准输出和错误同时重定向到同一个文件, 可以使用 2>&1:
./bookstore > combined.txt 2>&1你可能在想 “这都是啥?” 2>&1 的意思是: 把 stderr 重定向到 stdout 的位置, 然后 stdout 再被重定向到 combined.txt. 所以最终两个流都会进入同一个文件.
那你可能又会问 “上述命令为什么不写成这样: ./bookstore 2>&1 > combined.txt # 这样写不对!”
顺序很重要! 上面错误的命令会先把 stderr 重定向到 当前的 stdout(通常是屏幕), 然后再把 stdout 重定向到文件. 所以最终 stderr 仍然会显示在屏幕上, 而不是进入文件. 所以说应当先重定向 stdout 到文件, 然后再把 stderr 重定向到 stdout. 事实上, 现代的 Bash 版本允许你使用更简洁的 &> 来实现同样的效果:
./bookstore &> combined.txt重定向到 /dev/null#
当你想完全丢弃输出时, 可以重定向到 null:
# 丢弃 stderr 输出
./bookstore 2> /dev/null
# 丢弃所有输出
./bookstore &> /dev/null上一章节我们学习了怎么将命令的输出重定向到文件中. 现在我们来学习另一个非常强大的功能: 管道(Pipes).
管道符(|)是 Linux Shell 中非常强大的功能. 它可以把一个命令的输出直接传递给另一个命令作为输入.
基本语法#
命令A | 命令B这会把 命令A 的标准输出传递给 命令B 的标准输入. 让我们来看一个简单的例子:
echo 'Hello World!' | cat它应当输出 Hello World!.
这里发生了什么:
- echo 'Hello World!'输出 “Hello World!”
- 管道符 |把这个输出传递给cat
- cat显示输入内容, 结果就是 “Hello World!”
在前一章节中我们提了一嘴 Linux 中有三种标准流: 标准输入 (stdin)、标准输出 (stdout) 和标准错误 (stderr). 管道符就是把前一个命令的标准输出连接到后一个命令的标准输入.
那你们可能会想“我要这个管道符它有何用?”接下来我们会通过一些实用的例子来展示管道的强大之处.
ps -A | grep bashps -A 列出所有正在运行的进程(你们不需要掌握它), grep bash 过滤出包含 “bash” 的行. 这是极有用的, 因为你电脑中可能运行这成千上万的进程. 如果你只想看和 bash 相关的进程, 管道就能帮你实现.
多重管道组合#
你可以把多个命令串联起来:
cat /var/log/syslog | grep ERROR | wc -l这里发生了什么:
- cat /var/log/syslog读取系统日志文件内容.
- grep ERROR过滤出包含 “ERROR” 的行.
- wc -l统计这些行的数量.
这会统计系统日志中包含 “ERROR” 的行数. 类似的, 你也可以将其用作你代码的调试, 在你们这学期会写到的 bookstore 项目中, 快速定位到你问题, 让你的 debug 过程更高效.
Perceiving Permissions
Understanding file permissions is crucial for system security and proper file management. This module will teach you about users, groups, and the permission system that controls who can read, write, and execute files.
You'll learn how to read permission strings, how to change file permissions, and understand the concepts of file ownership. This knowledge is essential for managing your own files and collaborating with others on shared systems.
Linux 是一个多用户系统, 这意味着它内置了一套规则来防止文件被不该访问或修改的人读取或改动. 本课将帮助你理解并控制这些规则.
用户和用户组#
在 Linux 中, 一切都归属于某个用户. 每个文件、每个进程(正在运行的程序)都关联到一个特定的用户账户, 系统据此“记账”.
用户 (user): 一个独立的账户. 当你登录时, 你是以特定用户的身份行动. 最强大的用户称为 root(或超级用户), 它对整个系统拥有几乎不受限制的访问能力.
用户组 (group): 用户的集合. 用户组使得同时管理多个用户的权限变得容易. 一个常见例子是用于网页服务的 www-data 组, 你不希望每个普通用户都能改网站文件; 同样, 也不希望 Nginx、Caddy 等服务进程访问你的私人文件. 这篇文章有更详细的说明 ↗.
你可以使用 whoami 命令查看你是谁.
whoamirwx 权限#
在之前的课程中, 你已经学习了 ls -la 命令, 其中 -l 参数显示文件的详细信息, 包括它们的权限. 你可能看到像这样的内容:
total 96
drwxr-x--- 119 theunknownthing  staff   3.7K Sep 15 16:09 ..
drwxr-xr-x   8 theunknownthing  staff   256B Sep 15 16:00 contents
drwxr-xr-x  15 theunknownthing  staff   480B Sep 15 15:59 .git
-rw-r--r--   1 theunknownthing  staff   1.1K Sep  9 20:58 README.md
...看到 drwxr-xr-x 部分了吗?这就是显示权限的地方.
让我首先解释这些字符的含义. 有三种基本权限:
- 
读取 (r): 查看文件内容或列出目录内容的能力. 
- 
写入 (w): 更改或删除文件, 或在目录内创建/删除文件的能力. 
- 
执行 (x): 运行文件(如果它是程序或脚本)或进入目录( cd进入)的能力.
第一个字符表示文件类型: d 表示目录, - 表示普通文件, l 表示符号链接, 以及其他字符表示特殊文件类型.
所以, 在 drwxr-xr-x  15 theunknownthing  staff   480B Sep 15 15:59 .git 中:
- 
d表示这是一个目录.
- 
然后你可以看到恰好 9 个字符, 分为三组, 每组三个. 前 3 个字符 ( rwx) 是所有者的权限. 这里,rwx意味着所有者可以读取、写入和执行. 此文件的所有者是theunknownthing.中间 3 个字符 ( r-x) 是用户组的权限. 这里,r-x意味着用户组可以读取和执行, 但不能写入. 此文件的用户组是staff.最后 3 个字符 ( r-x) 是其他人(其余所有人)的权限. 同样,r-x意味着他们可以读取和执行, 但不能写入.
为了加深理解, 让我们看另一个例子: -rw-r--r--   1 theunknownthing  staff   1.1K Sep  9 20:58 README.md
在看下一页的答案之前, 请先自己解读一下. 思考:
- 这是什么类型的文件?
- 所有者拥有什么权限?为什么所有者没有执行权限?
- 用户组拥有什么权限?
- 其他人拥有什么权限?
第一个字符是 -, 表示这是一个普通文件.
所有者拥有 rw- 权限, 意味着他们可以读取和写入文件, 但不能执行它(这很明显, 因为它是一个Markdown文本, 你不能运行它).
Tips: 并非所有文本文件都“不能执行”, 而是“默认没有执行位”. 若它是脚本并设置了执行位且有正确的 shebang(例如 #!/usr/bin/env bash), 可直接执行; 即使没有执行位, 也可用解释器显式运行, 例如 bash script.sh 需要脚本具备读权限. 了解 shebang, 请参阅这里 ↗. 不过, 我不要求你现在掌握它.
用户组拥有 r-- 权限, 意味着 staff 组中的用户可以查看文件内容, 但不能修改或执行它.
其他人也拥有 r-- 权限, 意味着他们也只能读取文件.
使用 chmod 更改权限#
chmod(change mode)命令用于更改文件的权限. 你可以通过两种常见方式来做这件事: 符号表示法或八进制表示法.
我们首先学习符号表示法, 这种方式更直观.
这种方法使用字母(u 表示 user(所有者), g 表示 group, o 表示 others, a 表示 all)和符号(+ 添加, - 删除, = 设置)来修改权限.
# 为owner添加执行权限
chmod u+x script.sh
# 设置others的权限为只读
chmod o=r README.md
# 移除除owner外所有人的读取权限, 这里 `go` 表示 `g` 和 `o`
chmod go-r README.md
# 移除groups的所有权限
chmod g= script.sh八进制表示法#
这种方法使用数字来表示每个类别的权限. 这有点像二进制! r = 4, w = 2, x = 1. 你可以将想要的权限对应的数字相加.
| 数字 | 权限 | 含义 | 
|---|---|---|
| 7 | rwx | 读、写和执行, 4+2+1 | 
| 6 | rw- | 读和写, 2+4 | 
| 5 | r-x | 读和执行, 4+1 | 
| 4 | r— | 只读, 4 | 
| 0 | --- | 无权限, 0 | 
使用八进制表示法的 chmod 命令使用一个 3 位数字, 分别代表用户、组和其他人的权限(按此顺序). 要设置权限为 rwxr-xr-x(用户可以做任何事; 组和其他人可以读取和执行), 你将使用数字 755.
chmod 755 script.sh这将为所有者设置 7(rwx), 为组设置 5(r-x), 为其他人设置 5(r-x).
使用 sudo 获取超级用户权限#
当你需要执行一些你的普通用户账户无权执行的操作时, 例如安装软件或编辑系统配置文件, 会发生什么?
apt update你会得到错误:
Reading package lists... Done
E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)
E: Unable to lock directory /var/lib/apt/lists/
W: Problem unlinking the file /var/cache/apt/pkgcache.bin - RemoveCaches (13: Permission denied)
W: Problem unlinking the file /var/cache/apt/srcpkgcache.bin - RemoveCaches (13: Permission denied)为此, 你需要管理员权限. sudo 命令让你以超级用户(root)身份执行单个命令.
sudo apt update🈲 root 用户可以做任何事, 包括意外删除你的整个系统. 在使用 sudo 运行任何命令前请仔细检查. 你可能见过一些梗图让你执行
sudo rm -rf /*. 别做! 我不会来帮你恢复系统的!
如果你想亲自登录 root 用户, 可以使用 su 命令(substitute user, 替换用户).
sudo su这相当于用 sudo 以提权方式运行 su; 在不带参数时, su 默认切到 root.
Tips: 直接运行 su(不带 sudo)默认会询问“目标用户的密码”(通常是 root 的密码). 在 Ubuntu 上, root 账户默认可能被锁定, 此时 su 不可用, 但 sudo su 会要求你输入当前用户的密码(前提是你在 sudoers 中).
当然, su 不仅限于切换到 root. 如果你知道其密码, 你可以切换到任何用户.
su some_username如果你知道他们的密码, 或者如果你当前的用户拥有比该用户更高的权限, 这将切换到 some_username. 所以从 root 切换到任何用户不需要该用户的密码.
Super Cow Powers
The final piece of essential shell knowledge is understanding how to install and manage software packages. This module will introduce you to package managers and how they help you keep your system up to date.
You'll learn how to search for, install, and manage software packages using command-line package managers. You'll also learn about package repositories and mirrors, which can help you get faster and more reliable access to software.
到目前为止, 你已经学会了使用 shell 的主要功能.
你知道在 shell 下如何应用各种不同的软件与包, 但是这些包又从何而来?
于是乎在本关卡中, 我们将会接触 apt 和 mirror 的概念.
什么是包#
在计算机科学中, 你可以把包 (Package) 理解为一个或者多个软件的集合, 以及它们需要依赖什么系统资源的信息.
在 Linux 系统中, 安装软件的方式通常是安装包.
包管理器#
一般来说, 所有的包都被由称作包管理器的系统所管理.
以常见的 Ubuntu 系统为例, 其默认包管理器为 apt , 它极大地简化了软件的获取、安装、升级和移除过程:
你往往只需要一条简单命令如 sudo apt install firefox 即可完成软件安装.
不仅如此, 包管理器能够自动处理软件包之间复杂的依赖关系 (Dependencies), 确保所需的所有库和组件都能被正确安装.
如果你想知道为什么这个章节叫做 Super Cow Powers, 那是因为
apt的一个隐藏彩蛋. 你可以试试apt --help读一读末尾, 或者运行apt moo.
在你安装 wsl2 的早期, 你想必已经尝试了一些命令, 比如:
sudo apt install package    # 安装包
sudo apt update             # 更新包列表
sudo apt upgrade            # 升级已安装的包这些命令通常可以帮助你自动从对应软件包的仓库中下载. 这比在网上找安装包更加方便, 不是吗?
当然, 如果你从网站上下载了一个 .deb 文件, 你也可以使用 apt 来安装它.
sudo apt install ./path/to/package.deb关于镜像源#
在上一个单元中, 你学习了使用 apt 来安装软件包.
但你有没有想过, 这些软件包是从哪里来的呢?
事实上,所有的软件包都存储在称为软件仓库 (Repository) 的服务器上. 这些仓库通常由操作系统的维护者或第三方组织管理.
但是很多时候, 由于不可抗力因素, 在国内访问国外软件仓库源速度往往较慢.
为了显著提升下载速度, 我们强烈建议将系统的软件源替换为国内的镜像源(如 TUNA, USTCLUG, 阿里云).
它们与系统默认的软件源往往拥有相同的内容, 但拥有更高的传输速度.
事实上, 常见的软件安装包如
pip,huggingface,modelscope等都是有镜像的, 各位可以自行查询.
更换镜像源#
一般来说, 在镜像网站上你能读到如何修改软件源的说明, 以清华镜像源为例, 你可以访问: https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/ ↗
- 
将 ubuntu.sources备份到同目录下的ubuntu.sources.bak.
- 
修改 ubuntu.sources, 在这个例子里你需要将其修改为:
Types: deb
URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
# 以下安全更新软件源包含了官方源与镜像站配置,如有需要可自行修改注释切换
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg在完成这一步骤后, 你可以测试与 apt 相关的命令, 一般能够提速 100 倍甚至更高.
以上就是关于镜像源的内容. 如果你希望使用它提速, 可以在自己的 wsl 上做相关的配置. 祝您旅途愉快!
[flag]