从这一课起进入第七阶段:Shell 与脚本编程。前面几十课,你一直在"用命令"——lscdgrepgcc——但你有没有想过:你敲下 ls 回车,到底是谁在背后把它翻译成动作、再交给内核执行的? 那个幕后的"翻译官",就叫 shell。这一课,我们回到终端这个最熟悉的地方,揭开 shell 的面纱。

从"敲命令"这个动作说起

你在终端敲下 ls,回车,屏幕上出现一堆文件名。

这中间发生了什么?不是内核直接"看懂"了 ls 这两个字母。内核只认"执行某个程序"这种机器指令,它不知道 ls 是什么。中间有个翻译官:

  1. 你敲 ls
  2. shell 读到这两个字母,理解"你想执行 ls 这个命令";
  3. shell 去 PATH 里找到 /bin/ls 这个程序;
  4. shell fork 一个子进程去跑它(还记得第 7 课的 fork 吗);
  5. 结果打印回你的屏幕。
这就是 shell 的本职:命令解释器

25.1 shell 是什么

一句话理解:shell 是命令解释器——你在终端敲的命令,由它解析、找到程序、交给内核去执行,再把结果打印回来。它是"你和内核之间的翻译官"。

shell 扮演的角色,可以这样记:

  • 对上(对用户):接收你敲的命令,理解你的意图;
  • 对下(对内核):找到程序、fork 子进程、让它去执行;
  • 结果:把输出打印回来给你看。
所以 shell 不是内核的一部分,它是一个用户态程序,只是它的工作就是"帮你跟内核打交道"。

25.2 bash / zsh / sh 的关系

你可能听过好几个名字:shbashzsh。它们都是 shell,是一家人:

一句话理解sh 是最早的 shell,bash 是它的增强兼容版(Linux 默认),zsh 交互体验更强(补全、主题)。脚本第一行 #!/bin/bash 就是指定用哪个解释器。
  • sh:最古老、最标准的 shell;
  • bashsh 的增强兼容版,Linux 的默认 shell,功能更强、更好用;
  • zsh:交互体验最炫(强大的自动补全、主题、插件),很多人手动换它当日常 shell。
你写脚本时,第一行的 #!/bin/bash(叫 shebang)就是在声明:"这个脚本,请用 bash 来解释执行"。

两个命令帮你了解现状:

  • echo $SHELL:看当前默认 shell;
  • cat /etc/shells:看系统装了哪些 shell。

25.3 登录 shell vs 非登录 shell

shell 还分两种"身份",加载的配置不同:

类型什么时候加载的配置
登录 shellSSH 登录、su -/etc/profile~/.bash_profile/~/.profile
非登录 shell开新终端窗口、跑脚本~/.bashrc
一句话理解:登录时读"全局 + 个人 profile",日常开终端读 ~/.bashrc——所以你的别名和提示符通常写在 ~/.bashrc
  • 登录 shell:你 SSH 登录进系统、或 su - user 切用户时,那个 shell 是"登录 shell",会加载全局配置 /etc/profile 和个人配置 ~/.bash_profile
  • 非登录 shell:你登录后新开一个终端窗口,或跑一个脚本,那个 shell 是"非登录 shell",只加载 ~/.bashrc
这就是为什么你平时定义的别名(alias ll='ls -l')通常写在 ~/.bashrc 里——因为日常用的都是非登录 shell,读的是它。

判断方法:echo $0,前面带 - 的是登录 shell;su - user 是登录、su user 不是。


25.4 环境变量与 PATH

一句话理解:环境变量是"传给子进程的全局设置",PATH 是最重要的一个——它列出 shell 找命令时搜索的目录列表。

环境变量:一串 名字=值 的全局设置,会被传给子进程

其中最重要的就是 PATH

你敲 ls,shell 去哪找?就是PATH 里列出的目录,一个个找过去,找到第一个叫 ls 的可执行文件就用它。
  • echo $PATH:看到一串冒号分隔的目录(比如 /usr/local/bin:/usr/bin:/bin);
  • which ls:告诉你 ls 到底在哪个目录被找到。
