Bash 脚本编写教程 / 10 - 正则表达式
10 - 正则表达式
10.1 正则表达式基础
正则表达式(Regular Expression, Regex)是文本匹配的强大工具。在 Bash 生态中,有多种正则引擎:
| 引擎 | 工具 | 类型 | 说明 |
|---|
| BRE | grep(默认), sed | 基本正则 | \+ \? 需转义 |
| EERE | grep -E, sed -E, awk | 扩展正则 | + ? ` |
| PCRE | grep -P, perl | Perl 正则 | 支持 \d \w 前瞻后顾 |
| Glob | [[ == ]] | 通配符 | * ? [abc] |
| Bash Regex | [[ =~ ]] | ERE 子集 | 支持 BASH_REMATCH 捕获 |
BRE vs ERE 语法差异
| 功能 | BRE | ERE |
|---|
| 任意字符 | . | . |
| 量词(0或多次) | * | * |
| 量词(1或多次) | \+ | + |
| 量词(0或1次) | \? | ? |
| 量词(n次) | \{n\} | {n} |
| 量词(n-m次) | \{n,m\} | {n,m} |
| 或 | | | | |
| 分组 | \(\) | () |
| 转义 | \ | \ |
# BRE(grep 默认)
echo "abc123" | grep 'abc[0-9]\+'
# ERE(grep -E)
echo "abc123" | grep -E 'abc[0-9]+'
# 等价但 ERE 更易读
10.2 字符类与元字符
常用元字符
| 元字符 | 含义 | 示例 |
|---|
. | 任意单个字符 | a.c 匹配 abc、a1c |
^ | 行/字符串开头 | ^Hello |
$ | 行/字符串结尾 | World$ |
\b | 单词边界 | \bcat\b 匹配 cat 但不匹配 catch |
\d | 数字(PCRE) | [0-9] 等价 |
\w | 单词字符(PCRE) | [a-zA-Z0-9_] 等价 |
\s | 空白字符(PCRE) | [ \t\n\r\f] 等价 |
字符类
# 方括号字符类
[abc] # a 或 b 或 c
[^abc] # 不是 a、b、c
[a-z] # 小写字母
[A-Z] # 大写字母
[0-9] # 数字
[a-zA-Z0-9] # 字母和数字
[[:alpha:]] # 字母
[[:digit:]] # 数字
[[:alnum:]] # 字母和数字
[[:space:]] # 空白字符
[[:upper:]] # 大写字母
[[:lower:]] # 小写字母
[[:punct:]] # 标点符号
量词
| 量词 | 含义 | 示例 |
|---|
* | 0 次或多次 | ab*c → ac, abc, abbc |
+ | 1 次或多次 | ab+c → abc, abbc |
? | 0 次或 1 次 | ab?c → ac, abc |
{n} | 恰好 n 次 | a{3} → aaa |
{n,m} | n 到 m 次 | a{2,4} → aa, aaa, aaaa |
{n,} | 至少 n 次 | a{2,} → aa, aaa, … |
10.3 grep 正则匹配
# 基本匹配
echo "Hello World" | grep "World" # 匹配
echo "Hello World" | grep -i "hello" # 忽略大小写
# 行号显示
grep -n "error" /var/log/syslog
# 递归搜索
grep -rn "TODO" ./src/
# 只输出匹配部分
echo "Hello World 123" | grep -oE '[0-9]+' # 输出: 123
# 反向匹配(不包含)
grep -v "debug" logfile.txt
# 仅匹配文件名
grep -rl "pattern" /path/to/dir/
# 统计匹配行数
grep -c "error" logfile.txt
# 上下文显示
grep -B 2 -A 3 "error" logfile.txt # 前2行后3行
# ERE 模式
grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<< "192.168.1.1"
# PCRE 模式
grep -P '\d{3}-\d{4}' <<< "电话: 123-4567"
10.4 sed 正则替换
# 基本替换
echo "Hello World" | sed 's/World/Bash/' # Hello Bash
echo "hello hello" | sed 's/hello/Hi/g' # Hi Hi(全局替换)
# 使用不同的分隔符(处理路径时有用)
echo "/usr/local/bin" | sed 's|/usr/local|/opt|' # /opt/bin
echo "/usr/local/bin" | sed 's#/usr/local#/opt#' # /opt/bin
# 捕获组与反向引用
echo "2026-05-10" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'
# 输出: 10/05/2026
# 删除匹配行
sed '/^#/d' config.txt # 删除注释行
sed '/^$/d' config.txt # 删除空行
# 插入和追加
sed '2a\新增的一行' file.txt # 在第2行后追加
sed '2i\新增的一行' file.txt # 在第2行前插入
# 范围操作
sed '3,5d' file.txt # 删除第3-5行
sed '/start/,/end/d' file.txt # 删除 start 到 end 之间的行
# 就地编辑(直接修改文件)
sed -i 's/old/new/g' file.txt # Linux
sed -i '' 's/old/new/g' file.txt # macOS
sed -i.bak 's/old/new/g' file.txt # 带备份
10.5 Bash [[ =~ ]] 正则匹配
# 基本匹配
email="[email protected]"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "有效的邮箱地址"
fi
# BASH_REMATCH 捕获组
version="v1.2.3"
if [[ "$version" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "完整匹配: ${BASH_REMATCH[0]}" # v1.2.3
echo "主版本: ${BASH_REMATCH[1]}" # 1
echo "次版本: ${BASH_REMATCH[2]}" # 2
echo "补丁: ${BASH_REMATCH[3]}" # 3
fi
# 提取 IP 地址
log_line="2026-05-10 192.168.1.100 ERROR Something happened"
if [[ "$log_line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
echo "IP: ${BASH_REMATCH[0]}"
fi
# 验证输入格式
validate_date() {
local input="$1"
if [[ "$input" =~ ^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$ ]]; then
return 0
else
return 1
fi
}
validate_date "2026-05-10" && echo "有效日期" || echo "无效日期"
validate_date "2026-13-45" && echo "有效日期" || echo "无效日期"
# 正则变量(需要存入变量才能正常工作)
pattern='^[0-9]+$'
if [[ "12345" =~ $pattern ]]; then
echo "是纯数字"
fi
# ⚠️ 注意:不能将正则放在引号中
# [[ "123" =~ "^[0-9]+$" ]] # ❌ 这会变成字面匹配
⚠️ 重要:在 [[ =~ ]] 中,正则表达式不能用引号包裹(会变成字面匹配)。但可以存入变量中使用。
10.6 awk 模式匹配
# awk 正则匹配
echo -e "apple\nbanana\napricot\ncherry" | awk '/^a/'
# 条件匹配
awk '$3 ~ /^[0-9]+$/ {print $1, $3}' <<< "item1 price 100"
# 提取并转换
echo "2026-05-10 error: something failed" | \
awk '{
if (match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
print "日期:", substr($0, RSTART, RLENGTH)
}
if (match($0, /error: (.+)/, arr)) {
print "错误:", arr[1]
}
}'
# 复杂文本解析
cat << 'EOF' | awk -F'|' 'NR>2 && $3 ~ /active/ {printf "%s (%s)\n", $2, $4}'
ID|Name |Status |Email
--|------ |-------|-----
1 |张三 |active |[email protected]
2 |李四 |inactive|[email protected]
3 |王五 |active |[email protected]
EOF
10.7 业务场景:日志解析与验证
#!/bin/bash
# log_parser.sh —— 正则表达式综合应用
set -euo pipefail
# 验证函数集合
validators::is_ipv4() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
# 检查每段 0-255
IFS='.' read -r a b c d <<< "$ip"
((a <= 255 && b <= 255 && c <= 255 && d <= 255))
}
validators::is_email() {
[[ "$1" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}
validators::is_url() {
[[ "$1" =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]]
}
validators::is_semver() {
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$ ]]
}
# 测试
echo "=== 输入验证测试 ==="
tests=("192.168.1.1" "999.999.999.999" "not-an-ip")
for ip in "${tests[@]}"; do
validators::is_ipv4 "$ip" && echo "✅ $ip 是有效IP" || echo "❌ $ip 无效"
done
# Nginx 日志解析
parse_nginx_log() {
local line="$1"
if [[ "$line" =~ ^([0-9.]+)\ -\ -\ \[([^\]]+)\]\ \"([A-Z]+)\ ([^\ ]+)\ [^\"]+\"\ ([0-9]+)\ ([0-9]+) ]]; then
echo "IP: ${BASH_REMATCH[1]}"
echo "时间: ${BASH_REMATCH[2]}"
echo "方法: ${BASH_REMATCH[3]}"
echo "路径: ${BASH_REMATCH[4]}"
echo "状态码: ${BASH_REMATCH[5]}"
echo "大小: ${BASH_REMATCH[6]} bytes"
else
echo "解析失败"
fi
}
echo ""
echo "=== Nginx 日志解析 ==="
sample_log='192.168.1.100 - - [10/May/2026:13:45:30 +0000] "GET /api/users HTTP/1.1" 200 1234'
parse_nginx_log "$sample_log"
# 从文本中提取所有 URL
extract_urls() {
grep -oE 'https?://[a-zA-Z0-9./?=&#%_-]+' "$@" 2>/dev/null
}
# 从文本中提取所有邮箱
extract_emails() {
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' "$@" 2>/dev/null
}
10.8 正则表达式常用模式速查
| 模式 | 用途 | 示例 |
|---|
^[0-9]+$ | 纯数字 | 12345 |
^[a-zA-Z]+$ | 纯字母 | Hello |
^[a-zA-Z0-9_]+$ | 字母数字下划线 | user_123 |
^\S+@\S+\.\S+$ | 简易邮箱 | [email protected] |
^https?:// | URL 开头 | https://... |
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} | IPv4 | 192.168.1.1 |
^# | 注释行 | # comment |
^\s*$ | 空白行 | |
\b[0-9a-f]{32}\b | MD5 哈希 | d41d8cd98f00... |
[0-9]{4}-[0-9]{2}-[0-9]{2} | ISO 日期 | 2026-05-10 |
10.9 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|
=~ 中使用引号正则 | 变成字面匹配 | 将正则存入变量 |
BRE 中 + ? 不工作 | 需要转义 | 使用 grep -E 或 sed -E |
| 贪婪匹配 | .* 匹配尽可能多 | 使用 .*?(PCRE)或更精确的模式 |
. 匹配换行 | 默认 . 不匹配 \n | 使用 [\s\S] 或 s 标志 |
| 特殊字符未转义 | . * + 等在文本中 | 使用 \ 转义 |
10.10 扩展阅读