脚本基础:shebang 与执行方式
Shebang 行
脚本文件的第一行以 #! 开头,告诉操作系统用哪个解释器来执行这个脚本。这一行称为 shebang(sharp + bang)。
#!/bin/bash # 指定使用 bash 执行
#!/usr/bin/env bash # 更可移植:从 PATH 中找 bash(推荐)
#!/usr/bin/env python3 # Python 脚本的 shebang
#!/bin/sh # POSIX sh(兼容性最强但功能受限)
# 严格模式(推荐所有脚本使用)
#!/usr/bin/env bash
set -euo pipefail
# -e : 任何命令返回非零退出码时立即退出
# -u : 使用未定义的变量时报错退出
# -o pipefail : 管道中任一命令失败则整个管道失败
# 执行脚本的三种方式
chmod +x script.sh
./script.sh # 使用 shebang 指定的解释器
bash script.sh # 显式用 bash 执行(忽略 shebang)
source script.sh # 在当前 shell 中执行(变量/函数在当前会话可用)
. script.sh # source 的简写
变量
# 赋值:等号两边不能有空格!
name="Alice"
count=42
pi=3.14
today=$(date +%Y-%m-%d) # 命令替换:赋值为命令输出
size=$(du -sh /var | cut -f1)
# 使用变量
echo "$name" # 加引号(推荐,避免单词分割)
echo "${name}Script" # 花括号界定变量名边界
echo '$name' # 单引号:不展开变量,输出字面 $name
# 默认值操作符
echo "${name:-default}" # 若 name 未设置或为空,使用 default
echo "${name:=default}" # 若 name 未设置或为空,赋值为 default
echo "${name:?'name is required'}" # 若未设置则报错退出
# 字符串操作
str="Hello, World!"
echo ${#str} # 字符串长度:13
echo ${str:7:5} # 截取:World(从位置7取5个字符)
echo ${str/World/Linux} # 替换第一个匹配:Hello, Linux!
echo ${str//l/L} # 替换所有匹配:HeLLo, WorLd!
echo ${str#Hello, } # 从前缀删除:World!
echo ${str%!} # 从后缀删除:Hello, World
echo ${str,,} # 转小写(bash 4.0+)
echo ${str^^} # 转大写
# 数组
fruits=("apple" "banana" "cherry")
echo ${fruits[0]} # apple
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 数组长度:3
fruits+=("date") # 追加元素
for f in "${fruits[@]}"; do echo "$f"; done
# 关联数组(字典)
declare -A person
person["name"]="Alice"
person["age"]=30
echo ${person["name"]}
echo ${!person[@]} # 所有键
条件判断
if / elif / else
# 基本 if
if [ "$name" = "Alice" ]; then
echo "Hello, Alice!"
elif [ "$name" = "Bob" ]; then
echo "Hello, Bob!"
else
echo "Hello, stranger!"
fi
# [[ ]] 是增强版,支持正则匹配和更安全的字符串比较(推荐)
if [[ "$name" == Alice* ]]; then
echo "Name starts with Alice"
fi
if [[ "$str" =~ ^[0-9]+$ ]]; then
echo "纯数字"
fi
# 常用测试表达式
# 字符串:= != -z(空)-n(非空)< >(字典序)
[ -z "$var" ] # 变量为空
[ -n "$var" ] # 变量非空
[ "$a" = "$b" ] # 字符串相等
[ "$a" != "$b" ] # 字符串不等
# 数字:-eq -ne -lt -le -gt -ge
[ "$count" -gt 0 ] # 大于
[ "$count" -eq 42 ] # 等于
(( count > 0 )) # 算术表达式(更直观)
# 文件:-f -d -e -r -w -x -s
[ -f "/etc/hosts" ] # 是普通文件且存在
[ -d "/tmp" ] # 是目录
[ -e "/path" ] # 存在(任何类型)
[ -r "$file" ] # 可读
[ -w "$file" ] # 可写
[ -x "$script" ] # 可执行
[ -s "$file" ] # 存在且非空
# 逻辑运算
[ -f "$f" ] && [ -r "$f" ] # AND(-a 或 &&)
[ -z "$a" ] || [ -z "$b" ] # OR(-o 或 ||)
[[ -f "$f" && -r "$f" ]] # [[ ]] 内支持 && 和 ||
case 语句
# case:用于多值匹配(比 if-elif 更简洁)
read -p "请输入选项 [start|stop|restart]: " action
case "$action" in
start)
echo "启动服务..."
;;
stop)
echo "停止服务..."
;;
restart|reload) # 多个模式匹配同一分支
echo "重启服务..."
;;
[Yy]|[Yy]es) # 通配符匹配
echo "确认"
;;
*) # 默认分支
echo "未知选项: $action"
exit 1
;;
esac
循环
# for 循环——遍历列表
for fruit in apple banana cherry; do
echo "水果: $fruit"
done
# for 循环——遍历数组
files=("a.txt" "b.txt" "c.txt")
for f in "${files[@]}"; do
echo "处理: $f"
done
# for 循环——C 风格数字
for (( i=1; i<=5; i++ )); do
echo "第 $i 次"
done
# for 循环——seq 生成数字序列
for i in $(seq 1 5); do echo $i; done
for i in $(seq 0 10 2); do echo $i; done # 步长为 2:0 2 4 6 8 10
# for 循环——遍历目录文件
for f in /var/log/*.log; do
echo "日志文件: $f($(wc -l < "$f") 行)"
done
# while 循环
count=0
while [ $count -lt 5 ]; do
echo "count=$count"
(( count++ ))
done
# while read——逐行读取文件/标准输入
while IFS= read -r line; do
echo "行内容: $line"
done < /etc/hosts
# while read——处理命令输出
ps aux | while read -r user pid cpu mem rest; do
if (( $(echo "$cpu > 50" | bc) )); then
echo "高 CPU 进程: $pid($cpu%)"
fi
done
# until 循环(条件为假时继续)
until systemctl is-active nginx &>/dev/null; do
echo "等待 nginx 启动..."
sleep 2
done
echo "nginx 已启动!"
# break 和 continue
for i in {1..10}; do
[ $i -eq 5 ] && break # 遇到 5 退出循环
[ $i -eq 3 ] && continue # 跳过 3
echo $i
done
函数
# 函数定义(两种语法均可)
function greet() {
local name="$1" # local 声明局部变量
echo "Hello, $name!"
}
backup() {
local src="$1"
local dst="${2:-/tmp/backup}"
if [ ! -f "$src" ]; then
echo "错误: 文件 $src 不存在" >&2
return 1 # 函数内用 return 返回退出码
fi
cp "$src" "$dst/$(basename $src).$(date +%Y%m%d)"
echo "备份完成"
return 0
}
# 调用函数
greet "Alice"
backup /etc/nginx/nginx.conf /var/backups
# 捕获函数返回值(退出码)
if backup /etc/hosts; then
echo "备份成功"
else
echo "备份失败"
fi
# 函数返回字符串(通过 echo + 命令替换)
get_timestamp() {
echo "$(date +%Y%m%d_%H%M%S)"
}
ts=$(get_timestamp)
echo "时间戳: $ts"
# 递归函数
factorial() {
local n=$1
if (( n <= 1 )); then
echo 1
else
local sub=$(factorial $(( n - 1 )))
echo $(( n * sub ))
fi
}
echo "5! = $(factorial 5)"
位置参数与特殊变量
#!/usr/bin/env bash
# 调用: ./deploy.sh production v1.2.3 --dry-run
echo "脚本名: $0" # ./deploy.sh
echo "第1个参数: $1" # production
echo "第2个参数: $2" # v1.2.3
echo "参数个数: $#" # 3
echo "所有参数: $@" # production v1.2.3 --dry-run
echo "所有参数: $*" # 与 $@ 类似,但引号行为不同
echo "上一命令退出码: $?"
echo "当前进程 PID: $$"
echo "最近后台 PID: $!"
# 遍历所有参数(推荐用 $@)
for arg in "$@"; do
echo "参数: $arg"
done
# shift:移除第一个参数($2 变成 $1,以此类推)
while [ $# -gt 0 ]; do
case "$1" in
--env|-e)
ENV="$2"
shift 2 # 移除 --env 和其值
;;
--dry-run)
DRY_RUN=true
shift
;;
*)
echo "未知参数: $1"
exit 1
;;
esac
done
读取用户输入:read
# 基本读取
read username # 读取一行到变量 username
read -p "请输入用户名: " username # 显示提示语
read -s -p "请输入密码: " password # -s 静默模式(不显示输入)
read -t 10 -p "10秒内确认 [y/N]: " confirm # -t 超时秒数
read -n 1 -p "按任意键继续..." key # -n 只读取N个字符
# 读取多个变量(按空格分割)
read first last <<< "Alice Smith"
echo "$first $last" # Alice Smith
# 处理用户确认
read -p "确认执行?[y/N] " confirm
case "${confirm,,}" in # ,, 转小写
y|yes)
echo "继续执行..."
;;
*)
echo "已取消"
exit 0
;;
esac
退出码与错误处理
# 退出码:0=成功,1-255=各种错误
exit 0 # 正常退出
exit 1 # 通用错误
exit 2 # 用法错误(参数不对)
# 检查上一命令的退出码
cp source.txt dest.txt
if [ $? -ne 0 ]; then
echo "复制失败" >&2
exit 1
fi
# 更简洁的写法
cp source.txt dest.txt || { echo "复制失败" >&2; exit 1; }
# 自定义错误函数
die() {
echo "错误: $*" >&2
exit 1
}
[ -f "$config" ] || die "配置文件 $config 不存在"
# 检查必要命令是否存在
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "需要安装 $1"
}
require_cmd docker
require_cmd kubectl
trap:信号处理与清理
# trap 用于捕获信号,常用于脚本退出时的清理工作
#!/usr/bin/env bash
set -euo pipefail
TMPDIR=$(mktemp -d) # 创建临时目录
# 注册退出时的清理函数
cleanup() {
echo "清理临时文件..."
rm -rf "$TMPDIR"
echo "清理完成"
}
trap cleanup EXIT # EXIT:脚本以任何方式退出时触发
trap cleanup INT TERM # INT=Ctrl+C,TERM=kill 信号
# 常用 trap 信号
trap 'echo "收到 Ctrl+C,退出..."' INT
trap 'echo "错误发生在第 $LINENO 行"' ERR
trap 'echo "脚本结束"' EXIT
# 实际脚本示例:带清理的部署脚本
deploy() {
local tmpdir
tmpdir=$(mktemp -d)
trap "rm -rf '$tmpdir'" RETURN # 函数返回时清理
echo "下载到临时目录: $tmpdir"
curl -sL https://example.com/app.tar.gz | tar -xz -C "$tmpdir"
echo "部署..."
cp -r "$tmpdir/app" /opt/
echo "部署完成!"
}
综合示例:实用备份脚本
#!/usr/bin/env bash
set -euo pipefail
# 配置
BACKUP_DIR="/var/backups"
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 工具函数
log() { echo "[$(date +%H:%M:%S)] $*"; }
die() { echo "错误: $*" >&2; exit 1; }
# 参数处理
[ $# -lt 1 ] && die "用法: $0 <源目录> [备份目录]"
SRC="${1}"
DST="${2:-$BACKUP_DIR}"
# 检查
[ -d "$SRC" ] || die "源目录不存在: $SRC"
[ -d "$DST" ] || mkdir -p "$DST"
command -v tar >/dev/null || die "需要安装 tar"
# 备份
ARCHIVE="$DST/$(basename $SRC)_$TIMESTAMP.tar.gz"
log "开始备份 $SRC → $ARCHIVE"
tar -czf "$ARCHIVE" -C "$(dirname $SRC)" "$(basename $SRC)"
log "备份完成:$(du -sh $ARCHIVE | cut -f1)"
# 清理旧备份
OLD_COUNT=$(find "$DST" -name "*.tar.gz" -mtime +$RETENTION_DAYS | wc -l)
if [ "$OLD_COUNT" -gt 0 ]; then
log "清理 $OLD_COUNT 个超过 ${RETENTION_DAYS} 天的旧备份"
find "$DST" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
fi
log "当前备份列表:"
ls -lh "$DST"/*.tar.gz 2>/dev/null || echo " (无)"
Shell 脚本调试技巧
bash -n script.sh— 语法检查(不执行)bash -x script.sh— 追踪执行(打印每条命令)- 在脚本中加
set -x开启追踪,set +x关闭 - 使用 ShellCheck 在线工具或
shellcheck script.sh做静态分析 - 在关键位置加
echo "DEBUG: var=$var" >&2输出调试信息
本章小结
Shell 脚本编程的核心要点:① 始终在脚本第一行加 set -euo pipefail——-e 遇错即退,-u 未定义变量报错,-o pipefail 管道失败不被忽略,这三个选项是安全脚本的基础;② 变量赋值等号两边不能有空格,引用变量始终加双引号 "$var" 防止单词分割和通配符展开;③ 用 local 声明函数局部变量,防止污染全局作用域;④ [[ ]] 比 [ ] 更安全(不需要引号防护,支持正则 =~);⑤ while IFS= read -r line 是逐行读取文件的标准写法,-r 防止反斜杠转义,IFS= 保留行首尾空格;⑥ trap cleanup EXIT 确保脚本无论正常还是异常退出都能清理临时资源;⑦ bash -x 和 ShellCheck 是调试脚本的两个必备工具。