大多数人写 Shell 脚本是这样的:
./script.sh
能跑,就结束了。
但如果你不知道 Bash 是如何解析、创建进程、处理变量作用域、管理子 Shell 的,你迟早会踩坑。
这一篇我们系统讲清楚执行模型。
一、Shell 的本质:命令解释器
Shell 不是操作系统,它是"命令解释器"。
在 Linux 中,常见的 Shell 有:
-
Bash
-
Zsh
-
Sh
当你输入一条命令时,流程是:
-
读取输入
-
词法分析(分割参数)
-
变量替换
-
命令查找
-
fork 创建子进程
-
exec 执行程序
-
等待返回状态码
这就是 Shell 的核心执行流程。
二、Shebang 的真实作用
脚本开头常见:
#!/bin/bash
这一行叫 shebang。
作用是告诉内核:
这个文件应该由哪个解释器执行
如果你写:
#!/bin/sh
在某些系统中,/bin/sh 可能不是 Bash,而是 dash。
这会导致:
-
\[ 语法报错
-
某些参数展开不支持
运维场景建议明确写:
#!/usr/bin/env bash
这种方式更通用。
三、执行脚本的三种方式(差异非常重要)
1)直接执行
./script.sh
前提:有执行权限。
特点:
-
创建子进程
-
在新 Shell 环境中运行
-
不影响当前 Shell
2)bash script.sh
bash script.sh
效果类似上面,也是子进程执行。
3)source script.sh(最容易出问题)
source script.sh
或者:
. script.sh
特点:
-
不创建子进程
-
在当前 Shell 中执行
-
会修改当前环境变量
区别非常关键。
举例:
VAR=100
执行:
bash script.sh
echo $VAR
输出为空。
但如果:
source script.sh
echo $VAR
输出 100。
原因:是否创建子进程。
四、父进程与子进程
Shell 执行外部命令时会:
-
fork:复制当前进程
-
exec:替换子进程为目标程序
这意味着:
-
子进程不能修改父进程变量
-
环境变量是拷贝传递
例如:
VAR=10
bash -c 'VAR=20'
echo $VAR
输出仍然是 10。
很多新手误以为变量会"共享"。其实不会。
五、子 Shell 与括号陷阱
这段代码:
(cd /tmp; ls)
pwd
pwd 仍然是原目录。
因为括号 () 会创建子 Shell。
而花括号不会:
{ cd /tmp; ls; }
pwd
此时当前目录会改变。
这类差异在自动化脚本中非常关键。
六、管道为什么会"丢变量"
经典问题:
count=0
cat file.txt | while read line
do
((count++))
done
echo $count
输出是 0。
原因:
管道会创建子 Shell。
while 在子 Shell 中执行,变量修改只存在子进程。
解决方式:
while read line
do
((count++))
done < file.txt
这是很多线上脚本统计错误的根源。
七、退出状态码与 set 机制
Shell 每个命令都有返回码:
-
0 表示成功
-
非 0 表示失败
你可以查看:
echo $?
工程化脚本必须控制错误传播:
set -e
表示遇到错误立即退出。
更严格的写法:
set -euo pipefail
含义:
-
-e:错误退出
-
-u:未定义变量报错
-
-o pipefail:管道任一命令失败即失败
在运维脚本中,这是基本规范。
八、执行顺序总结
Shell 执行顺序是:
-
读取命令
-
参数展开
-
重定向处理
-
fork
-
exec
-
等待返回
-
获取状态码
理解这条流程,你会明白:
-
为什么变量在某些地方失效
-
为什么管道有副作用
-
为什么 source 会污染环境
-
为什么有些命令执行失败却脚本继续运行
本篇总结
写 Shell 的第一步,不是学语法。
而是理解:
每一行命令,背后都是一个进程模型。
如果不理解子 Shell、变量作用域、fork/exec,你写出来的脚本迟早出问题。