Linux Shell 中路径截取的正则表达式
在 Shell 脚本里处理路径时,最经常遇到的不是“怎么拼字符串”,而是“怎么从一串目录名里把文件名、目录、扩展名准确截出来”。这类操作看起来很小,但在部署脚本、日志处理脚本和自动化构建流程里非常常见。一个小小的路径处理写错,可能会让脚本把整个文件夹名误当成文件名。
假设有一个文件路径:
file=/dir1/dir2/dir3/my.file.txt
我们可以用 ${ } 的参数展开方式获得不同的值。
从左边截取(使用 #)
# 表示删除匹配到的最短前缀(从左边开始,非贪婪),## 表示删除匹配到的最长前缀(贪婪)。
# 拿掉第一条 / 及其左边的字串 → dir1/dir2/dir3/my.file.txt
echo ${file#*/}
# 拿掉最后一条 / 及其左边的字串 → my.file.txt
echo ${file##*/}
# 拿掉第一个 . 及其左边的字串 → file.txt
echo ${file#*.}
# 拿掉最后一个 . 及其左边的字串 → txt
echo ${file##*.}
从右边截取(使用 %)
% 表示删除匹配到的最短后缀(从右边开始,非贪婪),%% 表示删除匹配到的最长后缀(贪婪)。
# 拿掉最后一条 / 及其右边的字串 → /dir1/dir2/dir3
echo ${file%/*}
# 拿掉第一条 / 及其右边的字串 → (空值)
echo ${file%%/*}
# 拿掉最后一个 . 及其右边的字串 → /dir1/dir2/dir3/my.file
echo ${file%.*}
# 拿掉第一个 . 及其右边的字串 → /dir1/dir2/dir3/my
echo ${file%%.*}
各结果对照表
以 file=/dir1/dir2/dir3/my.file.txt 为例:
| 表达式 | 结果 |
|---|---|
${file#*/} |
dir1/dir2/dir3/my.file.txt |
${file##*/} |
my.file.txt |
${file#*.} |
file.txt |
${file##*.} |
txt |
${file%/*} |
/dir1/dir2/dir3 |
${file%%/*} |
(空值) |
${file%.*} |
/dir1/dir2/dir3/my.file |
${file%%.*} |
/dir1/dir2/dir3/my |
记忆技巧
- 看符号朝向:
#在键盘上位于数字键左侧,表示从左边开始删;%表示从右边开始删; - 看符号个数:单个符号是“最短匹配”(非贪婪),双符号是“最长匹配”(贪婪);
- 常见用法:
${file##*/}取文件名、${file%/*}取所在目录、${file%.*}去掉最后一个扩展名、${file##*.}取扩展名。
实际应用示例
在脚本中获取某个文件的路径、文件名和扩展名:
file=/var/www/html/index.php
dir=$(dirname "$file") # /var/www/html
base=$(basename "$file") # index.php
name=${file%.*} # /var/www/html/index
ext=${file##*.} # php
一个更贴近真实场景的例子
假设你在写一个部署脚本,目标是把上传的压缩包解压后,自动生成目标目录并记录日志。例如:
archive=/data/releases/app-v1.2.3.tar.gz
base_dir=${archive%.*}
package_name=${archive##*/}
extension=${archive##*.}
mkdir -p "/data/releases/${base_dir##*/}"
echo "部署包: $package_name"
echo "扩展名: $extension"
真实部署场景:从压缩包名生成目标目录
在实际发布流程里,通常不是只处理一个文件,而是要同时管理多个版本包、回滚包和临时目录。假设你把一个压缩包上传到服务器,文件名是 /data/releases/app-service-2026.08.01.tar.gz,想把它解压到一个和版本号一致的目录,并把日志也放到同名目录下。这个时候就很适合把文件名、父目录和后缀拆开处理:
archive=/data/releases/app-service-2026.08.01.tar.gz
version=${archive##*/}
base_name=${version%.tar.gz}
release_dir=${archive%/*}
target_dir="$release_dir/$base_name"
mkdir -p "$target_dir"
echo "解压目录: $target_dir"
这个例子体现了几个实用点:首先,${archive##*/} 用来取出压缩包名;其次,${version%.tar.gz} 可以把 .tar.gz 这种后缀去掉,得到更适合目录命名的版本标识;最后,${archive%/*} 取出父目录,避免把路径硬编码在脚本里。这样的写法在 CI/CD 发布、站点回滚和日志归档场景里都非常稳妥。
排查提示:先把中间变量打印出来
如果脚本的结果和预期不一致,最先做的不是立刻修改正则,而是把提取出的中间值打印出来。下面这个小技巧特别适合排查“为什么目录名变了”或者“为什么扩展名取错了”的情况:
archive=/data/releases/app-service-2026.08.01.tar.gz
echo "原始路径: $archive"
echo "文件名: ${archive##*/}"
echo "父目录: ${archive%/*}"
echo "去后缀: ${archive%.tar.gz}"
echo "扩展名: ${archive##*.}"
这样你能很快判断问题是出在路径格式、文件名里的点号,还是后缀匹配方式上。它比盲目猜测更有效,也更容易被后续维护者理解。
这类路径截取技巧在编写部署脚本、日志处理脚本时非常实用。更多 Shell 脚本与服务器运维内容,可以参考开发工具分类下的Linux 服务器运维基础和服务器运维技巧。
结合 dirname 和 basename 的实用组合
在实际脚本里,单独使用路径截取很容易让人陷入“只会取值,不知道怎么配合其他命令”的状态。dirname 和 basename 是两个非常实用的辅助工具,它们和参数展开一起使用时,能把路径处理逻辑写得更清楚。比如你要把一个压缩包解压到与包名相同的目录,先取出文件名和目录名,再生成目标目录,就会比硬编码路径更稳。
archive=/opt/releases/app-2026.08.01.tar.gz
folder=$(basename "$archive" .tar.gz)
workdir=$(dirname "$archive")
mkdir -p "$workdir/$folder"
echo "解压目录: $workdir/$folder"
常见陷阱:路径末尾的斜杠和空字符串
Shell 路径处理里还有一个容易犯的错误:当路径以 / 结尾时,很多人会误把它当成普通目录名。比如 /var/www/html/ 和 /var/www/html 的含义看起来很接近,但在脚本里如果后续拼接路径,结果可能会多出一个斜杠,或在某些命令下出现意外空字符串。遇到这种情况,最好先做一次规范化处理,再继续做后续操作。
path="/var/www/html/"
path=${path%/}
[ -d "$path" ] && echo "目录存在"
这类写法在自动化发布、日志归档和站点部署脚本里尤其常见。它比单纯的字符串替换更容易被后续维护者看懂。
什么时候值得把路径处理封装成函数
如果你写脚本的次数越来越多,路径处理逻辑会反复出现。比如某些脚本需要取目录、另一些需要取文件名,甚至会把扩展名、时间戳或版本号拼接到路径里。此时把常见逻辑封装成一个小函数,会比每次手写参数展开更容易维护。一个简单的例子可以把文件名、目录和扩展名提取成统一入口,后面改动时只需要改一处。
get_path_parts() {
local path="$1"
echo "dir=${path%/*}"
echo "base=${path##*/}"
echo "ext=${path##*.}"
}
get_path_parts "/var/www/html/index.php"
这类小封装通常不会立刻显得“高级”,但它能显著降低脚本在后续迭代时的维护成本。
原文链接:https://www.cnblogs.com/cqzhuomi/articles/17284324.html(博客园 CQZHUOMI,转载)