Shell 入门与变量
# 第 1 章 Shell 入门与变量
# 目录介绍
# 1.1 Shell 简介与类型
# 1.1.1 Shell 是什么?
Shell 是操作系统最外层的"壳"——它接受你输入的命令,翻译给内核执行,然后把结果返回给你。
用户 (你)
│ 输入命令
▼
┌──────────────┐
│ Shell │ ← 命令解释器——翻译人类语言 → 系统调用
└──────────────┘
│ 系统调用
▼
┌──────────────┐
│ Kernel │ ← 内核——管理硬件、进程、文件
└──────────────┘
Shell 脚本的价值:把一系列手动敲的命令写入文件——一次编写、反复执行、完全可重复。服务器运维、CI/CD 流水线、自动化部署——全部依赖 Shell 脚本。
# 手动操作——每次都要敲
cd /var/log
grep "ERROR" app.log | wc -l
tar -czf backup_$(date +%Y%m%d).tar.gz /data/
# 写成脚本——一行 cron 搞定
# check_errors.sh
#!/bin/bash
ERROR_COUNT=$(grep "ERROR" /var/log/app.log | wc -l)
if [ "$ERROR_COUNT" -gt 100 ]; then
echo "⚠️ 错误数 $ERROR_COUNT——超过阈值!" | mail -s "告警" admin@company.com
fi
# 1.1.2 bash / zsh / sh 的区别
# 查看当前 Shell
echo $SHELL # /bin/zsh(macOS 默认)或 /bin/bash(Linux 默认)
# 查看系统安装了哪些 Shell
cat /etc/shells
# 查看 bash 版本
bash --version
主流 Shell 对比:
| Shell | 全称 | 特点 | 默认系统 |
|---|---|---|---|
| sh | Bourne Shell | 最古老、最基础——POSIX 标准的最小公共集 | 所有 Unix |
| bash | Bourne Again Shell | sh 的超集——功能丰富、最广泛使用 | Linux |
| zsh | Z Shell | bash 的超集——更好的自动补全、主题、插件 | macOS (10.15+) |
| dash | Debian Almquist Shell | 精简版——启动快,Debian 的 /bin/sh | Debian/Ubuntu |
🔑 写脚本用哪个? ——用 bash。它是 Linux/macOS/CI 环境的共同语言。zsh 交互体验最好(补全强),但脚本写 bash 兼容性最广。
# 每个脚本的第一行(§1.1.4 详解)
#!/bin/bash # ← 明确声明:用 bash 解释执行
# 1.1.3 第一个 Shell 脚本
#!/bin/bash
# hello.sh —— 第一个 Shell 脚本
echo "Hello, Shell!"
echo "当前用户:$USER"
echo "当前目录:$(pwd)"
echo "当前时间:$(date '+%Y-%m-%d %H:%M:%S')"
# ① 赋予执行权限(只需一次)
chmod +x hello.sh
# ② 运行
./hello.sh
# 输出:
# Hello, Shell!
# 当前用户:yangchong
# 当前目录:/home/yangchong
# 当前时间:2025-06-12 14:30:00
🔑 chmod +x 干了什么?
ls -l hello.sh
# -rw-r--r-- 1 user group 123 Jun 12 14:30 hello.sh ← 没有 x
chmod +x hello.sh
ls -l hello.sh
# -rwxr-xr-x 1 user group 123 Jun 12 14:30 hello.sh ← 有了 x(execute)
# 1.1.4 shebang (#!) 的奥秘
每个脚本第一行的 #!/bin/bash 叫 shebang——它告诉操作系统"用哪个程序来执行这个文件":
#!/bin/bash # 用 bash 解释
#!/usr/bin/env bash # 用 env 找到 bash(更可移植——推荐!)
#!/bin/zsh # 用 zsh 解释
#!/usr/bin/python3 # 用 Python 解释——Shell 脚本也可以是 Python!
#!/usr/bin/env python3
# hello.py —— 加 shebang 后可以直接 ./hello.py 执行
print("这是 Python 写的 'Shell 脚本'")
🔑 #!/bin/bash vs #!/usr/bin/env bash:
#!/bin/bash | #!/usr/bin/env bash | |
|---|---|---|
| bash 路径 | 写死 /bin/bash | 由 env 在 PATH 中搜索 |
| 可移植性 | macOS 可能没有 /bin/bash 最新版 | ✅ 到处能用(只要 PATH 里有 bash) |
| 推荐 | 绝对路径明确 | ✅ 推荐——更可移植 |
# 1.1.5 四种执行方式对比
# ===== 方式 ①:./script.sh —— 子进程执行(最常用) =====
./hello.sh
# 开一个子 Shell,在子 Shell 里执行脚本
# 脚本里的变量不会污染当前 Shell
# ===== 方式 ②:bash script.sh —— 显式调用解释器 =====
bash hello.sh
# 和 ./hello.sh 效果相同——但不需要执行权限
# ===== 方式 ③:source script.sh 或 . script.sh —— 当前 Shell 执行 =====
source hello.sh # 或
. hello.sh
# ⚠️ 在当前 Shell 中执行——脚本里的变量会留在当前环境!
# 常用于加载环境变量:source ~/.bashrc
# ===== 方式 ④:管道——把脚本内容塞给 bash =====
curl -s https://example.com/setup.sh | bash
# ⚠️ 危险——永远不要对不可信的脚本这样做!
四种方式对比表:
| 方式 | 开启新进程? | 需要 x 权限? | 变量污染? | 场景 |
|---|---|---|---|---|
./script.sh | ✅ | ✅ | ❌ | 执行脚本 |
bash script.sh | ✅ | ❌ | ❌ | 调试/无权限 |
source script.sh | ❌ | ❌ | ✅ | 加载环境变量 |
curl ... \| bash | ✅ | ❌ | ❌ | 远程安装(危险!) |
# 1.1.6 脚本调试三板斧
Shell 脚本难调试——没有 IDE 断点、没有漂亮的调用栈。但有三招能定位 90% 的问题:
#!/bin/bash
# ===== 第一招:set -x —— 打印每条执行命令 =====
set -x
name="张三"
echo "Hello, $name"
set +x # 关闭调试模式
# + name=张三
# + echo 'Hello, 张三'
# Hello, 张三
# + set +x
# ===== 第二招:set -e —— 遇错即停 =====
set -e # 任何一条命令返回非 0 就退出
cd /nonexistent # ← 这条失败了——脚本立即退出,不会继续
echo "不会执行到这里"
# ===== 第三招:set -u —— 未定义变量报错 =====
set -u
echo "$UNDEFINED_VAR" # ← bash: UNDEFINED_VAR: unbound variable
# ===== 生产级脚本开头 =====
#!/bin/bash
set -euo pipefail # 三合一:遇错停 + 未定义报错 + 管道失败也报错
# -e: 命令失败停止
# -u: 使用未定义变量报错
# -o pipefail: 管道中任一命令失败都算失败
# 演示 pipefail 的必要性
set -e
cat nonexistent.txt | wc -l # ❌ 不加 pipefail:cat 失败但 wc -l 成功,脚本继续
echo "这行仍然执行了——危险!" # ← 不应该执行到这里
set -eo pipefail
cat nonexistent.txt | wc -l # ✅ 加了 pipefail:cat 失败 → 整条管道失败 → 脚本退出
echo "这行不会执行" # ← 正确行为
# 1.2 变量与替换
# 1.2.1 变量定义与引用
Shell 变量的核心规则——等号两边不能有空格(这是 Shell 最反直觉的规矩):
#!/bin/bash
# ===== 定义 =====
name="张三" # ✅ 等号两边无空格——Shell 铁律!
age=25
readonly PI=3.14159 # 只读变量——不能修改
# PI=3.14 # ❌ bash: PI: readonly variable
# ===== 引用——必须加 $ =====
echo $name # 张三
echo "我的名字是 $name" # 双引号内变量展开
echo '我的名字是 $name' # 单引号内——不展开!输出「我的名字是 $name」
# ===== 删除变量 =====
unset name
echo "name = ${name:-未定义}" # name = 未定义
# ===== 花括号——消除歧义 =====
animal="dog"
echo "I have 4 ${animal}s" # I have 4 dogs(花括号告诉 Shell 变量名边界)
echo "I have 4 $animals" # I have 4 ($animals 不存在——空)
🔑 单引号 vs 双引号——Shell 最重要的概念之一:
name="张三"
echo "Hello $name" # Hello 张三——双引号:变量展开、转义生效
echo 'Hello $name' # Hello $name——单引号:一切原样输出
echo Hello $name # Hello 张三——无引号:变量展开,但空格导致分词
# 区别在这一点上最明显:
echo "$USER's home is $HOME" # yangchong's home is /home/yangchong
echo '$USER'"'s home is $HOME" # $USER's home is /home/yangchong(拼接技巧)
# 1.2.2 环境变量 vs 局部变量
#!/bin/bash
# ===== 局部变量——仅当前 Shell 可见 =====
local_var="只有我能看见"
# ===== 环境变量——子进程也能看到 =====
export GLOBAL_VAR="我和我的子进程都能看见"
# 或两行:GLOBAL_VAR="..." ; export GLOBAL_VAR
# ===== 验证——在子 Shell 中查看 =====
bash -c 'echo "子进程中:local_var=$local_var, GLOBAL_VAR=$GLOBAL_VAR"'
# 输出:子进程中:local_var=, GLOBAL_VAR=我和我的子进程都能看见
# ↑ 看不到! ↑ 能看到!
# ===== 查看所有环境变量 =====
env # 或 printenv
echo $PATH # 最重要——命令搜索路径
echo $HOME # 用户主目录
echo $USER # 当前用户名
常用环境变量速查:
| 变量 | 含义 | 示例值 |
|---|---|---|
$HOME | 用户主目录 | /home/yangchong |
$PATH | 命令搜索路径 | /usr/bin:/bin:/usr/local/bin |
$USER | 当前用户名 | yangchong |
$PWD | 当前工作目录 | /home/yangchong/project |
$SHELL | 当前 Shell | /bin/zsh |
$LANG | 语言/区域 | zh_CN.UTF-8 |
$PS1 | 命令提示符 | \u@\h:\w$ |
$? | 上条命令退出码 | 0 成功 / 非 0 失败 |
# 1.2.3 特殊变量速查
Shell 提供了一组以 $ 开头的特殊变量——脚本参数和状态的全部信息:
#!/bin/bash
# special_vars.sh
echo "脚本名:$0" # ./special_vars.sh
echo "第 1 个参数:$1" # 运行时 ./special_vars.sh a b c → a
echo "第 2 个参数:$2" # b
echo "参数个数:$#" # 3
echo "所有参数(空格分隔):$*" # a b c
echo "所有参数(独立引号):$@" # a b c(配合 for 用——见下)
echo "上一个命令的退出码:$?" # 0
echo "当前 Shell PID:$$" # 12345
# $* vs $@ ——在带引号时有本质区别
# $* = "a b c"(所有参数合并成一个字符串)
# $@ = "a" "b" "c"(每个参数独立——遍历安全)
# 实战:脚本参数处理
#!/bin/bash
set -euo pipefail
if [ $# -lt 2 ]; then
echo "用法:$0 <输入文件> <输出目录>"
exit 1
fi
input="$1"
output_dir="$2"
echo "正在处理 $input → $output_dir/"
# ...
# 1.2.4 命令替换 $() 与反引号
命令替换 = 把命令的输出当作字符串塞进变量:
#!/bin/bash
# ===== 方式 1:$() —— 推荐!支持嵌套 =====
today=$(date '+%Y-%m-%d')
file_count=$(ls | wc -l)
line_count=$(wc -l < /var/log/app.log) # < 防止文件名也输出
# 嵌套——$() 天然支持
nested=$(echo "文件数:$(ls | wc -l)")
# ===== 方式 2:反引号 `` —— 不推荐——嵌套困难 =====
today=`date '+%Y-%m-%d'` # 和 $() 效果相同
# nested=`echo "文件数:\`ls | wc -l\`"` # 嵌套需要转义——可读性差
echo "今天是 $today,当前目录有 $file_count 个文件"
🔑 $() vs 反引号:
$() | 反引号 `` | |
|---|---|---|
| 嵌套 | $(echo $(date))——自然 | 需要转义 |
| 可读性 | ✅ 一眼看出起止 | ❌ 容易漏看 |
| 所有 POSIX Shell | sh 不支持(bash 支持) | sh/bash 都支持 |
📌 新代码一律用
$()。
# 1.2.5 变量替换与默认值
Shell 的 ${} 远不止消除歧义——它有一整套变量替换语法:
#!/bin/bash
name="张三"
# ===== 1. 提供默认值(变量未定义/为空时使用)=====
echo "姓名:${name:-匿名}" # name 有值 → 张三
unset name
echo "姓名:${name:-匿名}" # name 无值 → 匿名
# ===== 2. 赋值默认值(:=——同时赋值给变量)=====
unset count
echo "数量:${count:=10}" # 10——并且 count 被赋值为 10
echo "count=$count" # count=10
# ===== 3. 变量必须存在(:?——否则报错退出)=====
# echo "${REQUIRED_VAR:?必须设置 REQUIRED_VAR}" # bash: REQUIRED_VAR: 必须设置 REQUIRED_VAR
# ===== 4. 有值就用另一个值(:+——替代值)=====
name="张三"
echo "欢迎 ${name:+尊敬的 $name}" # name 有值 → 尊敬的 张三
unset name
echo "欢迎 ${name:+尊敬的 $name}" # name 无值 → (空)
# ===== 5. 字符串操作 =====
file="backup_2025.tar.gz"
echo "${file%.tar.gz}" # backup_2025(从末尾删除最短匹配)
echo "${file%%.*}" # backup_2025(从末尾删除最长匹配)
echo "${file#backup_}" # 2025.tar.gz(从开头删除最短匹配)
echo "${file##*_}" # 2025.tar.gz(从开头删除最长匹配)
echo "${#file}" # 19——字符串长度
echo "${file:0:6}" # backup——子串(位置 0,长度 6)
# 1.2.6 算术运算
Shell 的算术运算不能直接写 x = 1 + 1——需要用 $(( )):
#!/bin/bash
# ===== $(( )) —— 算术运算(只支持整数!)=====
a=10
b=3
echo $((a + b)) # 13
echo $((a - b)) # 7
echo $((a * b)) # 30
echo $((a / b)) # 3(整数除法)
echo $((a % b)) # 1(取余)
echo $((a ** b)) # 1000(幂——a 的 b 次方)
# 变量前缀 $ 可省略
echo $((a + 5)) # 15
# 自增/自减
((a++))
echo $a # 11
((a += 5))
echo $a # 16
# if 里的条件判断——用 (( ))
if ((a > 10)); then
echo "a 大于 10"
fi
# ===== let 命令(老语法——不推荐)=====
let "c = a + b"
echo $c # 和 $(()) 结果一样
⚠️ Shell 没有原生浮点运算——需要用外部工具:
# bc 计算器——最常用
echo "scale=2; 10 / 3" | bc # 3.33
pi=$(echo "scale=4; 4*a(1)" | bc -l) # 3.1416
# awk
awk 'BEGIN {print 10/3}' # 3.33333
# 1.2.7 数组
Bash 支持一维数组——语法和 Python 差别很大:
#!/bin/bash
# ===== 定义 =====
fruits=("苹果" "香蕉" "橘子" "葡萄") # 小括号,空格分隔
nums=(1 2 3 4 5)
mixed=("hello" 42 "world") # 可以混类型(但不推荐)
# 也可逐个赋值
fruits[4]="西瓜" # 追加到索引 4
# ===== 访问 =====
echo "${fruits[0]}" # 苹果(第一个元素)
echo "${fruits[1]}" # 香蕉
echo "${fruits[-1]}" # 西瓜(最后一个——bash 4.2+)
# ===== 遍历 =====
echo "--- 遍历方式 1:取所有元素 ---"
echo "${fruits[@]}" # 苹果 香蕉 橘子 葡萄 西瓜
echo "--- 遍历方式 2:for 循环 ---"
for fruit in "${fruits[@]}"; do
echo " 水果:$fruit"
done
# ===== 数组长度 =====
echo "数组长度:${#fruits[@]}" # 5
# ===== 切片 =====
echo "${fruits[@]:1:2}" # 香蕉 橘子(从索引 1 开始,取 2 个)
# ===== 删除 =====
unset fruits[2] # 删除索引 2(橘子)
echo "${fruits[@]}" # 苹果 香蕉 葡萄 西瓜(索引不连续了)
🔑 关联数组(类似 Python dict)——bash 4.0+:
#!/bin/bash
# 声明关联数组——必须用 declare -A
declare -A student
student["name"]="张三"
student["age"]="25"
student["city"]="深圳"
echo "姓名:${student[name]}" # 张三
echo "年龄:${student[age]}" # 25
# 遍历关联数组
for key in "${!student[@]}"; do
echo " $key = ${student[$key]}"
done
# name = 张三
# age = 25
# city = 深圳
# 1.2.8 Here Document 与交互
<< (Here Document)让你在脚本里嵌入多行文本——告别一堆 echo:
#!/bin/bash
# ===== Here Document:多行输出 =====
cat <<EOF
这是第一行
这是第二行
变量也会被展开:当前用户是 $USER
EOF
# 输出:
# 这是第一行
# 这是第二行
# 变量也会被展开:当前用户是 yangchong
# ===== 阻止变量展开——定界符加引号 =====
cat <<'EOF'
变量不会被展开:当前用户是 $USER
EOF
# 输出:变量不会被展开:当前用户是 $USER
# ===== <<- 可选:自动忽略行首 Tab =====
if true; then
cat <<-EOF
这行的 Tab 会被去掉
这一行也是
EOF
fi
# 输出(无缩进):
# 这行的 Tab 会被去掉
# 这一行也是
# ===== 实战:生成配置文件 =====
port=8080
db_name="myapp"
cat > config.yaml <<EOF
server:
port: $port
database:
name: $db_name
host: localhost
EOF
# 直接写入 config.yaml 文件!
🔑 Here String <<<——把字符串当作 stdin 传给命令:
# 不用 echo + 管道
echo "hello world" | wc -c # 12
# 用 Here String——更简洁
wc -c <<< "hello world" # 12
# 实战:把变量传给命令
read -r first rest <<< "apple banana orange"
echo "first=$first, rest=$rest" # first=apple, rest=banana orange
read 命令——带提示的输入:
#!/bin/bash
# 基础输入
echo -n "请输入你的名字:"
read -r name
echo "你好,$name!"
# 一行搞定——-p 提示
read -r -p "请输入年龄:" age
echo "你 ${age} 岁了"
# 限定最大字符数——密码输入
read -r -s -p "请输入密码:" password # -s:输入不显示(silent)
echo "" # 换行
echo "密码已接收(长度 ${#password})"
# 按分隔符拆分
read -r -p "输入姓名和年龄(空格分隔):" name age
echo "姓名:$name,年龄:$age"
# 1.2.9 综合案例:系统巡检脚本
把本章所有知识串联成一个生产级脚本——每天自动检查系统健康状态:
#!/bin/bash
# system_check.sh —— 系统巡检脚本
# 用法:./system_check.sh [输出文件名]
# 示例:./system_check.sh report_$(date +%Y%m%d).txt
set -euo pipefail
# ===== 1. 配置:变量 + 默认值 =====
OUTPUT_FILE="${1:-report.txt}" # 输出文件名——默认 report.txt
HOSTNAME=$(hostname) # 主机名(命令替换)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') # 时间戳
# 阈值配置(环境变量可覆盖)
CPU_THRESHOLD="${CPU_THRESHOLD:-80}" # CPU 使用率超过 80% 告警
MEM_THRESHOLD="${MEM_THRESHOLD:-85}" # 内存使用率超过 85% 告警
DISK_THRESHOLD="${DISK_THRESHOLD:-90}" # 磁盘使用率超过 90% 告警
LOG_KEEP_DAYS="${LOG_KEEP_DAYS:-30}" # 日志保留天数
# ===== 2. 函数:带颜色输出 =====
# 颜色变量
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color——恢复默认
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# ===== 3. 检查函数 =====
check_cpu() {
# 获取 CPU 使用率(取 100 - idle)
local cpu_idle
cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d'.' -f1)
local cpu_usage=$((100 - cpu_idle))
echo "CPU 使用率:${cpu_usage}%"
if (( cpu_usage > CPU_THRESHOLD )); then
log_warn "CPU 使用率 ${cpu_usage}% 超过阈值 ${CPU_THRESHOLD}%!"
else
log_info "CPU 使用率正常"
fi
}
check_memory() {
# 从 free 命令提取内存使用率
local total used usage_pct
read -r _ total used _ <<< "$(free -m | grep Mem)"
usage_pct=$(( used * 100 / total ))
echo "内存使用率:${usage_pct}%(已用 ${used}MB / 总量 ${total}MB)"
if (( usage_pct > MEM_THRESHOLD )); then
log_warn "内存使用率 ${usage_pct}% 超过阈值 ${MEM_THRESHOLD}%!"
else
log_info "内存使用率正常"
fi
}
check_disk() {
# 检查根分区使用率
local usage_pct
usage_pct=$(df -h / | tail -1 | awk '{print $5}' | sed 's/%//')
echo "磁盘使用率:${usage_pct}%"
if (( usage_pct > DISK_THRESHOLD )); then
log_error "磁盘使用率 ${usage_pct}% 超过阈值 ${DISK_THRESHOLD}%——需要立即清理!"
elif (( usage_pct > DISK_THRESHOLD - 10 )); then
log_warn "磁盘使用率 ${usage_pct}% 接近阈值——建议清理"
else
log_info "磁盘空间充足"
fi
}
check_services() {
# 检查关键服务是否在运行
local services=("sshd" "nginx" "cron")
local failed=()
for svc in "${services[@]}"; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo " ✅ $svc"
else
echo " ❌ $svc(未运行!)"
failed+=("$svc")
fi
done
if [ ${#failed[@]} -gt 0 ]; then
log_error "以下服务未运行:${failed[*]}"
else
log_info "所有关键服务正常运行"
fi
}
check_logs() {
# 检查是否有异常日志(最近 1 小时)
local recent_errors
recent_errors=$(find /var/log -name "*.log" -mmin -60 \
-exec grep -l "ERROR\|FATAL\|CRITICAL" {} \; 2>/dev/null | wc -l)
echo "最近 1 小时出现 ERROR 的日志文件数:${recent_errors}"
if (( recent_errors > 0 )); then
log_warn "发现 ${recent_errors} 个日志文件包含 ERROR——请检查详情"
else
log_info "近期无 ERROR 日志"
fi
}
cleanup_old_logs() {
# 清理旧日志
local count
count=$(find /var/log -name "*.log" -mtime "+${LOG_KEEP_DAYS}" 2>/dev/null | wc -l)
echo "超过 ${LOG_KEEP_DAYS} 天的日志文件:${count} 个"
}
# ===== 4. 主流程 =====
{
echo "========================================"
echo " 系统巡检报告"
echo " 主机:${HOSTNAME}"
echo " 时间:${TIMESTAMP}"
echo "========================================"
echo ""
echo "【CPU 检查】"
check_cpu
echo ""
echo "【内存检查】"
check_memory
echo ""
echo "【磁盘检查】"
check_disk
echo ""
echo "【服务检查】"
check_services
echo ""
echo "【日志检查】"
check_logs
echo ""
echo "【清理建议】"
cleanup_old_logs
echo ""
echo "========================================"
echo " 巡检结束"
echo "========================================"
} | tee "$OUTPUT_FILE" # tee:同时输出到屏幕和文件
echo ""
log_info "报告已保存至:$OUTPUT_FILE"
# 退出码:0=健康,1=有告警
if grep -q "WARN\|ERROR" "$OUTPUT_FILE"; then
exit 1 # 有告警——退出码 1(CI 流水线可以根据这个判断失败)
else
exit 0 # 一切正常
fi
案例知识融合:这个脚本覆盖了本章全部核心知识——shebang + set -euo pipefail 三板斧、变量定义/引用/默认值/环境变量/花括号、命令替换 $()、$1 参数处理、局部变量 local、数组定义/遍历/长度、算术运算 $(())、特殊变量 $? 退出码——100 行完成了一个可直接用于生产环境的系统巡检脚本。
🔑 学完这个案例你就掌握了 Shell 脚本的"骨架模式"——所有 Shell 脚本都是:配置变量 → 定义函数 → 主流程调用。
# 1.3 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 等号两边有空格 | name = "张三" → Shell 把 name 当命令执行。必须是 name="张三" |
| 2 | 变量引用忘加 $ | echo name 打印的是字面量 name,不是变量值。echo $name 才对 |
| 3 | [ 两边必须有空格 | if [ $a == $b ]——[ 是一个命令![$a 会被当作一条命令 |
| 4 | 单引号阻止变量展开 | echo '$USER' → $USER(原样输出)。变量展开用双引号 |
| 5 | 未加引号的变量被分词 | for f in $files——如果文件名包含空格会被拆散。用 "$files" 或 "${array[@]}" |
陷阱 3 详解——[ 是一个命令:
# ❌ 少空格——bash 报错
if [$a == $b ]; then # bash: [a: command not found
echo "相等"
fi
# ✅ 正确——[ 是 test 命令的别名,必须用空格和参数分开
if [ "$a" == "$b" ]; then
echo "相等"
fi
# 验证:[ 真的是一个命令
which [ # /bin/[
ls -l /bin/[ # -rwxr-xr-x /bin/[
陷阱 5 详解——空格分词:
# ❌ 没有引号——文件名包含空格就炸了
files=$(ls) # file1.txt my doc.pdf photo.jpg
for f in $files; do # 迭代:file1.txt → my → doc.pdf → photo.jpg(被拆散了!)
echo "处理:$f"
done
# ✅ 加引号——正确处理空格
for f in *.pdf; do # 直接用 glob——原生支持空格
echo "处理:$f"
done
# 1.4 综合思考题
sourcevs./的本质差异:source script.sh不创建子进程——这意味着脚本可以修改当前 Shell 的环境变量(如PATH)。为什么~/.bashrc必须用source而不是./加载?如果反过来会怎样?set -e的"假安全":set -e会让脚本在命令失败时退出——但管道中最后一个命令失败不会触发(除非加-o pipefail)。还有哪些命令不触发set -e?(提示:if/while条件、&&/||左侧)$@vs$*的深度差异:"$@"展开为每个参数分别引号括起、"$*"展开为一个字符串——这导致在for arg in "$@"; do和for arg in "$*"; do中,引号内的空格会不会被分词。实验验证二者在处理./script.sh "a b" c时的输出差异。Shell 脚本的 Python 替代:超过 200 行的 Shell 脚本通常难以维护——没有真正的数据结构、错误处理原始、语法反直觉。什么情况下应该用 Python 替代 Shell 脚本?什么情况下 Shell 仍然是最优选择?
shebang 的历史与设计:
#!是 Dennis Ritchie 在 1980 年引入的——#是注释符、!是"魔法数"。同一行#!/bin/bash,Shell 看到的是#注释行(不执行),内核看到#!就知道要用/bin/bash来解释这个文件。除了#!外,还有哪些"同一行、不同角色看到不同含义"的计算机黑魔法?(提示:HTTP 的 Content-Type、ELF 文件头)