Skip to content

条件循环与函数

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"
fi

Bash 脚本中可以用 [[ ]] 来获得更安全的行为;需要兼容 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 很适合实现子命令——startstopstatusbackup 等等。

四、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

-print0read -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 里用字符串操作拼凑更可读。