这个命令是干啥的
mv 是 move 的缩写,用来移动文件和重命名。表面上看它很简单,但底层逻辑很不一样。
关键要理解一点:mv 在同一分区和跨分区是两种完全不同的操作。同一分区下,mv 只是修改文件名(目录项),数据块纹丝不动,速度极快。跨分区的话,mv 实际上是 cp + rm,先把数据拷贝过去再删除源文件。
知道这个区别后,很多问题就能理解了。比如为什么移动一个大文件到另一个硬盘要等那么久,为什么移动大量小文件很慢而移动大文件反而快。
基本用法(3分钟上手)
# 文件重命名
mv oldname.txt newname.txt
# 文件移动到目录
mv file.txt /target/dir/
# 移动多个文件到目录
mv file1.txt file2.txt file3.txt /target/dir/
# 移动目录
mv mydir/ /target/dir/
进阶骚操作
1. 安全移动:-i 和 -u
# 覆盖前询问
mv -i important.doc /backup/
# 只有目标不存在或更旧时才移动
mv -u source.txt /backup/
-i 在手动操作时很有用。我之前有一次写了个脚本要 mv 一批文件,结果目录搞错了,覆盖了重要文件,当时要是加了 -i 就不会出事。
-u 和 cp 的 -u 类似,只移动更新的文件。在增量整理场景很好用。
2. 备份已存在的文件(-b)
# 如果目标已存在,自动备份
mv -b source.txt dest.txt
# 会生成 dest.txt~ 作为备份
这和 cp 的 -b 类似。我整理文件的时候经常用,万一覆盖错了能从备份文件找回。
3. -v 显示详细过程
# 显示每个文件的移动情况
mv -v *.log /archive/logs/
# 输出类似:
# renamed 'app.log' -> '/archive/logs/app.log'
# renamed 'error.log' -> '/archive/logs/error.log'
写脚本的时候加 -v 可以配合日志记录,出了问题方便排查。
4. 批量重命名(用 rename 替代 mv)
mv 一次只能操作一个文件,批量重命名得写循环。这时候 rename 命令更好用:
# 把所有 .txt 改成 .md
rename 's/\.txt$/.md/' *.txt
# 把文件名中的空格替换成下划线
rename 's/ /_/g' *.mp3
# 把文件名统一转小写
rename 'y/A-Z/a-z/' *.JPG
我整理照片的时候,相机拍出来是 IMG_2024.JPG 这种格式,用 rename 'y/A-Z/a-z/' *.JPG 一行全转小写了。用 mv 的话得写个 for 循环。
如果你用的是 Ubuntu/Debian 的 rename(Perl 版本),语法是正则替换。CentOS 默认不带 rename,得装 yum install rename。
5. 移动前确认目标是否存在
# 移动前检查目标是否存在
test -f /target/dir/file.txt && echo "已存在" || mv file.txt /target/dir/
我常在脚本里这样写,避免不小心覆盖重要文件。
避坑指南
坑1:跨分区移动超慢
这是最坑的地方。很多人以为 mv 在任何情况下都很快,结果从 / 移到 /home(如果 /home 是单独分区)时发现慢得要死。
# 跨分区的 mv 其实是 cp + rm
# 假设 / 和 /data 是不同分区
mv /bigfile.tar.gz /data/backup/
# 这条命令等价于:
# cp /bigfile.tar.gz /data/backup/bigfile.tar.gz
# rm /bigfile.tar.gz
知道这个以后,大文件跨分区移动我直接用 rsync + 确认删除:
# 先拷贝,确认成功再删除源文件
rsync -avhP /bigfile.tar.gz /data/backup/
rm /bigfile.tar.gz
# 或者用 mv 的 -v 参数,如果速度异常就 Ctrl+C
坑2:目录末尾斜杠
和 cp 一样,目录的斜杠有讲究:
# 这俩效果相同
mv dir1/ /target/ # 斜杠可有可无
mv dir1 /target/
# 区别在目标
mv dir1 /target/ # 如果 /target/ 存在 -> /target/dir1/
mv dir1 /target # 如果 /target 存在 -> /target/dir1/
# 注意上面两个结果一样
真正要小心的是目标目录不存在的情况:mv dir1 /target 如果 /target 不存在,会把 dir1 重命名为 target。很多新人被这个搞蒙过。
坑3:移动大量小文件性能差
移动几十万个 1KB 的文件,速度慢得离谱。原因不是数据量大,而是文件系统元数据操作太多:
# 不要这样
mv /tmp/smallfiles/* /data/smallfiles/
# 如果只是批量操作,考虑打包
tar cf smallfiles.tar -C /tmp/smallfiles/ .
mv smallfiles.tar /data/
每个小文件的 mv 都需要修改目录项、更新 inode、写日志。10 万个文件就是 10 万次元数据操作。这种情况我一般先打成 tar 包再移动,到目标位置再解压,快很多。
坑4:通配符匹配不到文件时
# 如果当前目录没有 .log 文件
mv *.log /backup/
# bash 会把 *.log 原样传给 mv
# mv 报错:cannot stat '*.log': No such file or directory
脚本里最好加个检查:
# 先检查有没有匹配的文件
if ls *.log 2>/dev/null; then
mv *.log /backup/
fi
坑5:覆盖只读文件
# 覆盖只读文件会弹确认,但 mv 默认会覆盖
chmod 444 important.txt
mv newfile.txt important.txt
# 提示:overwrite 'important.txt'? (y/n) [n]
如果没注意,按了 y,只读文件就被覆盖了。-f 可以强制覆盖不提示。
实战场景(重点!结合真实运维场景)
场景1:日志轮转
运维中最常见的 mv 用法之一:
#!/bin/bash
# 日志轮转脚本:保留最近7天的日志
LOG_DIR="/var/log/myapp"
CURRENT_LOG="${LOG_DIR}/app.log"
# 如果当前日志超过 100MB,就轮转
if [ $(stat -c%s "$CURRENT_LOG") -gt 104857600 ]; then
# 给旧日志重命名,加上日期戳
mv "$CURRENT_LOG" "${LOG_DIR}/app.$(date +%Y%m%d_%H%M%S).log"
# 创建新的空日志文件
touch "$CURRENT_LOG"
# 保留最近7天的日志,删除更早的
find "$LOG_DIR" -name "app.*.log" -mtime +7 -delete
fi
这个脚本我放 cron 里每小时跑一次,日志超过 100MB 自动轮转,保留一周。mv 比 cp + truncate 的方式好,因为 mv 后原日志文件就被释放了,应用程序会重新创建日志文件(前提是应用是每次打开文件写入的那种)。
场景2:发布系统原子替换
发布新版本时,用 mv 做原子替换,避免服务中断:
#!/bin/bash
# 蓝绿发布:新旧版本目录切换
# 新版代码已经部署到了 /app/release-v2/
# 当前版本在 /app/current/(软链接)
NEW_RELEASE="/app/release-v2"
# 停服前先备份当前版本
mv /app/current /app/rollback-bak
# 把新版本切换到当前
ln -sf "$NEW_RELEASE" /app/current
# 通知服务重载配置
systemctl reload myapp
echo "发布完成,回滚命令:rm /app/current && mv /app/rollback-bak /app/current"
mv 是原子操作(在同一文件系统内),不会出现文件读写一半的情况。如果 cp -r 替换目录,拷贝过程中应用读到的是新旧混合的文件,直接炸。所以线上替换文件用 mv 准没错。
场景3:文件整理和归档
每天处理报表时,把当天文件按类型分类:
#!/bin/bash
# 按扩展名整理下载目录的文件
DOWNLOAD_DIR="/home/user/downloads"
# 创建分类子目录
mkdir -p "$DOWNLOAD_DIR"/{images,docs,videos,archives,others}
# 用 for 循环配合 mv 批量整理
for file in "$DOWNLOAD_DIR"/*; do
if [ -f "$file" ]; then
case "${file##*.}" in
jpg|jpeg|png|gif|webp)
mv "$file" "$DOWNLOAD_DIR/images/"
;;
pdf|doc|docx|xlsx|txt)
mv "$file" "$DOWNLOAD_DIR/docs/"
;;
mp4|avi|mkv|mov)
mv "$file" "$DOWNLOAD_DIR/videos/"
;;
zip|tar|gz|rar|7z)
mv "$file" "$DOWNLOAD_DIR/archives/"
;;
*)
mv "$file" "$DOWNLOAD_DIR/others/"
;;
esac
fi
done
这个脚本我每周跑一次,下载目录瞬间清爽。如果文件已经被占用(比如正在下载),mv 会失败,case 分支里可以加点判断。
今日作业
你的 /data/logs/ 目录下有大量 .log 文件,需要按月份归档到 /data/archive/ 下。文件的命名格式是 app_20240601.log(年月日+后缀)。
写一行命令(或者一个简短脚本),把 2024 年 6 月的所有日志文件从 /data/logs/ 移动到 /data/archive/202406/ 目录下,要求覆盖前给已存在的目标文件做个备份(带 ~ 后缀),并显示每个文件的移动过程。
(提示:结合通配符和 -v -b 参数。想一想如果目标目录不存在会怎样?)