Linux · 5 分钟阅读
Shell 字符串截取
${var#pattern}不是"删除文件前缀"——它是 Shell 参数展开(parameter expansion)里的一类:去掉字符串开头/结尾匹配的内容。这条语法在处理文件路径、日志轮转、批量重命名时高频出现,看懂一次就省无数次cut/sed。
一句话解释
${var#pattern} — 去掉 var 开头最短匹配 pattern 的部分。
${var##pattern} — 去掉 var 开头最长匹配 pattern 的部分。
${var%suffix} — 去掉 var 结尾最短匹配。
${var%%suffix} — 去掉 var 结尾最长匹配。
| 符号 | 位置 | 匹配方式 |
|---|---|---|
# |
头部 | 最短 |
## |
头部 | 最长 |
% |
尾部 | 最短 |
%% |
尾部 | 最长 |
一个
#是单字符去掉;两个##是贪婪去掉。%同理,方向相反。
跑一遍例子
f="/usr/local/bin/app.tar.gz"
echo "${f#*/}" # usr/local/bin/app.tar.gz
echo "${f##*/}" # app.tar.gz
echo "${f%.*}" # /usr/local/bin/app.tar
echo "${f%%.*}" # /usr/local/bin/app
四个搭配把"路径名 / 文件名 / 去掉扩展名 / 只留目录"全拆出来了,不用 basename / dirname 也能写。
文件名处理的四件套
path="/srv/web/main.py"
dir="${path%/*}" # /srv/web
file="${path##*/}" # main.py
base="${file%.*}" # main
ext="${file##*.}" # py
dir用了%/*(去掉最后一个/及之后),file用了##*/(去掉最后一个/及之前)。这是 basename/dirname 思路的 shell 版。
批量改后缀
for f in *.md; do
mv "$f" "${f%.md}.markdown"
done
${f%.md} 去掉结尾的 .md,再拼上新后缀。比 sed 简单一截。
去前缀:去掉 UUID / 时间戳
# 文件名是 20260804_report.csv,想拿 "report.csv"
name="20260804_report.csv"
echo "${name#*_}" # report.csv
# 想去掉所有数字前缀
echo "${name##*[[:alpha:]]}" # report.csv
*是通配符——这里它匹配"任意字符任意长度",不是正则。
模式里能用通配符
# 和 % 的 pattern 用的是 shell 通配符(* ? [...]),不是正则。
v="abc123def456"
echo "${v#*[0-9]}" # 23def456 去掉到第一个数字为止
echo "${v##*[0-9]}" # 456 去掉到最后一个数字为止
echo "${v%[a-z]*}" # abc123 去掉末尾字母
字符类
[0-9]跟正则写法一致。[[:digit:]]这类 POSIX 字符类也支持。
跟 sed 比一比
| 需求 | 参数展开 | sed |
|---|---|---|
| 去掉路径前缀 | ${p#*/} |
`echo “$p” | sed 's |
| 去掉扩展名 | ${f%.*} |
`echo “$f” | sed 's |
| 改后缀 | ${f%.old}.new |
`echo “$f” | sed 's |
参数展开是 shell 内置,没有 fork 子进程,比 sed 快 10–100 倍——批量处理上千个文件时差距明显。
一些容易踩的坑
1. 模式没匹配 → 原样返回
x="hello"
echo "${x#xyz}" # hello(没匹配上,原样输出)
2. 模式里别加引号
f="app.tar.gz"
echo "${f%".gz"}" # 错误!把 ".gz" 当字面量,引号反而让 % 不工作
echo "${f%.gz}" # 正确
3. 数组也能用
arr=("/a/b/c" "/d/e/f")
echo "${arr[@]##*/}" # c f
4. 默认值(容易和 # 混淆)
${var:-default} 是默认值,和 ${var#pattern} 长得像但完全两回事。- 和 + 操作符的列表:
| 写法 | 含义 |
|---|---|
${var:-x} |
为空或未定义时用 x |
${var:=x} |
为空或未定义时赋值为 x |
${var:+x} |
非空时返回 x,否则空 |
${var:?msg} |
为空时打印 msg 并退出 |
${var#x} / ${var##x} |
头部截短 |
${var%x} / ${var%%x} |
尾部截短 |
-=+?是"默认值族";###%%%是"截短族"。看符号位置分清。
实战:日志归档
# 把 access.log.YYYYMMDD 滚动成 .gz
for f in /var/log/nginx/access.log.*; do
[[ "$f" =~ \.log\.[0-9]{8}$ ]] || continue
date_part="${f##*.log.}" # 取日期部分
gz="/var/log/nginx/archive/access-${date_part}.log.gz"
gzip -c "$f" > "$gz"
rm "$f"
done
实战:批量改 Makefile 里的旧路径
for f in Makefile */Makefile; do
sed -i.bak "s|/opt/old|/opt/new|g" "$f"
[[ -f "$f.bak" ]] && rm "$f.bak"
done
这个例子用
sed因为要做"全文件替换 + 原地改写"——参数展开的定位是"取一个值出来",不是"全文改"。
调试小技巧
# 在脚本里看展开结果
set -x
f="/a/b/c.tar.gz"
echo "${f##*/}"
# + echo c.tar.gz
set -x 把每条命令展开后的样子打出来。觉得展开不符合预期时这样对照最快。
一些经验
- 处理"已知结构"的字符串优先用参数展开:零 fork,可读
- 全文替换、模式很复杂才上
sed/awk - 写
##%%时手画一遍匹配范围,贪婪和最短别弄反 - 在
for f in *.log这类循环里配合${f%.*}做扩展名操作非常顺手