3. Shell 运算符:算术、比较、逻辑、字符串

运算符这东西,说白了就是让 shell 能干点“正经事”。没有它们,变量就是一堆死数据。我刚开始写脚本时,觉得运算符就是加减乘除,后来发现陷阱还真不少。今天咱们把四类运算符掰开揉碎讲清楚。

3.1 算术运算符

Shell 里的算术运算不像其他语言那样直接写 a + b,它得靠 $((...)) 或者 let 来触发。为什么会这样?因为 shell 默认把所有东西当字符串处理,你写 a=1+2,它存进去的就是字符串 "1+2",不是数字 3。

我的习惯:能用 $(( )) 就别用 let 或 expr。 $(( )) 语法干净,支持 C 风格自增自减,嵌套也方便。

常用算术运算符一览:

运算符含义示例结果
+加法$((3+5))8
-减法$((10-3))7
*乘法$((4*5))20
/整数除法$((7/2))3(向下取整)
%取余$((7%2))1
**幂运算$((2**3))8
我曾经踩的坑:用 expr 做乘法时,* 必须转义成 \*,否则被当作通配符。现在用 $(( )) 就省心了,但老脚本里偶尔还能见到 expr,看到时留个心眼。

小数运算咋办?Shell 原生不支持浮点数。我一般用 awk 或 bc,比如 echo "scale=2; 5/3" | bc。

3.2 比较运算符

比较运算符分两套:一套给整数用,一套给字符串用。混用会出 bug,而且很难查。

整数比较

整数比较用方括号 [ ] 或双圆括号 (( ))。我个人偏好 (( )),因为它更像 C 语言,写起来顺手。

运算符含义示例
-eq等于[ "$a" -eq "$b" ]
-ne不等于[ "$a" -ne "$b" ]
-lt小于[ "$a" -lt "$b" ]
-le小于等于[ "$a" -le "$b" ]
-gt大于[ "$a" -gt "$b" ]
-ge大于等于[ "$a" -ge "$b" ]
注意:方括号里变量最好加双引号,否则变量为空时语法会报错。[ $a -eq 1 ] 在 $a 为空时展开成 [ -eq 1 ],直接炸掉。

字符串比较

字符串比较用 = 和 !=(注意是单等号,不是 ==)。还有 < 和 > 按字典序比较,但必须转义或用双括号。

运算符含义示例
=字符串相等[ "$str" = "hello" ]
!=字符串不相等[ "$str" != "hello" ]
-z字符串为空[ -z "$str" ]
-n字符串非空[ -n "$str" ]

你想想看,如果把 -eq 用在字符串上会怎样?不会报错,但结果永远是 false,因为 -eq 会把字符串转成数字,转不了就变 0。

我的避坑指南:写条件时心里默念“整数用 -eq,字符串用 =”,就不容易混。

3.3 逻辑运算符

逻辑运算符用于组合多个条件。Shell 里没有 && || 那种简洁写法吗?有,但分场景。

在 [ ] 中使用

  • ! expression —— 取反
  • expression1 -a expression2 —— 与(and)
  • expression1 -o expression2 —— 或(or)

比如:[ "$age" -gt 18 -a "$age" -lt 60 ]。

在 [[ ]] 中使用(推荐)

[[ ]] 是 bash 的增强版,支持 && || !,而且不用给变量加引号(但加了也没错)。我个人几乎只用 [[ ]],少写很多字符。

if [[ "$age" -gt 18 && "$age" -lt 60 ]]; then
    echo "成年但未退休"
fi
注意:在 [[ ]] 里不要用 -a 和 -o,那是古董写法,容易出优先级问题。我见过一个老脚本,-a 和 -o 混在一起,逻辑直接反了,查了半天。

3.4 字符串运算符

除了比较,字符串还有一些专用运算符。最常见的是拼接和取子串。

操作语法说明
拼接str1$str2直接挨着写就行
长度${#str}返回字符数
子串提取${str:start:len}从 start(0开始)取 len 个字符
模式匹配删除${str#pattern}删除最短匹配前缀
模式匹配删除${str##pattern}删除最长匹配前缀
模式匹配删除${str%pattern}删除最短匹配后缀
模式匹配删除${str%%pattern}删除最长匹配后缀
filename="audit_20240315.log"
echo ${filename#*_}        # 输出 20240315.log
echo ${filename%.*}        # 输出 audit_20240315
echo ${filename:6:8}       # 输出 20240315
项目中的用法:我在写日志分析脚本时,经常用 ${str##*/} 提取文件名,用 ${str%%.*} 去掉扩展名。少写很多 awk 调用。

知识体系图

把今天讲的四类运算符串起来看,结构是这样的:

Shell 运算符体系 Shell 运算符 算术运算符 整数比较 (-eq等) 字符串比较 (=, !=) 逻辑运算符 + - * / % ** -lt -le -gt -ge -z -n < > ! && || -a -o 💡 字符串运算符:拼接、长度 ${#str}、子串 ${str:0:5}、模式删除 ⚠️ 整数比较用 -eq,字符串比较用 =;逻辑运算符推荐用 [[ ]] + && || 核心原则:类型区分 + 变量引用保护 + 优先用 bash 增强语法

实战组合示例

我不喜欢讲干巴巴的理论。来看一个审计日志巡检里常用的例子:检查日志文件是否生成,且文件大小是否正常。

#!/bin/bash
log_dir="/var/log/audit"
today=$(date +"%Y%m%d")
log_file="${log_dir}/audit-${today}.log"

# 检查文件是否存在且非空
if [[ -e "$log_file" && -s "$log_file" ]]; then
    # 获取文件大小(字节)
    size=$(stat --format=%s "$log_file")
    # 大小必须在 1KB 到 100MB 之间
    if (( size >= 1024 && size <= 104857600 )); then
        echo "[OK] $log_file 大小正常: $size 字节"
    else
        echo "[WARN] $log_file 大小异常: $size 字节"
    fi
else
    echo "[ERROR] $log_file 不存在或为空"
fi

这里用到了 -e(存在)、-s(非空)、&&(逻辑与)、(( ))(整数比较)。你想想看,如果我不小心把 -s 写成 -z,那就变成检查文件是否为空字符串了——完全不对。

总结一下我的建议:
• 算术运算统一用 $(( )),少写 bug。
• 整数比较用 (( )) 或 [ ] + -eq 系列,别混用字符串比较。
• 逻辑运算优先用 [[ ]] + && ||,可读性强。
• 字符串处理多用 ${var#} ${var%} 这类内嵌运算符,少调外部命令。