08 - 字符串操作
08 - 字符串操作
8.1 字符串长度
str="Hello, 世界!"
# 获取字符串长度
echo "${#str}" # 输出: 11(字符数)
# 字节数(需要外部工具)
echo "$str" | wc -c # 字节数(UTF-8 中文占3字节)
echo -n "$str" | wc -m # 字符数
# 数组长度
arr=(a b c d e)
echo "${#arr[@]}" # 5(元素个数)
echo "${#arr[0]}" # 1(第一个元素的长度)
8.2 字符串切片(Substring)
str="Hello, World!"
# 从指定位置到末尾
echo "${str:7}" # 输出: World!
# 从指定位置开始,取指定长度
echo "${str:7:5}" # 输出: World
# 从倒数位置开始(Bash 4.2+)
echo "${str: -6}" # 输出: orld! (注意冒号后有空格)
echo "${str:(-6)}" # 输出: orld! (也可以用括号)
# 取倒数第 N 个字符开始的 M 个字符
echo "${str: -6:3}" # 输出: orl
# 从数组中切片
arr=("a" "b" "c" "d" "e")
echo "${arr[@]:1:3}" # 输出: b c d(从索引1开始取3个)
⚠️ 注意:${str: -6} 中冒号后面必须有空格,否则会被解析为 ${str:-6}(默认值)。
8.3 字符串替换
str="Hello, World! Hello, Bash!"
# 替换第一次出现
echo "${str/Hello/Hi}" # 输出: Hi, World! Hello, Bash!
# 替换所有出现(双斜杠)
echo "${str//Hello/Hi}" # 输出: Hi, World! Hi, Bash!
# 替换开头匹配
echo "${str/#Hello/Hi}" # 输出: Hi, World! Hello, Bash!
# 替换结尾匹配
echo "${str/%Bash/Shell}" # 输出: Hello, World! Hello, Shell!
# 删除匹配(替换为空)
filename="document.tar.gz"
echo "${filename%.gz}" # 输出: document.tar(删除最短后缀)
echo "${filename%%.*}" # 输出: document(删除最长后缀)
echo "${filename#*.}" # 输出: tar.gz(删除最短前缀)
echo "${filename##*.}" # 输出: gz(删除最长前缀)
模式匹配删除
| 操作 | 语法 | 说明 | 示例 ${str}=file.tar.gz |
|---|
| 删除最短前缀 | ${str#pattern} | 从左开始,最短匹配 | ${str#*.} → tar.gz |
| 删除最长前缀 | ${str##pattern} | 从左开始,最长匹配 | ${str##*.} → gz |
| 删除最短后缀 | ${str%pattern} | 从右开始,最短匹配 | ${str%.gz} → file.tar |
| 删除最长后缀 | ${str%%pattern} | 从右开始,最长匹配 | ${str%%.*} → file |
💡 记忆技巧:# 在键盘 $ 的左边 → 删除左边(前缀);% 在键盘 $ 的右边 → 删除右边(后缀)。一个符号 → 最短匹配;两个符号 → 最长匹配。
8.4 大小写转换(Bash 4.0+)
str="Hello, World!"
echo "${str^^}" # 全大写: HELLO, WORLD!
echo "${str,,}" # 全小写: hello, world!
echo "${str~}" # 首字母大小写切换(Bash 5.1+): hello, World!
echo "${str~~}" # 所有字母大小写切换(Bash 5.1+): hELLO, wORLD!
# 只转换首字母
echo "${str^^[h]}" # 只把 h 转大写: Hello, World!
echo "${str,,[W]}" # 只把 W 转小写: Hello, world!
# 单词首字母大写
capitalize() {
local str="$1"
echo "${str^}"
}
# 只影响第一个字符,需要更复杂的可以用 awk
echo "hello world" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1'
# 输出: Hello World
8.5 参数展开(Parameter Expansion)完整参考
默认值
# ${var:-default} —— 变量未定义或为空时返回默认值(不改变变量)
name="${1:-anonymous}"
echo "$name"
# ${var:=default} —— 变量未定义或为空时赋值为默认值
: "${config_file:=/etc/app/config.ini}"
echo "$config_file"
# ${var:+alternate} —— 变量已定义且非空时返回替代值
echo "${name:+(已设置: $name)}"
# ${var:?error} —— 变量未定义或为空时报错退出
: "${DB_HOST:?数据库主机未配置}"
: "${DB_PORT:?数据库端口未配置}"
| 语法 | 变量未设置/为空 | 变量已设置且非空 |
|---|
${var:-word} | 返回 word | 返回 $var |
${var:=word} | 设为 word 并返回 | 返回 $var |
${var:+word} | 返回空 | 返回 word |
${var:?msg} | 报错并退出 | 返回 $var |
替换与删除总结
| 操作 | 语法 | 说明 |
|---|
| 替换首个 | ${var/old/new} | 替换第一个匹配 |
| 替换全部 | ${var//old/new} | 替换所有匹配 |
| 替换开头 | ${var/#old/new} | 匹配开头 |
| 替换结尾 | ${var/%old/new} | 匹配结尾 |
| 删除前缀(短) | ${var#pattern} | 最短前缀匹配 |
| 删除前缀(长) | ${var##pattern} | 最长前缀匹配 |
| 删除后缀(短) | ${var%pattern} | 最短后缀匹配 |
| 删除后缀(长) | ${var%%pattern} | 最长后缀匹配 |
| 取子串 | ${var:offset:length} | 切片 |
| 长度 | ${#var} | 字符数 |
| 大写 | ${var^^} | 全大写 |
| 小写 | ${var,,} | 全小写 |
8.6 字符串拼接
# 直接拼接
first="Hello"
second="World"
result="$first, $second!"
echo "$result"
# 数组拼接
words=("Hello" "Beautiful" "World")
result="${words[*]}"
echo "$result" # Hello Beautiful World
# 指定分隔符拼接(IFS)
IFS=', '
result="${words[*]}"
echo "$result" # Hello,Beautiful,World
# 用 printf 拼接
join_by() {
local delimiter="$1"
shift
local first="$1"
shift
printf '%s' "$first" "${@/#/$delimiter}"
}
result=$(join_by ", " "${words[@]}")
echo "$result" # Hello, Beautiful, World
8.7 业务场景:路径处理工具
#!/bin/bash
# path_utils.sh —— 路径处理工具函数
# 获取文件名(去除路径)
file::basename() {
local path="$1"
echo "${path##*/}"
}
# 获取目录名
file::dirname() {
local path="$1"
echo "${path%/*}"
}
# 获取文件扩展名
file::ext() {
local path="$1"
local base="${path##*.}"
[[ "$base" == "$path" ]] && echo "" || echo "$base"
}
# 去除扩展名
file::stem() {
local path="$1"
local base="${path##*/}"
echo "${base%.*}"
}
# 绝对路径检测
file::is_absolute() {
[[ "$1" == /* ]]
}
# 路径拼接
file::join() {
local base="${1%/}"
local rel="$2"
echo "$base/$rel"
}
# 测试
path="/home/user/documents/report.tar.gz"
echo "完整路径: $path"
echo "文件名: $(file::basename "$path")" # report.tar.gz
echo "目录: $(file::dirname "$path")" # /home/user/documents
echo "扩展名: $(file::ext "$path")" # gz
echo "文件主名: $(file::stem "$path")" # report.tar
echo "是否绝对: $(file::is_absolute "$path" && echo 是 || echo 否)"
# 批量文件处理
process_files() {
local dir="$1"
local count=0
for file in "$dir"/*; do
[[ -f "$file" ]] || continue
local ext
ext=$(file::ext "$file")
case "$ext" in
jpg|jpeg|png|gif)
echo "图片: $(file::basename "$file")"
((count++))
;;
mp4|avi|mkv)
echo "视频: $(file::basename "$file")"
((count++))
;;
esac
done
echo "共找到 $count 个多媒体文件"
}
8.8 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|
${str: -6} 空格 | 没有空格会被解析为默认值 | 冒号后加空格 |
${#str} 多字节 | 多字节字符的长度计算 | 使用 wc -m |
| 模式匹配 vs 正则 | # % 使用 glob 模式 | 不是正则表达式 |
替换中的 / | 路径中的 / 导致冲突 | 使用其他分隔符 ${var//old/new} |
| 空变量展开 | ${str:-} 可能不生效 | 检查变量是否 unset vs 空 |
8.9 扩展阅读