麒麟 Linux|Shell字符串表达式全解析:99%的脚本

在Shell世界里,字符串不是“小事”——它几乎决定了脚本的成败。

无论是数据库连接、日志处理,还是配置文件解析,字符串表达式都是最被低估却最常出错的环节。

本文带你全面掌握Shell中的字符串操作,从-z、-n到${}参数扩展,再到正则与模式匹配,用最直观的示例拆解每个用法的正确姿势。

一文吃透Shell字符串处理的全部技能点,让你的脚本稳如磐石!

01

字符串测试与比较

条件测试是判断字符串状态的基础。

这一般在 if 语句中,通过 [] (单括号) 或 [[]] (双括号) 实现。

1

常用字符串测试操作符

麒麟 Linux|Shell字符串表达式全解析:99%的脚本

注意

  • 在 [] 中,= 和 == 是同义的。
  • 在 [[]] 中,< 和 > 执行字典序比较。在 [] 中,它们是重定向操作符,不能用于字符串比较。
  • 在 [] 中,必须对变量使用双引号,以防止因变量为空或包含空格而导致的解析错误。[[]] 则无此问题。

2

实验:字符串状态与比较

&&、|| 和 ; 等操作符根据前一个命令的退出码,来决定是否执行以及如何执行后一个命令。

02

控制操作符详解

场景

检查一个PostgreSQL数据库连接字符串和用户名的状态。

#!/bin/bash

DB_USER="postgres"
EMPTY_USER=""
CONN_STR_A="host=localhost port=5432"
CONN_STR_B="host=localhost port=5432"

echo "--- 字符串状态测试 ---"
if [[ -z $EMPTY_USER ]]; then
    echo "变量 EMPTY_USER 为空。"
fi
if [[ -n $DB_USER ]]; then
    echo "变量 DB_USER 不为空,值为: $DB_USER"
fi

echo -e "
--- 字符串相等性比较 ---"
if [[ "$CONN_STR_A" == "$CONN_STR_B" ]]; then
    echo "连接字符串A和B相等。"
fi

echo -e "
--- 字典序比较(陷阱演示) ---"
VERSION_A="2.1.10"
VERSION_B="2.1.2"
if [[ "$VERSION_A" < "$VERSION_B" ]]; then
    echo "字典序比较结果: '$VERSION_A' 小于 '$VERSION_B' (由于字符'1'小于'2')"
fi

解析

  • -z 和 -n 是判断字符串是否为空的可靠方法。
  • == 用于准确比较两个字符串的内容。
  • 字典序比较的实验揭示了一个常见陷阱:Shell是逐字符比较,因此 “2.1.10” 会被判定为小于 “2.1.2”。这说明不能用简单的字符串比较来处理版本号。

03

字符串参数扩展

参数扩展是Shell中进行字符串操作的核心,它无需调用外部命令(如 sed, awk, cut),效率极高。

1

