批量改文件名,Shell 脚本跑到一半炸了,我查到是引号没加对

举报
yd_242783495 发表于 2026/11/12 14:23:01 2026/11/12
【摘要】 一堆从 Windows 传过来的图片文件名里带空格和括号,我写了个 for 循环批量重命名,结果跑到一半报错,文件名被截得乱七八糟。顺着报错一层层查,发现问题全在变量没加引号上,顺带还踩了 find 和 xargs 的两个坑。把排查过程和最后能用的版本记下来。

前两天同事发来一批图片,让我传到服务器上做归档。文件是从 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 的目标路径要写全。
  • 动手之前先备份,批量操作没有后悔药。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。