强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bash 脚本编写教程 / 04 - 运算符

04 - 运算符

4.1 算术运算符

Bash 支持整数运算,不支持浮点数(需要 bcawk)。

算术展开 $(( ))

a=10
b=3

echo "$((a + b))"     # 加法: 13
echo "$((a - b))"     # 减法: 7
echo "$((a * b))"     # 乘法: 30
echo "$((a / b))"     # 除法(整数): 3
echo "$((a % b))"     # 取余: 1
echo "$((a ** 2))"    # 幂运算: 100

# 自增自减
((a++))
echo "$a"             # 11
((a--))
echo "$a"             # 10

# 复合赋值
((a += 5))            # a = a + 5
((a -= 2))            # a = a - 2
((a *= 3))            # a = a * 3
((a /= 4))            # a = a / 4
((a %= 7))            # a = a % 7

算术运算的多种形式

# 方式一:$(( ))  (推荐)
result=$((10 + 20))

# 方式二:(( ))  作为命令
((result = 10 + 20))

# 方式三:let 命令
let "result = 10 + 20"
let result=10+20         # 不含空格时可省略引号

# 方式四:expr 命令(旧语法)
result=$(expr 10 + 20)   # 运算符两边必须有空格

# 方式五:declare -i
declare -i result
result=10+20

# 方式六:bc(支持浮点数)
result=$(echo "scale=2; 10 / 3" | bc)
echo "$result"           # 3.33

浮点数运算

# Bash 不原生支持浮点数
# echo "$((10 / 3))"    # 输出: 3(不是 3.33)

# 使用 bc
pi=$(echo "scale=10; 4 * a(1)" | bc -l)
echo "$pi"               # 3.1415926535

# 使用 awk
result=$(awk "BEGIN {printf \"%.2f\", 10/3}")
echo "$result"           # 3.33

# 使用 printf
printf "%.2f\n" "$(echo "10 / 3" | bc -l)"  # 3.33

4.2 比较运算符

整数比较

运算符含义等价写法
-eq等于 (equal)==(在 (( )) 中)
-ne不等于 (not equal)!=
-gt大于 (greater than)>
-ge大于等于 (greater or equal)>=
-lt小于 (less than)<
-le小于等于 (less or equal)<=
a=10
b=20

# [ ] 传统写法
if [ "$a" -eq "$b" ]; then
    echo "相等"
fi

# [[ ]] 推荐写法
if [[ $a -eq $b ]]; then
    echo "相等"
fi

# (( )) 算术比较(最直观)
if ((a == b)); then
    echo "相等"
fi

if ((a < b)); then
    echo "$a 小于 $b"
fi

# 三元运算(在 (( )) 中)
max=$(( a > b ? a : b ))
echo "较大值: $max"     # 较大值: 20

字符串比较

运算符含义示例
= / ==相等[[ "$a" = "$b" ]]
!=不相等[[ "$a" != "$b" ]]
<字典序小于[[ "$a" < "$b" ]]
>字典序大于[[ "$a" > "$b" ]]
-z为空(长度为 0)[[ -z "$a" ]]
-n非空(长度大于 0)[[ -n "$a" ]]
=~正则匹配[[ "$a" =~ ^[0-9]+$ ]]
== 通配符通配符匹配[[ "$a" == *.txt ]]
name="hello world"

# 字符串比较(必须加引号)
[[ "$name" = "hello world" ]] && echo "相等"
[[ "$name" != "goodbye" ]] && echo "不相等"

# 空字符串检查
str=""
[[ -z "$str" ]] && echo "字符串为空"
[[ -n "$name" ]] && echo "字符串非空"

# 字典序比较
[[ "apple" < "banana" ]] && echo "apple 在 banana 前"

# 通配符匹配(不支持正则)
file="document.pdf"
[[ "$file" == *.pdf ]] && echo "是 PDF 文件"

# 正则匹配
email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "有效的邮箱地址"
fi

