2.1 Shell 脚本入门
预计阅读时间:20 分钟
📖 目录
前置知识
- 1.9:重定向与管道 重定向与管道:理解 stdin/stdout/stderr、
|管道、>/>>重定向、$?退出码——这些是 Shell 脚本的基础构建块 - 1.6:文件查看与文本编辑 文件查看与文本编辑:熟悉 nano/vim 基本操作,用于编写和编辑脚本文件
学习目标
学完本章后,你将能够:
- 编写并执行第一个 Bash 脚本,理解 shebang(
#!/bin/bash)的作用 - 使用变量(定义、引用、默认值、特殊变量)存储和操作数据
- 使用
if/case条件分支实现逻辑判断 - 使用
for/while/until循环处理批量任务 - 定义和调用函数,理解局部变量与全局变量的区别
- 使用
set -euo pipefail严格模式编写健壮的脚本 - 使用
trap处理信号和清理临时文件
知识关联
- 前置知识:1.9:重定向与管道 重定向与管道($? 退出码、&&/|| 条件链);1.6:文件查看与文本编辑 文件查看与文本编辑(vim 编辑脚本)
- 后续影响:2.8:Shell 脚本进阶 Shell 脚本进阶(数组、trap、进程替换、flock);4.19:Shell 实战案例集 Shell 实战案例集(生产级脚本)
- 配套技术:2.10:正则表达式 正则表达式(在脚本中用 =~ 做模式匹配);3.15:Cron 定时任务 Cron 定时任务(用 cron/systemd timer 调度脚本)
- 在整个体系中的位置:Shell 脚本是 Linux 自动化的基石——从简单的文件批处理到复杂的运维流程,都离不开脚本。掌握脚本能力是从"会用命令"到"自动化运维"的分水岭
核心知识
- Shell 脚本(Shell Script)——将一系列命令写入文件,由 Shell 解释执行。相当于"命令的批处理文件"
- Shebang(#!)——脚本第一行
#!/bin/bash,告诉系统用哪个解释器执行此脚本 - 变量(Variable)——存储数据的容器,分为局部变量、环境变量、特殊变量
- 条件判断(Conditional)——
if/case根据条件执行不同分支 - 循环(Loop)——
for/while/until重复执行一段代码 - 函数(Function)——将一段代码封装为可复用的模块
- 退出码(Exit Code)——脚本执行完毕返回给调用者的状态码,0=成功,非0=失败
- 严格模式(Strict Mode)——
set -euo pipefail让脚本在遇到错误时立即终止,避免"静默失败"
原理讲解
Shell 的本质是"把命令行操作自动化"——变量让你记住命令的结果(如 files=$(ls *.txt)),循环让你批量执行操作(如 for f in *.txt; do wc -l "$f"; done)。如果没有变量,你每次都要重新执行命令获取结果;如果没有循环,100 个文件就要手动执行 100 次。Shell 的变量和循环不是"编程语言的简化版",而是"命令行操作的自然延伸"——它们的设计目标是让系统管理员用最少的代码完成最多的自动化任务。
1. 第一个 Shell 脚本——shebang 与执行方式
Shell 脚本本质上就是一个包含命令的文本文件,交给 Shell 逐行解释执行。创建脚本的第一步是写好 shebang 行:
#!/bin/bash
# 第一个脚本:hello.sh
echo "Hello, Linux!"
echo "当前用户: $(whoami)"
echo "当前时间: $(date)"
Shebang(#!):脚本第一行。它以 # 开头(所以是"注释"),但含义特殊——告诉内核"用哪个解释器来执行这个文件"。#!/bin/bash 表示用 Bash,#!/bin/sh 表示用 POSIX sh,#!/usr/bin/env python3 表示用 PATH 中的 python3。
执行脚本的三种方式:
# 方式 1:给执行权限后直接执行(推荐)
chmod +x hello.sh
./hello.sh
# 方式 2:用解释器显式执行(不需要执行权限)
bash hello.sh
# 方式 3:在当前 Shell 中执行(脚本中的变量/函数会留在当前 Shell)
source hello.sh # 等价于 . hello.sh
三种方式的区别:方式 1 和 2 都在子进程中执行脚本——脚本里定义的变量、cd 等不会影响当前 Shell;方式 3 在当前 Shell 中执行,适合加载配置。
Shell 脚本的优势:① 零依赖——每个 Linux 系统都自带 Bash;② 管道组合能力极强——ps aux | grep nginx | awk '{print $2}' 这种一行命令是 Shell 的天然优势;③ 系统管理原生命令(systemctl、ss、ip)配合 Shell 脚本无缝集成。Python 脚本的优势:① 复杂逻辑和数据结构(JSON 处理、API 调用、数据库操作)远超 Shell;② 错误处理更健壮(try/except vs set -e);③ 跨平台一致性更好。经验法则:Shell 擅长"把命令串起来",Python 擅长"把逻辑写清楚"。10 行以内的系统管理脚本用 Shell,超过 50 行或涉及复杂数据处理时考虑 Python。
2. 变量定义与引用
# 定义(等号两边绝对不能有空格!)
name="Alice"
age=25
# 引用(加 $ 前缀)
echo "$name" # 双引号内变量会展开
echo '$name' # 单引号内原样输出
# 花括号明确边界
echo "${name}Smith" # 输出 AliceSmith
echo "$nameSmith" # 输出空(nameSmith 未定义)
变量名的规则:只能由字母、数字、下划线组成,且不能以数字开头。引用时加 $ 前缀;字符串拼接场景用 ${var} 花括号明确边界。
3. 变量默认值与替换
# ${var:-default} —— 变量未设置或为空时使用默认值
echo "${DB_HOST:-localhost}"
# ${var:=default} —— 同上,但同时将默认值赋给变量
: "${DB_HOST:=localhost}"
# ${var:+value} —— 变量已设置时使用 value,否则为空
echo "${DEBUG:+Debug mode is ON}"
# ${var:?message} —— 变量未设置时报错退出
: "${CONFIG_FILE:?CONFIG_FILE is required}"
# 常见用法:脚本参数带默认值
BACKUP_DIR="${1:-/backup}"
RETENTION="${2:-30}"
默认值替换是脚本最常用的健壮性技巧——让脚本在没有参数或环境变量时也能安全运行。
4. 特殊变量全集
| 变量 | 含义 | 示例 |
|---|---|---|
$0 | 脚本自身名称 | ./script.sh |
$1 ~ $9 | 第1~9个位置参数 | |
${10} ~ | 第10个及以后(需花括号) | |
$# | 参数个数 | |
$@ | 所有参数,每个独立单词(推荐) | for arg in "$@"; do |
$* | 所有参数,合并为一个字符串 | |
$? | 上一条命令的退出状态码 | 0=成功,非0=失败 |
$$ | 当前 Shell 的进程 PID | |
$! | 最后一个后台命令的 PID | |
$_ | 上一条命令的最后一个参数 | |
$- | 当前 Shell 的选项标志 |
5. 命令替换
# $(command) —— 推荐,可嵌套
now=$(date +%Y%m%d)
files=$(ls /var/log | wc -l)
parent=$(dirname $(realpath "$0"))
# 旧式反引号写法,不可嵌套,不推荐
# now=`date +%Y%m%d` # 用 $() 替代
6. 条件判断(if/case)
[ ] vs [[ ]]
# [ ] 是 POSIX 标准命令(test 的别名),兼容所有 Shell
# [[ ]] 是 Bash 关键字,功能更强、更安全(推荐在 Bash 脚本中使用)
# [[ ]] 的优势:
# 1. 不需要引号保护空变量
[[ -z $var ]] # 即使 $var 为空也不会语法错误
[ -z "$var" ] # [ ] 必须加引号
# 2. 支持 && 和 || 逻辑运算
[[ -f $file && -r $file ]]
# 3. 支持 =~ 正则匹配
[[ $email =~ ^[a-z]+@[a-z]+.[a-z]+$ ]]
# 4. 支持词法排序比较
[[ "apple" < "banana" ]]
条件测试大全
| 数字测试 | 字符串测试 | 文件测试 |
|---|---|---|
-eq 等于 | = / == 等于 | -e 存在(任何类型) |
-ne 不等于 | != 不等于 | -f 普通文件 |
-gt 大于 | -z 字符串为空 | -d 目录 |
-lt 小于 | -n 字符串非空 | -L 符号链接 |
-ge 大于等于 | -r / -w / -x 可读/写/执行 | |
-le 小于等于 | -s 非空文件 | |
-nt / -ot 比...新/旧 |
if / elif / else
#!/bin/bash
read -p "输入数字: " num
if [[ $num -gt 10 ]]; then
echo "大于10"
elif [[ $num -eq 10 ]]; then
echo "等于10"
else
echo "小于10"
fi
# 一行写法
[[ -f config.ini ]] && echo "配置文件存在" || echo "配置文件缺失"
case 多分支匹配
#!/bin/bash
read -p "输入动作 (start|stop|restart|status): " action
case $action in
start|boot)
echo "启动服务..."
;;
stop|shutdown)
echo "停止服务..."
;;
restart|reload)
echo "重启服务..."
;;
status)
echo "服务状态: 运行中"
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
7. 算术运算
# (( )) 算术运算(仅整数)
a=10; b=3
echo $((a + b)) # 13
echo $((a / b)) # 3(整数除法)
echo $((a % b)) # 1(取余)
((a++)) # 自增
((a += 5)) # 复合赋值
# 浮点数运算用 bc 或 awk
echo "scale=2; 10/3" | bc
awk "BEGIN {printf \"%.2f\", 10/3}"
8. 循环(for/while/until)
for 循环
# 遍历列表
for fruit in apple banana orange; do
echo "$fruit"
done
# 花括号展开
for i in {1..10}; do echo "$i"; done
for i in {10..1..2}; do echo "$i"; done # 步长为2
# C 风格
for ((i=0; i<10; i++)); do
echo "$i"
done
# 遍历通配符(比解析 ls 输出更安全)
for file in *.txt; do
[[ -f "$file" ]] || continue
echo "处理: $file"
done
# 遍历命令输出(用 while read 更安全)
find . -name "*.log" -print0 | while IFS= read -r -d '' file; do
echo "$file"
done
while / until 循环
# while —— 条件为真时循环
count=1
while [[ $count -le 5 ]]; do
echo "计数: $count"
((count++))
done
# until —— 条件为假时循环(直到条件为真)
count=1
until [[ $count -gt 5 ]]; do
echo "计数: $count"
((count++))
done
# 逐行读取文件(最安全的读文件方式)
while IFS= read -r line || [[ -n "$line" ]]; do
echo "行内容: $line"
done < file.txt
# 无限循环
while true; do
check_status
sleep 60
done
循环控制:break / continue
# break N —— 跳出 N 层循环(默认 1 层)
for i in {1..5}; do
for j in {1..5}; do
if [[ $j -eq 3 ]]; then
break 2 # 跳出两层循环
fi
done
done
# continue —— 跳过当前迭代,继续下一次
for file in *; do
[[ -d "$file" ]] && continue # 跳过目录
echo "处理文件: $file"
done
9. 函数
函数定义与调用
# 定义(两种写法等价)
function greet { ... }
greet() { ... } # 推荐,POSIX 兼容
# 调用(不需要括号)
greet "Alice"
result=$(add 3 5) # 通过命令替换获取输出
# 函数内变量默认是全局的!
myfunc() {
name="Bob" # 全局变量!
}
myfunc
echo "$name" # 输出 Bob
函数参数与局部变量
greet() {
local name="$1" # local 声明局部变量
local age="${2:-18}" # 第二个参数带默认值
echo "Hello, $name ($age)"
}
greet "Alice" 25
# 函数内访问脚本参数需要传递
main() {
local script_arg="$1"
echo "Script arg: $script_arg"
}
main "$@" # 将脚本参数传给函数
函数返回值
# return 返回退出码(0-255 整数)
is_even() {
(( $1 % 2 == 0 )) && return 0 || return 1
}
if is_even 4; then echo "偶数"; fi
# echo 返回任意数据(通过命令替换获取)
get_timestamp() {
date +%Y%m%d_%H%M%S
}
ts=$(get_timestamp)
10. 输入输出——read、here-document 与 here-string
here-document 和 here-string 的基本语法在 1.9:重定向与管道 已讲过(<< 与 <<<),这里重点看它们在脚本中的典型用法:
Here Document(脚本中生成配置文件)
# 基本用法
cat << EOF > config.ini
[server]
host = localhost
port = 8080
EOF
# 带缩进(<<- 忽略前导 Tab)
cat <<- EOF
这行前面有 Tab
EOF
# 禁止变量展开(分隔符加引号)
cat << 'EOF'
$HOME 不会被展开
EOF
read —— 读取用户输入
# 基本读取
read -p "输入用户名: " username
# 静默输入(密码)
read -sp "输入密码: " password; echo
# 带超时(秒)
read -t 5 -p "5秒内输入: " answer
# 限制字符数
read -n 1 -p "按任意键继续..."
# 读取到数组
read -a arr <<< "one two three"
11. 信号处理(trap)
#!/bin/bash
# 定义清理函数
cleanup() {
echo "清理临时文件..."
rm -f /tmp/mytmp_$$
echo "退出。"
}
# 注册信号处理器
trap cleanup EXIT # 脚本退出时执行
trap 'echo "被中断!"; exit 1' INT TERM # Ctrl+C 或 kill 时
# 查看当前 trap 设置
trap -p
# 移除 trap
trap - EXIT
为什么需要 trap?脚本被 Ctrl+C 中断、或执行出错退出时,临时文件会残留在系统里。把清理动作注册到 EXIT 上,无论脚本怎么退出(正常结束、被信号中断、set -e 触发退出)都会执行清理。
12. 安全陷阱与最佳实践
#!/bin/bash
# 1. 严格模式(放在脚本开头)
set -euo pipefail
# -e: 任何命令失败立即退出
# -u: 使用未定义变量报错
# -o pipefail: 管道中任一命令失败则整体失败
# 2. 变量加引号(防止空格分词和通配符展开)
file="my document.txt"
rm "$file" # 正确
# rm $file # 错误!会删除 my 和 document.txt
# 3. 使用 [[ ]] 而非 [ ](Bash 脚本中)
if [[ -f "$file" && -r "$file" ]]; then ...
# 4. 路径用双引号包裹
cd "$HOME" || exit 1
# 5. 临时文件安全创建
tmpfile=$(mktemp) || exit 1
trap 'rm -f "$tmpfile"' EXIT
# 6. 检查命令是否存在
if ! command -v jq &> /dev/null; then
echo "请安装 jq: sudo apt install jq"
exit 1
fi
# 7. 脚本目录定位(处理符号链接)
SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
cd "$SCRIPT_DIR" || exit 1
set -euo pipefail 是生产脚本的"安全带",每个标志解决一类不同的"静默失败"问题:
-e(errexit):任何命令返回非零退出码时立即终止脚本。没有它,rm -rf /tmp/bak/如果目录不存在,脚本会继续执行后续的cp到那个不存在的目录——后果不堪设想。-u(nounset):引用未定义变量时报错退出。没有它,$BACKUP_DIR拼错成$BACKUP_DR时会静默展开为空字符串,rm -rf $BACKUP_DR/变成rm -rf /——这是真实的生产事故案例。-o pipefail:管道中任一命令失败则整体失败。默认情况下grep "pattern" file | wc -l中 grep 找不到匹配返回退出码 1,但 wc 成功返回 0,Shell 只看最后一个命令的退出码,所以set -e不会触发。pipefail修复了这个反直觉行为。
三者组合的效果:脚本从"尽力而为"变成"严格模式"——任何异常都立即暴露,而不是悄悄埋下隐患。
13. 子 Shell 与进程管理
# ( ) 创建子 Shell —— 不影响当前环境
(cd /tmp && ls) # cd 只在子 Shell 中生效
echo "$PWD" # 仍然是原目录
# { } 在当前 Shell 中执行命令组
{ cd /tmp && ls; }
echo "$PWD" # /tmp
# 后台执行与等待
long_task1 &
long_task2 &
wait # 等待所有后台任务完成
# 检查后台任务退出状态
long_task &
pid=$!
wait $pid
echo "Exit code: $?"
示例代码
下面是一个完整的实战示例——日志备份脚本。它用到了本章的大部分知识点:变量、默认值、条件判断、循环、函数、严格模式和 trap:
#!/bin/bash
# 示例:backup_logs.sh —— 备份指定目录下的日志并清理过期备份
set -euo pipefail
# ---- 配置(支持命令行参数覆盖默认值)----
SRC_DIR="${1:-/var/log}"
BACKUP_DIR="${2:-$HOME/log-backups}"
KEEP_DAYS="${3:-7}"
# ---- 函数 ----
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
# ---- 主流程 ----
# 1. 检查源目录
if [[ ! -d "$SRC_DIR" ]]; then
echo "错误: 源目录 $SRC_DIR 不存在" >&2
exit 1
fi
# 2. 创建备份目录(mkdir -p 幂等)
mkdir -p "$BACKUP_DIR"
# 3. 循环压缩 .log 文件
count=0
for file in "$SRC_DIR"/*.log; do
[[ -f "$file" ]] || continue
base=$(basename "$file" .log)
tar czf "$BACKUP_DIR/${base}_$(date +%Y%m%d).tar.gz" -C "$SRC_DIR" "$(basename "$file")"
((count++))
done
log "备份完成: $count 个文件 → $BACKUP_DIR"
# 4. 清理过期备份
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$KEEP_DAYS -delete
log "已清理 $KEEP_DAYS 天前的备份"
# 运行方式:
# ./backup_logs.sh /var/log ~/log-backups 7
# 输出示例:
# [2026-08-01 10:00:05] 备份完成: 3 个文件 → /home/alice/log-backups
# [2026-08-01 10:00:06] 已清理 7 天前的备份
独立练习:
# 练习 1:创建并运行你的第一个脚本
nano hello.sh
# 写入:#!/bin/bash
# echo "你好,$(whoami)!今天的学习目标是什么?"
chmod +x hello.sh
./hello.sh
# 练习 2:位置参数
# 写一个 add.sh:# echo "第一个参数: $1,第二个参数: $2,参数总数: $#"
./add.sh apple banana
# 练习 3:for 循环批量改扩展名(先备份再改)
mkdir -p /tmp/demo && cd /tmp/demo
touch a.txt b.txt c.txt
for f in *.txt; do
mv "$f" "${f%.txt}.bak"
done
ls # a.bak b.bak c.bak
# 练习 4:综合——系统健康检查(free/df/uptime + 条件判断)
# if [[ $(free -m | awk '/Mem:/{print $7}') -lt 500 ]]; then
# echo "警告:可用内存不足 500MB"
# fi
真实踩坑案例
# 错误写法
for file in $(ls /tmp); do
rm $file # 如果文件名含空格,$file 会被拆成多个参数
done
# 正确写法
for file in /tmp/*; do
[[ -f "$file" ]] && rm "$file" # 双引号保护空格
done
根因:Shell 默认按空格分词,$file 不加引号时,my file.txt 会被拆成 my 和 file.txt 两个参数。排查思路:用 set -x 开启调试输出,观察变量展开后的实际值。
#!/bin/bash
set -e
# 期望:grep 找不到匹配时脚本退出
grep "not_exist" /etc/passwd | wc -l
echo "这行仍然会执行!"
根因:默认情况下,管道只检查最后一个命令的退出码。wc -l 成功(退出码 0),所以 set -e 不会触发。修复:使用 set -o pipefail,管道中任一命令失败则整体失败。
#!/bin/bash
TMPFILE=/tmp/myapp_$$.tmp
trap "rm -f $TMPFILE" EXIT # 错误:$TMPFILE 在定义 trap 时就被展开了
# 正确写法:用单引号延迟展开
trap 'rm -f "$TMPFILE"' EXIT
根因:双引号内的变量在定义 trap 时就被展开为当前值,而不是执行时的值。排查思路:用 trap -p 查看实际注册的清理命令。
常见错误
| 错误/误区 | 解决方案 |
|---|---|
| "name = "Alice" 报 command not found" | 变量赋值时等号两边不能有空格。name = "Alice" 会被解析为执行 name 命令并传参。正确:name="Alice"。 |
| "变量值输出为空或按字面量输出" | 引用变量要加 $ 前缀:echo $name 输出值,echo name 输出字面量。字符串拼接用 ${name}xxx 明确边界。 |
| "./script.sh 报 Permission denied" | 脚本没有执行权限。运行 chmod +x script.sh,或用 bash script.sh 直接交给解释器执行。 |
| "Windows 上写的脚本在 Linux 上报 $'\r' 错误" | Windows 换行符是 \r\n,Linux 只认 \n。用 dos2unix script.sh 或 sed -i 's/\r$//' script.sh 转换。 |
| "[ $var ] 对空变量报错" | [ ] 是命令,空参数会导致语法错误。要么给变量加引号 [ -z "$var" ],要么直接用 [[ ]](推荐)。 |
| "函数里的变量泄漏到脚本其他地方" | 函数内变量默认是全局的。在函数内用 local 声明:local var="..."。 |
| "脚本命令失败但还继续执行" | 加上 set -euo pipefail 严格模式,让脚本在第一个错误处立即停止。 |
最佳实践
| # | 建议 | 说明 |
|---|---|---|
| 1 | 脚本开头写 set -euo pipefail | 让错误立即暴露,而不是悄悄带病运行。这是生产脚本的第一行纪律。 |
| 2 | 所有变量引用加双引号 | "$file" 防止空格分词和通配符展开。唯一例外是刻意要分词的情况。 |
| 3 | 用 [[ ]] 而非 [ ] | Bash 脚本中用 [[ ]]:支持正则、&&/||、空变量安全。 |
| 4 | 用 shellcheck 做静态检查 | sudo apt install shellcheck 后运行 shellcheck script.sh,自动发现绝大多数常见错误。 |
| 5 | 函数内用 local 声明变量 | 防止函数污染全局命名空间,也让函数可读性更强。 |
| 6 | 临时文件用 mktemp + trap 管理 | tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT——避免临时文件残留和文件名冲突。 |
| 7 | 调试用 bash -x | bash -x script.sh 打印每条命令展开后的实际执行过程,是定位问题最快的方式。 |
| 8 | 依赖的外部命令用 command -v 检查 | 脚本开头检查 jq、docker 等关键依赖是否安装,缺失时给出友好提示而不是中途报错。 |
练习题
- (概念)shebang 的作用是什么?
#!/bin/bash和#!/bin/sh有什么区别? - (概念)
$@和$*的区别是什么?为什么推荐使用"$@"? - (实操)写一个脚本
count_files.sh,接受一个目录路径作为参数,输出该目录下的文件数量(不含子目录)。提示:ls "$1" | wc -l或find "$1" -maxdepth 1 -type f | wc -l。 - (实操)写一个 for 循环脚本,把当前目录下所有
.txt文件重命名为.bak。要求文件名含空格时也能正确处理。 - (实操)写一个带
set -euo pipefail的备份脚本:接受目录参数,用 tar 打包到~/backups/(目录不存在则创建),完成后输出备份文件名,并用 trap 保证失败时输出错误提示。 - (探究 🔍)在脚本开头加一行
set -x运行,观察输出与不加的区别。理解set -x(调试)和set -e(严格模式)的配合使用。也可以安装 shellcheck 检查你之前写的脚本。 - (探究 🔍)研究
source script.sh和./script.sh的执行差异(提示:$0的值、变量是否留在当前 Shell)。在什么场景下你会故意用source?
点击查看答案
- (概念)shebang 指定执行脚本的解释器路径。`#!/bin/bash` 使用 Bash(支持 [[、数组、${var:-} 等扩展语法);`#!/bin/sh` 使用 POSIX sh(Ubuntu 上指向 dash),兼容性更广但语法受限。写系统管理脚本用 bash 即可。
- (概念)`"$@"` 把每个参数作为独立单词传递(保留空格),`"$*"` 把所有参数合并为一个字符串。推荐 `"$@"`:`for arg in "$@"; do` 能正确处理含空格的参数。
- (实操)`#!/bin/bash\nDIR="${1:?用法: $0 目录}"\nfind "$DIR" -maxdepth 1 -type f | wc -l`。注意给参数加默认值或报错检查,避免空参数时 `ls` 报错。
- (实操)`for f in *.txt; do [[ -f "$f" ]] || continue; mv "$f" "${f%.txt}.bak"; done`。`${f%.txt}` 是去后缀的参数展开;引号和 `[[ -f ]]` 保护含空格文件名和空匹配。
- (实操)参考示例代码中的 backup_logs.sh 结构:`set -euo pipefail` → 参数检查 → `mkdir -p` → `tar czf` → trap 输出结果。`trap 'echo "备份失败" >&2; exit 1' ERR` 可捕获出错。
- (探究 🔍)`set -x` 用 `+ 命令` 格式打印每条命令展开后的实际内容,与 `set -e` 配合可在出错瞬间看到是哪条命令的问题。shellcheck 会指出未加引号的变量、`[ ]` 等隐患。
- (探究 🔍)`source`(等价 `.`)在当前 Shell 中执行,`$0` 仍是当前 Shell,脚本内变量/函数/`cd` 会保留;`./script.sh` 启动子 Shell,`$0` 是脚本路径,一切隔离。用 source 的场景:加载配置文件(如 `~/.bashrc`)、测试函数库。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 Shell 脚本中 shebang、变量、函数的作用 | 尝试向他人讲解 |
| 命令操作 | 能不查文档编写包含 if/for/while 的基础 Shell 脚本 | 在终端实际执行 |
| 原理掌握 | 能说出 set -euo pipefail 严格模式的作用和每个参数含义 | 画出脚本执行流程图 |
| 故障排查 | 能独立调试脚本中的变量未定义和命令失败错误 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么生产脚本应该使用严格模式 | 对比有无严格模式的脚本 |
本章总结
Shell 脚本是 Linux 自动化的基石。本章从编写第一个 Bash 脚本开始,逐步讲解了变量(定义、引用、默认值、特殊变量)、条件判断(if/case)、循环(for/while/until)、函数、信号处理(trap)以及严格模式(set -euo pipefail)。掌握这些基础后,你就能将日常的命令行操作转化为可复用、可调度的自动化脚本。
编写健壮脚本的关键原则:① 始终使用 set -euo pipefail 严格模式;② 变量一律加双引号 "$var";③ 用 [[ ]] 替代 [ ];④ 用 trap 清理临时文件;⑤ 用 local 声明函数内局部变量。
速查表
| 概念 | 一句话定义 |
|---|---|
| Shebang | #!/bin/bash 指定脚本解释器 |
| 变量 | 等号两边不能有空格;加 $ 引用;加双引号防分词 |
| ${var:-default} | 变量未设置时使用默认值 |
| $@ vs $* | "$@" 每个参数独立(推荐);"$*" 合并为一个字符串 |
| if [[ ]] | Bash 条件测试,支持 &&、||、=~ 正则 |
| for i in {1..10} | 花括号展开循环 |
| while read line | 逐行安全读取文件 |
| local var | 函数内局部变量 |
| set -euo pipefail | 严格模式:出错即退、未定义变量报错、管道任一失败则整体失败 |
| trap 'cmd' EXIT | 脚本退出时执行清理 |
下一步:进入 2.8:Shell 脚本进阶「Shell 脚本进阶」,学习数组、关联数组、进程替换、flock 加锁等高级技巧。
延伸阅读
- 官方手册:
man bash— Bash 参考手册是最权威的资料 - 书籍:《Bash Cookbook》(Carl Albing 等)——大量实战案例
- 网站:ShellCheck — 在线 Shell 脚本静态分析工具,自动检测常见错误
- 网站:The Linux Command Line — 免费的 Shell 脚本教程
- 章节:2.8:Shell 脚本进阶 Shell 脚本进阶(数组、trap、进程替换、flock);4.19:Shell 实战案例集 Shell 实战案例集(8 个生产级脚本)
- 章节:2.10:正则表达式 正则表达式(在脚本中用 =~ 做模式匹配);3.15:Cron 定时任务 Cron 定时任务(用 cron/systemd timer 调度脚本)