Shell 执行模型与环境加载
# 执行模型
# 父子 Shell
- 内建命令(如
cd)、赋值和函数通常在当前 Shell 执行;外部程序运行于独立进程,不能反向修改父进程环境 (...)创建子 shell:内部的cd、变量定义只在子 shell 内生效,不影响父 shell。适合临时切换环境- Bash 默认将管道各段放在子 Shell 环境。关闭作业控制并开启
shopt -s lastpipe时,末段可在当前环境运行;Zsh 默认可让末段在当前环境执行。不要把管道中的变量修改作为跨 Shell 保证 - 进程替换
<(...)同样是子 shell
经典陷阱:管道接 while 循环,循环内的计数在子 shell 里,循环外拿不到:
# ✗ count 在子 shell 中自增,循环结束后丢失
count=0
cat file.txt | while IFS= read -r line; do count=$((count + 1)); done
echo "$count" # Bash 默认输出 0;Zsh 默认保留末段计数
# ✓ 用重定向代替管道,循环在当前 shell 执行
count=0
while IFS= read -r line || [ -n "$line" ]; do count=$((count + 1)); done < file.txt
echo "$count"
2
3
4
5
6
7
8
9
# 进程替换
<(...) 用文件描述符路径或 FIFO 暴露命令输出(不是普通磁盘临时文件),供只接受文件参数的命令使用(bash/zsh):
diff <(ls /path1) <(ls /path2) # 比较两个命令的输出
cat <(echo "Hello World")
paste <(cut -d: -f1 /etc/passwd) <(cut -d: -f3 /etc/passwd)
2
3
# Here Document
<<TAG 把多行文本作为命令输入,TAG 为自定义结束标记(文本中未出现即可):
cat << END
这是一段
多行文本
END
2
3
4
关键区别:tag 加不加引号决定是否做变量展开——
cat << EOF # 展开 $var
$(date) $USER
EOF
cat << 'EOF' # 不展开,原样输出
$(date) $USER
EOF
2
3
4
5
6
7
写文件与赋值多行字符串:
cat << END > config.pbtx # 多行文本写入文件
这是一段配置
END
info=$(cat << 'EOF' # 多行字符串赋值给变量(引号防展开)
line1
line2
EOF
)
echo "$info"
2
3
4
5
6
7
8
9
10
# 重定向
| 写法 | 含义 |
|---|---|
cmd > file / cmd >> file | stdout 覆盖 / 追加到文件 |
cmd 2> file / cmd 2>> file | stderr 重定向 |
cmd > file 2>&1 | stdout 和 stderr 都进 file(bash 可简写 &>file) |
cmd > /dev/null 2>&1 | 丢弃全部输出 |
cmd 2>&1 | less | 合并后进管道 |
2>&1 拆解:2 是 stderr 的文件描述符,> 重定向,&1 表示"引用 fd 1(stdout)",即把 stderr 合并到 stdout 当前指向的位置。
顺序很重要:cmd 2>&1 > file 先把 stderr 指向当时的 stdout(终端),再把 stdout 指向 file——stderr 仍打到终端。要合并进文件必须 > file 2>&1。
# IFS 与逐行读取
IFS(Internal Field Separator)决定 shell 分词的边界。逐行读取的标准写法:
while IFS= read -r path; do
[ -z "$path" ] && continue
...
done < "$input_file"
2
3
4
IFS=(置空):不裁剪行首尾空白,整行作为单个变量值-r:反斜杠不做转义,路径等特殊字符原样保留
按分隔符给多个变量分配字段;最后一个变量接收剩余部分:
IFS=' ' read -r first rest <<< "abc ddd eee fff"
printf '<%s><%s>\n' "$first" "$rest" # <abc><ddd eee fff>
# 只写 read -r word 时,一个变量接收整行,不会逐词循环
2
3
# 执行、加载与继承
| 调用 | 执行环境 | 对当前终端的影响 |
|---|---|---|
./script.sh | 新进程,按 shebang 选择解释器(需执行权限) | 子进程的 cd、赋值不传回 |
bash script.sh | 新 Bash 进程,忽略脚本 shebang 的解释器选择 | 不能修改父 Shell |
source file / . file | 当前 Shell 读取执行;. 是 POSIX 写法 | 变量、函数、cd、选项、exit 均影响当前 Shell |
export NAME=value | 标记变量传给之后启动的子进程 | 子进程不能反向更新父环境;不传递普通别名 |
exec command | 外部命令替换当前 Shell 进程映像 | 正常不会返回;不应在日常终端随意执行 |
exec >log 2>&1 | 无命令时修改当前 Shell 文件描述符 | 后续输出持续重定向 |
PROJECT_HOME="$HOME/projects/demo" # 当前 Shell 变量
export PROJECT_HOME # 后续子进程可继承
bash -c 'printf "%s\n" "$PROJECT_HOME"' # 可以读到;子进程赋值不能改回父进程
2
3
配置加载与最短重载路径:交互 Zsh 在 ~/.zshrc 中放项目提供的单一加载入口,改配置后仍执行 source ~/.zshrc。加载链应由项目入口维护,不要求使用者记住内部环境文件。外部安装/初始化程序可以写入配置、安装到已有 PATH 目录,但不能直接改变已经运行的父 Shell;若新命令放进原有 PATH,往往立即可用。见 Zsh 配置。
| Shell | 常见启动文件(各模式不同) |
|---|---|
| Bash 登录 | /etc/profile,随后首个可读的 ~/.bash_profile、~/.bash_login、~/.profile |
| Bash 交互非登录 | ~/.bashrc;登录配置可显式加载它 |
| Zsh | .zshenv 总会参与常规启动;登录加 .zprofile,交互加 .zshrc,登录末尾加 .zlogin;路径受 ZDOTDIR 影响 |
$SHELL 通常表示账户登录 Shell,不代表当前脚本解释器。使用 type name / command -v name 检查别名、函数、命令与 PATH 命中情况;Bash 查找缓存必要时 hash -r,Zsh 可 rehash。
进程替换的失败:diff <(producer1) <(producer2) 只能直接得到 diff 的状态,生产者失败不会自动合并进它。关键数据先保存到受检的临时文件,再比较;不要认为 pipefail 会检查所有 <(...)。
依据:Bash 执行环境 (opens new window)、Bash 启动文件 (opens new window)、Zsh 执行规则 (opens new window)。