# 捕获组
version="v1.2.3"
if [[ "$version" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
    major="${BASH_REMATCH[1]}"
    minor="${BASH_REMATCH[2]}"
    patch="${BASH_REMATCH[3]}"
    echo "版本: 主版本=$major 次版本=$minor 补丁=$patch"
fi

⚠️ 注意:在 [ ] 中使用 < > 需要转义 \< \>,或使用 -lt -gt。在 [[ ]] 中可以直接使用。

4.3 逻辑运算符

# && 逻辑与
[[ -f "/etc/hosts" ]] && echo "文件存在"

# || 逻辑或
[[ -d "/nonexistent" ]] || echo "目录不存在"

# ! 逻辑非
[[ ! -f "/nonexistent" ]] && echo "文件不存在"

# 组合条件
age=25
role="admin"

if [[ $age -ge 18 && "$role" = "admin" ]]; then
    echo "成年管理员"
fi

if [[ $age -lt 18 || "$role" = "guest" ]]; then
    echo "未成年或访客"
fi

# 短路求值
# &&:第一个为真才执行第二个
# ||:第一个为假才执行第二个

# 实用模式:默认值
name="${1:-default_name}"   # 参数展开(推荐)

# 条件执行链
cd /tmp && mkdir -p test && touch test/file.txt && echo "创建成功"

4.4 文件测试运算符

运算符含义示例
-e文件存在[[ -e "$file" ]]
-f是普通文件[[ -f "$file" ]]
-d是目录[[ -d "$dir" ]]
-L / -h是符号链接[[ -L "$link" ]]
-r可读[[ -r "$file" ]]
-w可写[[ -w "$file" ]]
-x可执行[[ -x "$file" ]]
-s文件非空[[ -s "$file" ]]
-b是块设备[[ -b "/dev/sda" ]]
-c是字符设备[[ -c "/dev/null" ]]
-S是 Socket[[ -S "$sock" ]]
-p是管道[[ -p "$pipe" ]]
-nt更新于 (newer than)[[ "$a" -nt "$b" ]]
-ot旧于 (older than)[[ "$a" -ot "$b" ]]
-ef同一文件 (same file)[[ "$a" -ef "$b" ]]
file="/etc/hosts"
dir="/tmp"

# 基础文件测试
if [[ -e "$file" ]]; then
    echo "$file 存在"
fi

if [[ -f "$file" ]]; then
    echo "$file 是普通文件"
fi

if [[ -d "$dir" ]]; then
    echo "$dir 是目录"
fi

# 复合检查
config_file="/etc/myapp/config.toml"
if [[ ! -f "$config_file" ]]; then
    echo "配置文件不存在,创建默认配置..."
    mkdir -p "$(dirname "$config_file")"
    cat > "$config_file" << 'EOF'
[server]
host = "0.0.0.0"
port = 8080
EOF
fi

# 权限检查
if [[ -r "$config_file" && -w "$config_file" ]]; then
    echo "配置文件可读写"
elif [[ -r "$config_file" ]]; then
    echo "配置文件只读"
else
    echo "无法访问配置文件"
    exit 1
fi

# 文件新旧比较
if [[ "/tmp/new.txt" -nt "/tmp/old.txt" ]]; then
    echo "new.txt 比 old.txt 新"
fi

4.5 位运算符

a=12     # 二进制: 1100
b=10     # 二进制: 1010

echo "$((a & b))"      # 按位与: 8   (1000)
echo "$((a | b))"      # 按位或: 14  (1110)
echo "$((a ^ b))"      # 按位异或: 6 (0110)
echo "$((~a))"         # 按位取反: -13
echo "$((a << 2))"     # 左移: 48
echo "$((a >> 2))"     # 右移: 3

# 权限位操作(实际应用)
# 设置文件权限位
EXECUTE=1    # 001
WRITE=2      # 010
READ=4       # 100

# 组合权限
perms=$((READ | WRITE))    # rw-: 6
echo "权限值: $perms"

# 检查权限位
if ((perms & READ)); then
    echo "有读权限"
fi
if ((perms & EXECUTE)); then
    echo "有执行权限"
else
    echo "无执行权限"
fi

4.6 赋值运算符

# 简单赋值
x=10

# 复合赋值
((x += 5))   # x = x + 5 = 15
((x -= 3))   # x = x - 3 = 12
((x *= 2))   # x = x * 2 = 24
((x /= 4))   # x = x / 4 = 6
((x %= 5))   # x = x % 5 = 1

# 位赋值
y=12
((y &= 10))   # y = y & 10
((y |= 5))    # y = y | 5
((y ^= 3))    # y = y ^ 3
((y <<= 2))   # y = y << 2
((y >>= 1))   # y = y >> 1

4.7 运算符优先级

优先级运算符说明
1()括号
2++ --自增自减
3**
4~ ! -取反、逻辑非、负号
5* / %乘、除、取余
6+ -加、减
7<< >>位移
8< <= > >=比较
9== !=相等比较
10&按位与
11^按位异或
12|按位或
13&&逻辑与
14||逻辑或
15?:三元条件
16= += -=赋值
17,逗号

4.8 业务场景:服务器健康检查

#!/bin/bash
# health_check.sh —— 服务器健康检查脚本
set -euo pipefail

readonly WARN_DISK_PERCENT=80
readonly CRIT_DISK_PERCENT=90
readonly WARN_MEM_PERCENT=80
readonly CRIT_MEM_PERCENT=95
readonly WARN_LOAD_RATIO=2.0

# 读取 CPU 核心数
readonly CPU_COUNT=$(nproc 2>/dev/null || echo 1)

check_disk() {
    local mount_point="$1"
    local usage
    usage=$(df "$mount_point" | awk 'NR==2 {gsub(/%/,""); print $5}')
    
    if ((usage >= CRIT_DISK_PERCENT)); then
        echo "  🔴 磁盘 $mount_point: ${usage}% (严重!)"
        return 2
    elif ((usage >= WARN_DISK_PERCENT)); then
        echo "  🟡 磁盘 $mount_point: ${usage}% (警告)"
        return 1
    else
        echo "  🟢 磁盘 $mount_point: ${usage}% (正常)"
        return 0
    fi
}

check_memory() {
    local total used percent
    read -r total used _ <<< "$(free | awk '/^Mem:/ {print $2, $3}')"
    percent=$((used * 100 / total))
    
    if ((percent >= CRIT_MEM_PERCENT)); then
        echo "  🔴 内存: ${percent}% (严重!)"
        return 2
    elif ((percent >= WARN_MEM_PERCENT)); then
        echo "  🟡 内存: ${percent}% (警告)"
        return 1
    else
        echo "  🟢 内存: ${percent}% (正常)"
        return 0
    fi
}

check_load() {
    local load_1m
    load_1m=$(awk '{print $1}' /proc/loadavg)
    local threshold
    threshold=$(awk "BEGIN {printf \"%.1f\", $CPU_COUNT * $WARN_LOAD_RATIO}")
    
    if awk "BEGIN {exit !($load_1m > $threshold)}"; then
        echo "  🔴 负载: $load_1m (阈值: $threshold,严重!)"
        return 2
    elif awk "BEGIN {exit !($load_1m > $CPU_COUNT)}"; then
        echo "  🟡 负载: $load_1m (CPU核心数: $CPU_COUNT,警告)"
        return 1
    else
        echo "  🟢 负载: $load_1m (正常)"
        return 0
    fi
}

# 主检查
echo "================================"
echo "  服务器健康检查 — $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================"

exit_code=0

echo ""
echo "[磁盘使用]"
check_disk "/" || exit_code=$?

echo ""
echo "[内存使用]"
check_memory || exit_code=$?

echo ""
echo "[系统负载]"
check_load || exit_code=$?

echo ""
case $exit_code in
    0) echo "✅ 所有检查通过" ;;
    1) echo "⚠️  存在警告,请关注" ;;
    2) echo "🔴 存在严重问题,请立即处理!" ;;
esac

exit "$exit_code"

4.9 注意事项

陷阱说明解决方案
字符串与整数混用[[ "abc" -gt 5 ]] 报错先验证是否为数字
[ ] 中未加引号[ $var = "x" ]$var 为空时报错[[ "$var" == "x" ]]
浮点比较-gt 不支持浮点数使用 bcawk
< >[ ]被解释为重定向使用 [[ ]]-lt -gt
==[ ]部分系统不支持使用 =[[ ]]

4.10 扩展阅读