获取长度: ${#string}

描述: 返回字符串中的字符数。

示例:

FILENAME="backup_prod_db.sql.gz"
LEN=${#FILENAME}
echo "文件名 '$FILENAME' 的长度是: $LEN"
# 输出: 文件名 'backup_prod_db.sql.gz' 的长度是: 23

2

子字符串提取: ${string:offset:length}

描述: 从 offset 位置(从0开始)开始,提取 length 个字符。

如果省略 length,则提取到末尾。

示例: 从日志时间戳中提取日期和时间。

LOG_TIMESTAMP="2025-10-15T14:30:05"
DATE_PART=${LOG_TIMESTAMP:0:10}  # 从第0位开始,取10个字符
TIME_PART=${LOG_TIMESTAMP:11}    # 从第11位开始,取到末尾
echo "日期: $DATE_PART, 时间: $TIME_PART"
# 输出: 日期: 2025-10-15, 时间: 14:30:05

3

模式匹配与移除

这是处理文件名和路径最强劲的工具。

  • ${string#pattern}

从开头删除最短匹配 pattern 的部分。

  • ${string##pattern}

从开头删除最长匹配 pattern 的部分。

  • ${string%pattern}

从结尾删除最短匹配 pattern 的部分。

  • ${string%%pattern}

从结尾删除最长匹配 pattern 的部分。

示例: 解析一个完整的文件路径。

FULL_PATH="/var/lib/postgresql/data/base/16384/12345.fsm"

FILENAME=${FULL_PATH##*/}
echo "文件名: $FILENAME"

DIR_PATH=${FULL_PATH%/*}
echo "目录路径: $DIR_PATH"

BASENAME=${FILENAME%.*}
echo "文件基础名: $BASENAME"

EXTENSION=${FILENAME##*.}
echo "文件扩展名: $EXTENSION"

输出:

文件名: 12345.fsm
目录路径: /var/lib/postgresql/data/base/16384
文件基础名: 12345
文件扩展名: fsm

4

搜索与替换

  • ${string/pattern/replacement}

将第一个匹配 pattern 的部分替换为 replacement。

  • ${string//pattern/replacement}

将所有匹配 pattern 的部分替换为 replacement。

示例: 格式化一个主机列表。

HOST_LIST="db-node1 db-node2 db-node3"
CSV_HOSTS=${HOST_LIST// /,}
echo "CSV格式的主机列表: $CSV_HOSTS"

FIRST_HOST_RENAMED=${HOST_LIST/db-/database-}
echo "重命名第一个主机后: $FIRST_HOST_RENAMED"

输出:

CSV格式的主机列表: db-node1,db-node2,db-node3
重命名第一个主机后: database-node1 db-node2 db-node3

5

大小写转换 (Bash 4.0+)

  • ${string^pattern}

将第一个匹配 pattern 的字符转为大写。

  • ${string^^pattern}

将所有匹配 pattern 的字符转为大写。

  • ${string,pattern}

将第一个匹配 pattern 的字符转为小写。

  • ${string,,pattern}

将所有匹配 pattern 的字符转为小写。

如果省略 pattern,则作用于所有字符。

示例:

STATUS="running"
echo "状态 (大写): ${STATUS^^}"
# 输出: 状态 (大写): RUNNING

6

设置默认值

这对于编写能处理未定义或空变量的健壮脚本至关重大。

麒麟 Linux|Shell字符串表达式全解析:99%的脚本

示例: 在脚本中处理可选和必需的配置。

#!/bin/bash

# PG_HOST 未被设置
# PG_USER 已被设置
PG_USER="admin"

# 1. 使用 :- 提供一个临时的默认值
# 用于连接命令,但不改变原始变量
echo "正在连接到主机: ${PG_HOST:-"localhost"}..."

# 2. 使用 := 设置一个持久的默认值
# 如果 LOG_LEVEL 未设置,则将其设置为 INFO
echo "原始日志级别: $LOG_LEVEL"
echo "当前日志级别: ${LOG_LEVEL:="INFO"}"
echo "设置后的日志级别: $LOG_LEVEL"

# 3. 使用 :? 强制要求变量必须存在
# 如果 DB_NAME 未设置,脚本将报错并退出
# DB_NAME="" # 撤销此行注释来触发错误
: "${DB_NAME:?"错误:必须提供数据库名称(DB_NAME)。"}"
echo "数据库名称: $DB_NAME" # 如果DB_NAME未设置,这行不会执行

# 4. 使用 :+ 根据变量是否存在来构造参数
# 如果 DEBUG_MODE 变量存在,则在命令后添加 --verbose 标志
DEBUG_MODE="true"
VERBOSE_FLAG="${DEBUG_MODE:+"--verbose"}"
echo "启动命令: my_app_runner ${VERBOSE_FLAG}"

输出 (假设 DB_NAME 被设置为 “testdb”):

正在连接到主机: localhost...
原始日志级别:
当前日志级别: INFO
设置后的日志级别: INFO
数据库名称: testdb
启动命令: my_app_runner --verbose

04

[[]] 中的高级字符串匹配

[[]] 关键字提供了 [] 所不具备的高级匹配功能。

1

通配符模式匹配 (Globbing)

在 [[]] 中,== 和 != 操作符的右侧若不加引号,会被视为一个通配符模式。

示例: 检查一个文件名是否为日志文件。

FILENAME="application-2025-10-15.log"
if [[ "$FILENAME" == *.log ]]; then
    echo "'$FILENAME' 是一个日志文件。"
fi

解析

*.log 匹配任何以 .log 结尾的字符串。

2

正则表达式匹配 (=~)

=~ 操作符允许使用扩展正则表达式(ERE)进行匹配。

示例: 验证一个字符串是否为合法的IP地址格式。

IP_ADDR="192.168.1.10"
REGEX="^([0-9]{1,3}.){3}[0-9]{1,3}$"

if [[ "$IP_ADDR" =~ $REGEX ]]; then
    echo "'$IP_ADDR' 格式符合IP地址规范。"
fi

注意

为了兼容性,正则表达式本身最好不要加引号。

05

最佳实践与注意事项

1

优先使用内置扩展

参数扩展(如 ${…})是Shell内置功能,其性能远高于调用外部命令如 sed, awk, cut。

在追求效率的脚本中应优先使用。

2

注意引号的使用

  • 在 [] 中,对变量使用双引号是强制性的,以防止解析错误。
  • 在 [[]] 中,变量一般无需加引号,但如果要进行准确的字面量比较而不是模式匹配,则应将 == 右侧的模式用引号括起来,例如 [[ “$VAR” == “*.log” ]]。

3

理解可移植性

[[]] 和许多高级参数扩展(如大小写转换)是Bash等现代Shell的特性,不被POSIX sh 支持。

如果脚本需要极高的可移植性,应使用 [] 和更基础的参数扩展。

4

区分 = 和 ==

在 [] 中,两者是同义词。

在 [[]] 中,两者也都用于比较,但 == 具有模式匹配的特殊含义,因此在 [[]] 中使用 == 更能体现其意图。

写在最后

字符串处理,是Shell脚本中最常见、也最容易埋坑的领域。

真正的高手,不是记住多少命令,而是能写出无惧空值、兼容多Shell、少一行命令也能跑通的代码。

如果你想让脚本更机智、更优雅,收藏这篇文章,实践每一个例子——你会发现,掌握字符串,才算真正掌控了Shell。


作者介绍

麒麟 Linux|Shell字符串表达式全解析:99%的脚本

大家好,我是刘峰,安丫科技创始人 & 数据库技术高级讲师,专注于 PostgreSQL、国产数据库运维与迁移、数据库性能优化 等方向。

作为 PG中国分会官方授权讲师、PostgreSQL ACE 讲师认证专家,我长期活跃在一线项目实战中,拥有 10年以上大型数据库管理与优化经验,曾深度参与电信、金融、政务等多个行业的数据库性能调优与迁移项目。

欢迎关注我,一起深入探索数据库的无限可能,技术交流不设限!

觉得有收获的话,记得点赞、收藏、转发支持一下哦,别忘了关注我获取更多数据库干货~


安呀智数据坊|我们能做什么

无论你是业务系统的技术负责人,还是数据部门的第一响应人,我们都能为你提供可靠的支持:

  • 数据库类型支持

Oracle / MySQL / PostgreSQL / PG / SQL Server 等主流数据库

  • 核心服务内容

性能优化 / 故障处理 / 数据迁移 / 备份恢复 / 版本升级 / 补丁管理

  • 系统性支持

深度巡检 / 高可用架构设计 / 应用层兼容评估 / 运维工具集成

  • 专项能力补充

定制课程培训 / 甲方团队辅导 / 复杂问题协作排查 / 紧急救援支持

如果你有一张删不掉的表、一个跑不动的查询,或者一场说不清的升级风险,欢迎来找我们聊聊。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容