一句话:PATH 就是 shell 的"寻人启事"——它决定了哪些命令能"裸敲"(不带路径)直接执行。

25.5 内建命令 vs 外部命令

这是 shell 里一个很反直觉、但很重要的区别:

一句话理解cdexportecho 是 shell 内建的(在 shell 进程里直接执行);lsgrep外部程序(要 fork 子进程)。所以 cd 能改变当前 shell 的工作目录,外部命令不行。
  • 内建命令(builtin):cdexportecho 等,就是 shell 自己的一部分,在当前 shell 进程里直接执行;
  • 外部命令lsgrep 等,是独立的程序,shell 要 fork 一个子进程去跑。

这个区别导致一个关键现象:

cd 能真正改变你当前 shell 的工作目录,因为它是内建的、在"你"这个进程里执行。 而外部命令在子进程里跑,子进程怎么折腾(包括 cd)都影响不到父进程。

验证:

  • type cd → 显示 "cd is a shell builtin"(内建);
  • type ls → 显示 "ls is /bin/ls"(外部程序)。

动手实验:命令观察

echo $SHELL              # 当前默认 shell
echo $0                  # 当前是登录 shell 吗(带 - 前缀)
echo $PATH               # 看搜索路径
which python3            # python3 在哪
export GREETING="hello shell"
echo $GREETING           # 变量已导出
bash -c 'echo $GREETING' # 子 shell 也能看到(因为 export 了)

再做一组对比,理解"内建 vs 外部":

cd /tmp        # 内建命令,当前 shell 目录真的变了
pwd            # 确认:/tmp

然后写个小脚本:

cat > /tmp/cd_test.sh << 'EOF'
#!/bin/bash
cd /home
EOF
chmod +x /tmp/cd_test.sh
./cd_test.sh   # 脚本里 cd /home
pwd            # 结果还是 /tmp,没变!
为什么 pwd 还是 /tmp 因为脚本里的 cd /home 发生在子进程里——脚本运行在一个全新的子 shell,它 cd/home,子进程一结束就消失了,你的当前 shell 纹丝不动。这正好印证了"内建命令在自家里执行、能改状态;外部/子进程里的操作改不了父进程"。

深入点:两个进阶话题

① 子 shell 与变量作用域

不加 export 的变量,只在当前 shell 里有效;要传给子进程,必须 export
A=1            # 普通变量,只在本 shell
bash -c 'echo $A'   # 空(子进程看不到)
export A=1
bash -c 'echo $A'   # 1(export 后子进程能看到了)

source vs ./script

  • ./script开一个子进程跑脚本,脚本里改的环境变量、目录,跑完就没了;
  • source script在当前 shell 里逐行执行脚本,改的变量、目录保留在当前 shell
这就是为什么你改了 ~/.bashrc 后,要 source ~/.bashrc 才生效——因为要在当前 shell 里重新执行一遍,而不是开个子进程。

小结与思考题

这一课,我们揭开了 shell 的面纱:

  • shell 是命令解释器:你和内核之间的翻译官;
  • sh → bash → zsh:一家三代,bash 是 Linux 默认;
  • 登录 vs 非登录 shell:登录读 profile,日常读 .bashrc
  • 环境变量与 PATH:传给子进程的全局设置,PATH 决定命令去哪找;
  • 内建 vs 外部命令:内建在自家执行(能改状态),外部要 fork 子进程。

留三个问题:

  1. shell 在用户和内核之间扮演什么角色?
  2. 登录 shell 和非登录 shell 分别加载哪些配置文件?
  3. 为什么 export 之后子进程才能看到变量?
现在你知道了 shell 是什么、命令是怎么被执行的。但 shell 能做的远不止"一条条敲命令"——它还能把一堆命令写进一个文件,按顺序、带逻辑地自动执行,这就是 shell 脚本。脚本里的变量怎么定义?怎么判断、怎么循环?怎么写一个真正能用的自动化脚本?下一课,我们写第一个脚本。

标签: 计算机基础, 操作系统, Linux

添加新评论