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。
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 调用。
知识体系图
把今天讲的四类运算符串起来看,结构是这样的:
实战组合示例
我不喜欢讲干巴巴的理论。来看一个审计日志巡检里常用的例子:检查日志文件是否生成,且文件大小是否正常。
#!/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%} 这类内嵌运算符,少调外部命令。