强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 扩展阅读