批量改文件名,Shell 脚本跑到一半炸了,我查到是引号没加对
前两天同事发来一批图片,让我传到服务器上做归档。文件是从 Windows 机器上拷过来的,文件名五花八门,什么 IMG 2026 11 01 (1).jpg、会议 记录 - 终版 v2.png,带空格带括号,看着就头疼。我寻思写个 Shell 脚本批量处理一下,把空格换成下划线,括号去掉,十分钟的事。
结果这十分钟拖成了一个下午。
第一版脚本,跑到一半就炸了
我随手写了个 for 循环:
#!/bin/bash
for f in *.jpg *.png; do
new=$(echo $f | tr ' ' '_' | tr -d '()')
mv $f $new
done
跑起来,前几个文件还挺正常,到了 IMG 2026 11 01 (1).jpg 直接报错:
mv: 无法获取 'IMG' 的文件状态: 没有那个文件或目录
mv: 无法获取 '2026' 的文件状态: 没有那个文件或目录
文件名被空格切成了好几段,mv 收到的根本不是一个文件。我第一反应是 echo $f | tr 那边有问题,盯着看了半天才发现,问题在 mv $f $new 这一行——$f 和 $new 都没加引号。Shell 做单词拆分的时候,带空格的变量值会被拆成多个参数传给 mv。
改成这样就好了:
mv "$f" "$new"
这个坑我以前其实知道,写的时候手一快就漏了。更气人的是,echo $f | tr ' ' '_' 这一行里 $f 也没加引号,只不过 echo 收到多个参数会用空格连起来输出,效果上碰巧没错,把真正的毛病藏住了。
修复之后,括号又闹了一出
引号加上之后,空格的问题没了,括号又出来了。文件名里的 (1) 在 tr -d '()' 里倒是去掉了,但 for f in *.jpg 这一行本身就有隐患:通配符展开的结果如果文件名里带换行符,for 循环照样会拆错。这批文件里没有换行符,算我运气好,但既然写了脚本,就得把这个口子堵上。
我把循环改成了用 find 加 while read:
find . -maxdepth 1 -type f \( -name '*.jpg' -o -name '*.png' \) -print0 |
while IFS= read -r -d '' f; do
base=$(basename "$f")
new=$(echo "$base" | tr ' ' '_' | tr -d '()')
mv "$f" "$new"
done
-print0 加 read -d '' 是成对的,文件名里哪怕有换行符也能完整传过去。IFS= 不能少,不然 read 会把开头结尾的空格吃掉。-r 是防止反斜杠被转义。这三个细节少一个,这套写法就不牢靠。
find 找到的文件,mv 的时候又撞了名
脚本改完再跑,走到一半又停了:
mv: 'a_b.jpg' 和 'a b.jpg' 是同一文件吗?
不对,准确说报错是目标文件已存在。两个不同的原文件,a b.jpg 和 a b.jpg(两个空格),替换空格之后都变成了 a_b.jpg,第二个 mv 就把第一个覆盖了——还好 mv 默认会直接覆盖,我这批文件里撞名的那对内容不一样,差点丢数据。
我加了个判断,目标文件存在就跳过并记下来:
if [ -e "$new" ]; then
echo "跳过:$f -> $new 已存在" >> rename_skip.log
continue
fi
mv "$f" "$new"
顺手把跳过的文件记到日志里,跑完统一看一眼,再手动处理。批量操作最怕的就是静默覆盖,宁可跳过也别硬写。
最后一个坑:目录里混进了子目录
我把脚本放到图片目录里跑,有一次忘了加 -maxdepth 1,find 钻进子目录,把里面的文件也改了名,还因为 mv "$f" "$new" 里 $new 只有文件名没有目录,文件被挪到了当前目录下。目录结构直接乱了套。
教训是两点:find 不加 -maxdepth 默认递归,批量改名一定要先想清楚范围;mv 的目标路径要写完整,别只给文件名。这两个都是"看着没问题,跑起来才出事"的典型。
最后定下来的版本
折腾完之后,脚本长这样:
#!/bin/bash
set -u
LOG=rename_$(date +%Y%m%d).log
: > "$LOG"
find . -maxdepth 1 -type f \( -name '*.jpg' -o -name '*.png' \) -print0 |
while IFS= read -r -d '' f; do
base=$(basename "$f")
new=$(echo "$base" | tr ' ' '_' | tr -d '()')
[ "$base" = "$new" ] && continue
if [ -e "$new" ]; then
echo "跳过:$f -> $new 已存在" >> "$LOG"
continue
fi
mv "$f" "$new" && echo "已改名:$f -> $new" >> "$LOG"
done
跑之前我先拿个测试目录试了一遍,里面放了十几个带空格、括号、中文、甚至换行符的文件名,确认都没问题才在正式目录跑。正式跑之前还先用 cp -r 备了一份——批量改名这种操作,备份的成本比恢复的成本低得多。
这一下午我记住的几件事
- Shell 里变量引用默认要加双引号,不加就是等着单词拆分来坑你,
"$f"这种写法应该成肌肉记忆。 for f in *.jpg遇到特殊文件名就靠不住,find -print0配while read -d ''才是稳妥的批量处理写法。- 批量改名之前先想好撞名怎么办,跳过并记日志比直接覆盖安全得多。
find默认递归,范围要显式控制,mv的目标路径要写全。- 动手之前先备份,批量操作没有后悔药。
- 点赞
- 收藏
- 关注作者
评论(0)