Appearance
条件循环与函数
真正让 Shell 脚本能干活的是流程控制——根据条件走不同分支、循环处理一批东西、把重复逻辑收进函数。运维脚本里,判断服务状态、批量巡检、带重试的健康检查,全靠这一套。
一、条件测试
[ ] 是 test 命令的别名,最后的 ] 是语法要求,中间的内容是传给 test 的参数。[[ ]] 是 Bash 关键字(不是命令),支持更多匹配能力,也更能抵抗空格和通配符的干扰。
常见判断条件:
| 条件 | 含义 |
|---|---|
-f file | 文件存在且是普通文件 |
-d dir | 目录存在 |
-e path | 路径存在(不区分文件还是目录) |
-s file | 文件存在且大小大于 0(非空) |
-n "$var" | 字符串非空 |
-z "$var" | 字符串为空 |
"$a" = "$b" | 字符串相等 |
"$a" != "$b" | 字符串不等 |
$a -eq $b | 整数相等 |
$a -gt $b | 整数大于 |
$a -lt $b | 整数小于 |
文件存在性判断:
bash
config="/etc/nginx/nginx.conf"
if [ -f "$config" ]; then
echo "config exists: $config"
fi字符串判断:
bash
env_name="${1:-}"
if [ -z "$env_name" ]; then
echo "env name is required" >&2
exit 1
fi整数比较:
bash
used_percent=85
if [ "$used_percent" -gt 80 ]; then
echo "disk usage is high"
fi[[ ]] 在 Bash 中支持模式匹配和正则,且变量加不加引号都不会因空值出错:
bash
file="access.log"
if [[ "$file" == *.log ]]; then
echo "this is a log file"
fiBash 脚本中可以用 [[ ]] 来获得更安全的行为;需要兼容 POSIX sh(如 /bin/sh 指向 dash)的脚本只能使用 [ ]。
二、if 分支
if 本质上是判断命令的退出码。[ ] 和 [[ ]] 只是众多可被 if 判断的命令中的一种——任何命令都能放进 if 里,这是 Shell 比较灵活但也容易让人困惑的地方。
bash
if systemctl is-active --quiet nginx; then
echo "nginx is running"
else
echo "nginx is stopped"
fi多路分支:
bash
code="${1:-}"
if [ "$code" -eq 200 ]; then
echo "ok"
elif [ "$code" -ge 500 ]; then
echo "server error"
else
echo "other status"
fi组合多个条件:
bash
file="/var/log/app.log"
if [ -f "$file" ] && [ -s "$file" ]; then
echo "log file exists and is not empty"
fi-s 检查文件非空,巡检脚本中常用于确定日志、导出文件或结果文件是否实际产生了内容——光有文件名不够,得确认里面有东西。
三、case 分支
case 适合处理离散的固定选项,比长串 if/elif 更清晰:
bash
action="${1:-}"
case "$action" in
start)
systemctl start nginx
;;
stop)
systemctl stop nginx
;;
restart)
systemctl restart nginx
;;
*)
echo "usage: $0 {start|stop|restart}" >&2
exit 1
;;
esac匹配多个值(用 | 分隔):
bash
answer="${1:-}"
case "$answer" in
y|Y|yes|YES)
echo "confirmed"
;;
n|N|no|NO)
echo "cancelled"
;;
*)
echo "unknown answer: $answer" >&2
exit 1
;;
esac命令行运维工具脚本里,case 很适合实现子命令——start、stop、status、backup 等等,每个分支对应一个操作,比一堆 if 读起来清爽。
四、for 循环
遍历列表:
bash
for service in nginx sshd crond; do
systemctl is-active --quiet "$service" \
&& echo "$service running" \
|| echo "$service not running"
done遍历脚本参数:
bash
for file in "$@"; do
if [ -f "$file" ]; then
wc -l "$file"
fi
done遍历文件时不推荐用 ls 的输出——文件名里有空格或特殊字符时很容易出错。直接使用通配符更安全:
bash
for file in /var/log/*.log; do
[ -e "$file" ] || continue # 没有匹配到任何文件时跳过
echo "processing: $file"
done[ -e "$file" ] || continue 这行很关键——通配符没匹配到任何文件时,$file 会是字面值 *.log,不判断直接处理就出错了。
五、while 与 read
while read 是逐行读取文件的标准写法:
bash
while IFS= read -r line; do
echo "line: $line"
done < app.log两个关键参数:IFS= 防止 read 去掉行首行尾的空白字符,read -r 不将反斜杠当作转义符处理。这两个参数是"安全读取"的标配,不写的话遇到特殊字符的行就会出问题。
读取命令输出并逐行处理:
bash
find /var/log -type f -name "*.log" -print0 |
while IFS= read -r -d '' file; do
ls -lh "$file"
done-print0 和 read -d '' 是用 NUL 字符(而非换行符)作为分隔符来传递文件名。文件名中即使含有空格或换行,也不会被错误地拆成多个参数——这是处理文件名的最安全方式。
带重试逻辑的 while 循环:
bash
count=0
while [ "$count" -lt 3 ]; do
if curl -fsS http://127.0.0.1:8080/healthz; then
exit 0
fi
count=$((count + 1))
sleep 2
done
echo "health check failed after 3 attempts" >&2
exit 1$((...)) 是算术展开,用于整数运算。Shell 不适合做复杂的数学计算,简单计数和累加够用——真要算复杂的,换 Python。
六、函数
函数用于收拢重复的逻辑片段。函数内访问脚本变量是全局的,需要用 local 声明局部变量来避免意外覆盖——这是 Shell 函数最容易出问题的地方。
bash
log_info() {
printf '[INFO] %s\n' "$*"
}
log_error() {
printf '[ERROR] %s\n' "$*" >&2
}
check_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
log_error "missing command: $cmd"
return 1
fi
}
check_command curl
log_info "curl is ready"函数内的变量用 local 声明后就不会泄露到函数外部。
函数的"返回值"本质上是退出码,范围 0–255。要从函数向外部传递字符串数据,通过标准输出配合命令替换:
bash
get_today() {
date +%F
}
today="$(get_today)"
echo "$today"脚本变长后,日志函数、参数校验和依赖检查放在前面,主逻辑放在后面的 main 函数中,整体结构会清晰很多。
七、数组
Bash 支持索引数组(以数字为下标)和关联数组(以字符串为 key,Bash 4+)。
索引数组:
bash
services=(nginx sshd crond)
echo "${services[0]}" # 第一个元素
echo "${services[@]}" # 所有元素
for service in "${services[@]}"; do
systemctl is-active --quiet "$service" \
&& echo "$service running" \
|| echo "$service not running"
done
echo "${#services[@]}" # 数组长度遍历数组时必须写成 "${array[@]}"——加引号才能让每个元素保持独立,否则元素内容含有空格时会被拆分。
关联数组(需要 Bash 4+):
bash
declare -A ports=(
[nginx]=80
[mysql]=3306
[redis]=6379
)
echo "${ports[nginx]}"在 CentOS 7 等老系统上,Bash 默认版本是 4.2,关联数组基本可用。但极老的系统(如 CentOS 6 的 Bash 3.x)不支持关联数组,写脚本前先确认运行环境,不然脚本在某些机器上直接报错。
八、字符串处理
Shell 内建的参数展开能覆盖一些简单的字符串操作,不需要每次调用外部命令:
| 语法 | 含义 | 示例 |
|---|---|---|
${var:-default} | 变量为空或未定义时使用默认值 | ${name:-unknown} |
${var#prefix} | 从前往后删除最短匹配 | ${path#/} |
${var##prefix} | 从前往后删除最长匹配 | ${path##*/} |
${var%suffix} | 从后往前删除最短匹配 | ${path%.*} |
${var%%suffix} | 从后往前删除最长匹配 | ${path%/*} |
${#var} | 字符串长度 | ${#name} |
${var/old/new} | 替换第一个匹配 | ${name/prod/test} |
获取路径中的文件名和目录:
bash
path="/var/log/nginx/access.log"
echo "${path##*/}" # access.log(删除最后一个 / 之前的所有内容)
echo "${path%/*}" # /var/log/nginx(删除最后一个 / 之后的内容)简单的默认值、路径分解、后缀替换,参数展开完全能胜任。处理逻辑一旦变复杂,换用 awk、Python 或专门工具比硬在 Shell 里用字符串操作拼凑更可读——Shell 字符串操作写多了自己都看不懂。