1. 变量基础与赋值
一句话总结: Shell 变量没有类型,一切皆字符串;赋值时等号两侧不能有空格,读取时要在变量名前加 $。
Shell 变量分为环境变量、普通变量和特殊变量三类。环境变量通过 export 传递给子进程,普通变量只在当前进程内可见。
# 赋值:等号两侧不能有空格
name="plume" # ✅ 正确
# name = "plume" # ❌ 会被解析成执行命令 name 加参数
# 读取:前面加 $,推荐用花括号隔离边界
echo "$name"
echo "欢迎 ${name} 用户" # 花括号防止 name 与 "用户" 粘连成新变量名
# 环境变量:export 后子进程可见
export PATH="$PATH:/opt/mybin"
1.1 特殊变量速查
| 变量 | 含义 | 例子 |
|---|---|---|
$0 | 脚本名 | ./build.sh |
$1~$9 | 位置参数 | 第 1 到第 9 个参数 |
$# | 参数个数 | 3 |
$@ | 全部参数,逐个独立 | "a" "b" "c" |
$* | 全部参数,合并成串 | "a b c" |
$$ | 当前进程 PID | 12345 |
$? | 上一条命令退出码 | 0 成功 |
$! | 最近后台任务 PID | 12346 |
2. 词分裂与引号差异
一句话总结: 不加引号的变量会被 IFS 拆分,这就是词分裂;双引号保留一个单词但展开变量,单引号全部字面量。
词分裂(Word Splitting) 是 Shell 新手最常踩的坑:未加引号的 $var 会按 IFS(默认空格、制表符、换行)拆成多个词。
dirs="one two three"
# 无引号:拆成 3 个参数
for d in $dirs; do echo "dir=$d"; done
# 加引号:当成 1 个整体
for d in "$dirs"; do echo "whole=$d"; done
2.1 三种引号的语义对比
| 写法 | 行为 | 示例输出 |
|---|---|---|
$var | 展开并词分裂 | one two three(3 个词) |
"$var" | 展开但不分裂 | one two three(1 个词) |
'$var' | 完全不展开 | 字面量 $var |
msg="hello world"
printf '<%s>\n' $msg # <hello> <world> 词分裂
printf '<%s>\n' "$msg" # <hello world> 整体
printf '%s\n' '$msg' # $msg 字面量
# 参数数组也一样:$@ 与 "$@" 是天壤之别
set -- "a b" "c d"
printf '<%s>\n' $@ # <a> <b> <c> <d> 分裂
printf '<%s>\n' "$@" # <a b> <c d> 保留
一句话总结: 除极少数故意要词分裂的场景,变量一律加双引号。文件路径、日志消息、用户输入尤其必须加。
3. 变量展开高级语法
一句话总结:
${var:-默认值}系列语法在参数缺失时兜底,是脚本健壮性的第一道防线。
Bash 提供一套强大的参数展开语法,让"变量可能不存在"的情况变得可控:
# :- 有值用值,无值用默认(变量保持原状)
echo "${USER:-guest}"
# := 无值时顺带把默认值写回变量
echo "${port:=8080}"
# :? 无值时报错退出(调试利器)
echo "${required_var:?必须设置}"
# :+ 有值才替换,无值留空
echo "${DEBUG:+已开启调试}"
3.1 子串与替换
| 写法 | 作用 | 例子 |
|---|---|---|
${#var} | 字符串长度 | ${#name} → 5 |
${var:2} | 从第 3 个字符截断 | ${name:2} |
${var:2:3} | 截取 3 个字符 | ${name:2:3} |
${var#pattern} | 去最短前缀 | ${url#*/} |
${var##pattern} | 去最长前缀 | ${url##*/} |
${var%pattern} | 去最短后缀 | ${file%.*} |
${var%%pattern} | 去最长后缀 | ${file%%.*} |
${var/a/b} | 替换首个 | ${url/http/https} |
file="archive.tar.gz"
echo "${file%.*}" # archive.tar 去最短后缀
echo "${file%%.*}" # archive 去最长后缀
echo "${file##*.}" # gz 取扩展名
# 大小写转换(Bash 4+)
echo "${name^^}" # 全大写
echo "${name,,}" # 全小写
4. 条件判断 if 与 test
一句话总结:
[[ ]]比[ ]更强也更安全,数字比较用-eq、字符串用=、文件用-f等测试符。
if 的骨架是 if 命令; then ...; else ...; fi。命令的退出码 0 为真,非 0 为假。
if grep -q "error" /var/log/app.log; then
echo "日志中存在 error"
fi
# 常见写法:if test 条件
if [ "$count" -gt 5 ]; then
echo "count 大于 5"
elif [ "$count" -eq 5 ]; then
echo "正好 5"
else
echo "小于 5"
fi
4.1 测试操作符
| 类型 | 操作符 | 含义 |
|---|---|---|
| 数字 | -eq -ne -gt -ge -lt -le | 等于/不等/大于/… |
| 字符串 | = != -z -n | 相等/不等/为空/非空 |
| 文件 | -f -d -x -r -w -s | 是文件/目录/可执行/可读/可写/非空 |
| 组合 | && || | 且 / 或 |
# 推荐使用 [[ ]],支持模式匹配与正则
[[ "$name" == p* ]] && echo "以 p 开头"
[[ "$url" =~ ^https?:// ]] && echo "合法 URL"
# 防御性写法:变量加双引号避免 test 报错
[ -n "$var" ] && echo "非空"
5. 循环 for while until
一句话总结: for 遍历列表,while 依赖条件为真继续,until 条件为真退出,各自适合不同的循环场景。
# 标准 for
for env in dev test prod; do
echo "部署到 $env"
done
# 花括号与 seq 生成序列
for i in {1..5}; do
echo "i=$i"
done
# C 风格 for(Bash 支持)
for ((i=0; i<10; i++)); do
printf '%d ' "$i"
done
echo
5.1 while 与 read 逐行处理
# 逐行读取文件(核心惯用法)
while IFS= read -r line; do
printf '处理: %s\n' "$line"
done < config.txt
# until:直到条件成立才退出
until ping -c 1 db-host >/dev/null 2>&1; do
echo "等待数据库..."
sleep 2
done
一句话总结: 处理文件请用
while IFS= read -r line,-r防止反斜杠被吞,IFS=保留行首行尾空格。
6. case 多分支
一句话总结: case 用模式匹配做分支,天然支持通配符与
|合并,比一串 if/elif 清晰得多。
case "$1" in
start)
echo "启动服务"
;;
stop|shutdown)
echo "停止服务"
;;
restart)
echo "重启服务"
;;
*)
echo "用法: $0 {start|stop|restart}"
exit 1
;;
esac
6.1 模式匹配要点
| 模式 | 匹配内容 | 说明 |
|---|---|---|
*.tar.gz | 以 .tar.gz 结尾 | 通配符 |
dev|prod | dev 或 prod | 竖线合并 |
[0-9]* | 数字开头 | 字符类 |
? | 单个字符 | 任意单字符 |
# 判断扩展名
case "$file" in
*.md) echo "Markdown";;
*.sh) echo "Shell 脚本";;
*) echo "未知类型";;
esac
# case 里也能用变量展开与命令替换
case "$(date +%u)" in
6|7) echo "周末";;
*) echo "工作日";;
esac
7. 函数定义与作用域
一句话总结: 函数像小脚本:local 声明局部变量,return 返回退出码,
$1在函数内指函数参数而非脚本参数。
# 两种定义等价,推荐 function 关键字风格
log() {
local level="$1"
shift
echo "[$level] $*"
}
deploy() {
local app="$1"
if [[ -z "$app" ]]; then
return 1 # 返回退出码 1
fi
echo "部署 $app"
}
log INFO "开始构建"
deploy "web" && log OK "部署完成"
7.1 函数作用域规则
| 规则 | 说明 |
|---|---|
local | 变量只在本函数及子调用可见 |
| 全局 | 不加 local 的变量默认全局 |
| 位置参数 | 函数内 $1 $@ 覆盖为函数参数 |
return | 返回 0~255 的退出码 |
export -f | 把函数导出给子 shell 使用 |
counter=0
increment() {
local step="${1:-1}" # local 局部
counter=$((counter + step)) # 修改全局
}
increment 2
echo "counter=$counter" # 2
# 函数名冲突防护:命名带前缀
# 如 mylib_connect / mylib_close,避免覆盖系统函数
一句话总结: 函数内改全局变量要谨慎,配合
local声明临时变量,函数间的副作用会大幅减少。
8. 总结
| 环节 | 要点 |
|---|---|
| 变量 | 无类型全字符串,赋值无空格,读取加 $ 与花括号 |
| 词分裂 | 不引号被 IFS 拆分,"$@" 与 $@ 天壤之别 |
| 默认值 | ${var:-默认} 兜底,:? 报错,#/% 截断 |
| if 判断 | [[ ]] 优先,数字 -eq、字符串 =、文件 -f |
| 循环 | for 遍历、while+read 逐行、until 等条件 |
| case | 模式匹配分支,` |
| 函数 | local 隔离、return 退出码、参数按位取 |
变量、流程控制与函数是 Shell 脚本的地基。引号习惯(一律加双引号)和默认值兜底(:? :-)这两件事做对了,脚本的健壮性就赢了一大半。下一步可以进入文本处理,让 awk 与 sed 接管数据加工。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。