Appearance
条件循环与函数
Shell 脚本处理分支判断、循环遍历和逻辑复用时,需要用到条件测试、循环结构和函数。
一、test、[ ] 和 [[ ]]
[ ] 是 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 判断的命令中的一种。
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 等等。
四、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五、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 不适合做复杂的数学计算,简单计数和累加够用。
六、函数
函数用于收拢重复的逻辑片段。函数内访问脚本变量是全局的,需要用 local 声明局部变量来避免意外覆盖。
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 里用字符串操作拼凑更可读。