mv 命令详解与参数实测(Ubuntu 22.04)
GNU mv 参数实测:同目录改名 inode 语义、跨文件系统复制加删除、-i/-n/-f/-b/-t/-T/-u 覆盖与目标控制,Ubuntu 22.04 实测。
mv 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 GNU mv 的常用参数——同目录改名的 inode 语义、跨文件系统移动的「复制+删除」本质、覆盖控制
-i/-n/-f、备份-b、过程显示-v、目标目录-t、目标形态-T与更新-u,全部命令与回显已在 Ubuntu 22.04(GNU coreutils 8.32)实测通过。
一、环境信息
本篇所有命令与回显均来自同一台 Ubuntu 22.04 服务器的实测,演示数据统一放在 /root/demo/mv/ 下,跨文件系统演示使用 /dev/shm(内存盘),读者在任意 Ubuntu 22.04 机器上按相同步骤均可复现。
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- mv 版本:GNU coreutils 8.32(
mv --version实测) - mv 属于 coreutils 基础工具集,Ubuntu 22.04 默认预装,无需额外安装
- 演示目录:
/root/demo/mv/ - 跨文件系统演示目录:
/dev/shm(tmpfs,与根分区属不同文件系统) - 测试数据:
old.txt、cover-dst.txt、子目录dir/
二、命令概览
2.1 语法与测试数据准备
mv 的完整语法共三种形态(摘自 mv --help 的 Usage 行):单个源改名/移动到目标、多个源移动进目录、以及用 -t 显式指定目标目录。职责同样是两个:同文件系统内是纯粹的改名(rename),跨文件系统时退化为复制加删除(3.3 实测)。
💡 技术拓展:"把文件挪个位置/改个名"不止 mv 一种做法,横向对比——
方式 原理/职责 适用场景 mv 同文件系统 rename 原子完成;跨文件系统复制+删除 日常移动与改名,默认首选 cp + rm 复制一份再删源,两步非原子 需要保留源副本时 rename 按 Perl 正则批量改名 一批文件统一改名规则 rsync --remove-source-files 同步后删已传源文件,断点可续 大批量跨机/跨盘迁移 本文主角是 mv:单文件/目录的移动与改名;批量改名与跨机迁移分别交给 rename、rsync 更合适。
先准备一套演示数据:
mkdir -p /root/demo/mv/dir
cat > /root/demo/mv/old.txt <<'EOF'
rename me
EOF
cat > /root/demo/mv/cover-dst.txt <<'EOF'
old content
EOF
验证数据就绪(ls -li 连 inode 一起列出,供改名验证):
$ ls -li /root/demo/mv
total 12
31616868 -rw-r--r-- 1 root root 12 Oct 15 06:35 cover-dst.txt
31616866 drwxr-xr-x 2 root root 4096 Oct 15 06:35 dir
31616867 -rw-r--r-- 1 root root 10 Oct 15 06:35 old.txt
old.txt 的 inode 是 31616867,记下这个值,3.1 验证改名前后 inode 不变。
2.2 --help 全量回显
mv --help 输出全部参数清单(实测原样保留,可作为参数速查):
Usage: mv [OPTION]... [-T] SOURCE DEST
or: mv [OPTION]... SOURCE... DIRECTORY
or: mv [OPTION]... -t DIRECTORY SOURCE...
Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.
Mandatory arguments to long options are mandatory for short options too.
--backup[=CONTROL] make a backup of each existing destination file
-b like --backup but does not accept an argument
-f, --force do not prompt before overwriting
-i, --interactive prompt before overwrite
-n, --no-clobber do not overwrite an existing file
If you specify more than one of -i, -f, -n, only the final one takes effect.
--strip-trailing-slashes remove any trailing slashes from each SOURCE
argument
-S, --suffix=SUFFIX override the usual backup suffix
-t, --target-directory=DIRECTORY move all SOURCE arguments into DIRECTORY
-T, --no-target-directory treat DEST as a normal file
-u, --update move only when the SOURCE file is newer
than the destination file or when the
destination file is missing
-v, --verbose explain what is being done
-Z, --context set SELinux security context of destination
file to default type
--help display this help and exit
--version output version information and exit
The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX.
The version control method may be selected via the --backup option or through
the VERSION_CONTROL environment variable. Here are the values:
none, off never make backups (even if --backup is given)
numbered, t make numbered backups
existing, nil numbered if numbered backups exist, simple otherwise
simple, never always make simple backups
GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Report any translation bugs to <https://translationproject.org/team/>
Full documentation <https://www.gnu.org/software/coreutils/mv>
or available locally via: info '(coreutils) mv invocation'
💡 说明:帮助原文专门用一行强调覆盖参数的优先级——"If you specify more than one of -i, -f, -n, only the final one takes effect",即同时给出多个时只有最后一个生效(3.6 有实测)。备份后缀默认
~,可用-S或SIMPLE_BACKUP_SUFFIX改变。
2.3 版本确认
$ mv --version | head -1
mv (GNU coreutils) 8.32
三、参数逐项实测
本章逐个参数给出「命令 + 实测回显 + 说明」,所有回显与服务器日志逐字一致;演示文件全部位于 /root/demo/mv/。
3.1 默认行为:同目录改名(inode 不变)
同目录下改名是 mv 最轻量的操作——只修改目录项,数据与 inode 都不动。用 ls -i 前后对比:
$ cd /root/demo/mv && ls -i old.txt && mv old.txt new.txt && ls -i new.txt
31616867 old.txt
31616867 new.txt
改名前 old.txt 与改名后 new.txt 的 inode 同为 31616867——磁盘数据一个字节都没搬,只是目录里"名字→inode"的映射改了条目。这也是同目录改名在大文件上依然瞬间完成的原因。
mv 也常用来把文件移动进目录:
$ mv /root/demo/mv/new.txt /root/demo/mv/dir/ && ls -l /root/demo/mv/dir
total 4
-rw-r--r-- 1 root root 10 Oct 15 06:35 new.txt
目标以 / 结尾且是已存在目录时,文件被移动进该目录,名字保持不变。
3.2 默认行为:覆盖已有文件(不提示)
目标已存在时,mv 默认直接覆盖、无任何提示:
$ printf 'new content\n' > /root/demo/mv/cover-src.txt
$ cat /root/demo/mv/cover-dst.txt
old content
$ mv /root/demo/mv/cover-src.txt /root/demo/mv/cover-dst.txt && cat /root/demo/mv/cover-dst.txt
new content
cover-dst.txt 的内容从 old content 变成 new content,旧内容没有备份地消失了。脚本里批量 mv 前务必想清楚覆盖策略:要确认用 -i(3.4),要拒绝用 -n(3.5),要留底用 -b(3.7)。
3.3 跨文件系统移动:本质是复制加删除
同文件系统内的 mv 是 rename;目标在另一个文件系统上时,rename 系统调用无法工作,mv 退化为「先复制数据到目标、成功后删除源」。用根分区与 /dev/shm(内存盘)实测。先确认两处确实是不同文件系统:
$ df /root/demo/mv /dev/shm
Filesystem 1K-blocks Used Available Use% Mounted on
overlay 824057128 463054880 324312812 59% /
shm 65536 0 65536 0% /dev/shm
/root/demo/mv 在 overlay(根文件系统),/dev/shm 在 shm(tmpfs)。移动前记录 dev 与 inode:
$ ls -i /root/demo/mv/cross.txt && stat -c 'dev=%d inode=%i mount=%m name=%n' /root/demo/mv/cross.txt
31616884 /root/demo/mv/cross.txt
dev=1048633 inode=31616884 mount=/ name=/root/demo/mv/cross.txt
$ mv -v /root/demo/mv/cross.txt /dev/shm/cross.txt
copied '/root/demo/mv/cross.txt' -> '/dev/shm/cross.txt'
removed '/root/demo/mv/cross.txt'
$ stat -c 'dev=%d inode=%i mount=%m name=%n' /dev/shm/cross.txt
dev=1048793 inode=3 mount=/dev/shm name=/dev/shm/cross.txt
-v 的输出直接暴露了两步动作:copied 加 removed——对用户是"移动",对系统是复制加删除。移动后 dev 从 1048633 变为 1048793、inode 从 31616884 变为 3,数据被真正重新写入了一份。内容完好:
$ cat /dev/shm/cross.txt
cross
💡 说明:跨文件系统 mv 有两个衍生结论——其一,中途失败(磁盘满/权限不足)可能出现"目标有了一部分、源还在"的中间态,大批量迁移应改用 rsync 可续传;其二,硬链接跨文件系统必然失效(3.1 的 inode 不变结论只在同一文件系统内成立)。
实测完把文件移回并清理:
$ mv -v /dev/shm/cross.txt /root/demo/mv/cross.txt && rm /root/demo/mv/cross.txt
copied '/dev/shm/cross.txt' -> '/root/demo/mv/cross.txt'
removed '/dev/shm/cross.txt'
推论:跨文件系统移动大文件耗时与文件大小成正比(要复制数据),且中途断电可能出现"目标已建、源未删"的中间态;移动巨大目录树更推荐同盘内 rename 或先打包再传。
3.4 -i:覆盖已有文件前交互确认
-i(--interactive)在目标已存在时先询问。回答 n 跳过且退出码 0:
$ printf 'a\n' > /root/demo/mv/i-a.txt && printf 'b\n' > /root/demo/mv/i-b.txt
$ printf 'n\n' | mv -i /root/demo/mv/i-a.txt /root/demo/mv/i-b.txt; echo exit=$?
mv: overwrite '/root/demo/mv/i-b.txt'? exit=0
$ cat /root/demo/mv/i-a.txt /root/demo/mv/i-b.txt
a
b
回答 n 后两个文件都还在、内容未动。回答 y 则执行覆盖,源文件消失:
$ printf 'y\n' | mv -i /root/demo/mv/i-a.txt /root/demo/mv/i-b.txt
mv: overwrite '/root/demo/mv/i-b.txt'?
$ ls /root/demo/mv | grep i-
i-b.txt
只剩 i-b.txt,i-a.txt 已被移走。交互确认适合人工整理文件时防手滑。
3.5 -n:不覆盖已有文件
-n(--no-clobber)遇到已存在的目标直接跳过,无输出、退出码 0:
$ printf 'a2\n' > /root/demo/mv/n-a.txt && printf 'b2\n' > /root/demo/mv/n-b.txt
$ mv -n /root/demo/mv/n-a.txt /root/demo/mv/n-b.txt; echo exit=$?
exit=0
$ ls /root/demo/mv | grep n-
n-a.txt
n-b.txt
$ cat /root/demo/mv/n-b.txt
b2
源 n-a.txt 没被移走、目标 n-b.txt 内容没被覆盖(仍是 b2)。与 cp 的 -n 一样是静默跳过,脚本里无法靠退出码区分"移动成功"与"因存在而跳过"。
3.6 -f:覆盖且不提示(多选项时最后一个生效)
-f(--force)覆盖目标且从不询问。单独看它与非交互的默认行为一致,真正的用法是与 -i 组合时体现"最后一个生效"的规则——实测 -i -f 连用,无任何提示直接覆盖:
$ printf 'a3\n' > /root/demo/mv/f-a.txt && printf 'b3\n' > /root/demo/mv/f-b.txt
$ mv -i -f /root/demo/mv/f-a.txt /root/demo/mv/f-b.txt && ls /root/demo/mv | grep f-
f-b.txt
整条命令没有出现 overwrite 询问(对比 3.4 中 -i 单独使用时的提示),f-a.txt 直接覆盖为 f-b.txt。帮助原文 "only the final one takes effect" 得到验证:把 -f 写在后面,-i 就失效了;反之 -f -i 则会询问。
3.7 -b:覆盖前为已有目标生成备份
-b(--backup 无参形态)在覆盖前把旧目标改名为 原名~ 留底:
$ printf 'a4\n' > /root/demo/mv/b-a.txt && printf 'b4\n' > /root/demo/mv/b-b.txt
$ mv -b /root/demo/mv/b-a.txt /root/demo/mv/b-b.txt && ls -l /root/demo/mv | grep b-
-rw-r--r-- 1 root root 3 Oct 15 06:35 b-b.txt
-rw-r--r-- 1 root root 3 Oct 15 06:35 b-b.txt~
b-b.txt 是新内容,旧文件完整保留为 b-b.txt~。"移动即覆盖"的高危操作加 -b 后多一道保险,更新配置文件时推荐。
3.8 -v:显示移动过程
-v(--verbose)逐条打印移动动作,同文件系统显示 renamed:
$ printf 'm1\n' > /root/demo/mv/v1.txt && printf 'm2\n' > /root/demo/mv/v2.txt
$ mv -v /root/demo/mv/v1.txt /root/demo/mv/v2.txt /root/demo/mv/dir/
renamed '/root/demo/mv/v1.txt' -> '/root/demo/mv/dir/v1.txt'
renamed '/root/demo/mv/v2.txt' -> '/root/demo/mv/dir/v2.txt'
对比 3.3:同文件系统是 renamed(纯改名),跨文件系统是 copied 加 removed(复制加删除)——-v 的动词本身就区分了两种语义。
3.9 -t:显式指定目标目录
-t(--target-directory)把目标目录提到参数前面,多文件移动时更好组装:
$ printf 't1\n' > /root/demo/mv/t-target.txt
$ mv -t /root/demo/mv/dir /root/demo/mv/t-target.txt && ls /root/demo/mv/dir
new.txt
t-target.txt
v1.txt
v2.txt
t-target.txt 与前面的 v1、v2 都进了 dir 目录。mv -t 目录 源1 源2 形态下源可以列任意多个。
3.10 -u:仅当源比目标新或目标缺失时移动
-u(--update)实现"只更新"语义。目标缺失或比源旧时移动;目标比源新时跳过:
$ printf 'u-src\n' > /root/demo/mv/u-src.txt && printf 'u-old\n' > /root/demo/mv/u-dst.txt && touch -d '2025-01-01 08:00:00' /root/demo/mv/u-dst.txt
$ mv -u /root/demo/mv/u-src.txt /root/demo/mv/u-dst.txt && cat /root/demo/mv/u-dst.txt
u-src
目标 u-dst.txt 的 mtime 是 2025-01-01、比源旧,移动执行(内容变成 u-src)。反过来,目标较新时跳过,两个文件都保留:
$ printf 'u-x\n' > /root/demo/mv/u-dst2.txt && touch -d '2026-10-15 23:59:00' /root/demo/mv/u-dst2.txt && printf 'u-new\n' > /root/demo/mv/u-src2.txt
$ mv -u /root/demo/mv/u-src2.txt /root/demo/mv/u-dst2.txt; echo exit=$?
exit=0
$ ls /root/demo/mv | grep u-
u-dst.txt
u-dst2.txt
u-src2.txt
u-dst2.txt 的 mtime 被设为 2026-10-15 23:59,比刚创建的 u-src2.txt 新,mv -u 静默跳过(退出码 0):u-src2.txt 没被移走、u-dst2.txt 内容未动。适合"用新版本替换旧版本"的归集场景。
3.11 -T:目标不作为目录处理
目标是已存在目录时,mv 文件 目录 的语义是"移进目录";-T(--no-target-directory)强制把目标当普通文件处理,于是"文件覆盖目录"被判定为非法:
$ mkdir -p /root/demo/mv/edir
$ printf 'nf\n' > /root/demo/mv/nfile.txt
$ mv /root/demo/mv/nfile.txt /root/demo/mv/edir && ls -l /root/demo/mv/edir
total 4
-rw-r--r-- 1 root root 3 Oct 15 06:35 nfile.txt
$ printf 'nf2\n' > /root/demo/mv/nfile2.txt
$ mv -T /root/demo/mv/nfile2.txt /root/demo/mv/edir; echo exit=$?
mv: cannot overwrite directory '/root/demo/mv/edir' with non-directory
exit=1
不带 -T 时 nfile.txt 被移进 edir;带 -T 时 mv 把 edir 当作要被覆盖的目标名,文件不能覆盖目录,报错退出码 1。脚本里希望"目标必须是文件名、绝不允许溜进目录"时加 -T 能立刻暴露错误。
四、实战示例
4.1 更新配置文件时自动留底
用 -b 覆盖旧配置并保留一份 ~ 备份:
$ mv -b /root/demo/mv/b-a.txt /root/demo/mv/b-b.txt && ls -l /root/demo/mv | grep b-
-rw-r--r-- 1 root root 3 Oct 15 06:35 b-b.txt
-rw-r--r-- 1 root root 3 Oct 15 06:35 b-b.txt~
覆盖完成后 b-b.txt~ 就是上一版内容,回滚只需把它改回原名。
4.2 批量归集文件到指定目录
-t 让"目标在前、源随便列":
$ mv -t /root/demo/mv/dir /root/demo/mv/t-target.txt && ls /root/demo/mv/dir
new.txt
t-target.txt
v1.txt
v2.txt
配合 ls | grep 过滤结果可以批量归集同类文件;配合 -v 可逐条核对去向(见 3.8)。
4.3 只让新版本替换旧版本
-u 保证目标较新时不被旧文件覆盖:
$ mv -u /root/demo/mv/u-src.txt /root/demo/mv/u-dst.txt && cat /root/demo/mv/u-dst.txt
u-src
第一个示例中源较新、替换成功;目标较新的情形见 3.10 的第二个实测(跳过且两个文件都在)。
五、注意事项与常见问题
Q1:同目录 mv 改名会搬动数据吗?
不会。只是修改目录项里的名字映射,inode 不变(见 3.1 的 ls -i 实测),大文件也一样瞬间完成。
Q2:为什么 mv 跨文件系统时 inode 变了、速度还慢?
跨文件系统无法 rename,mv 退化为复制加删除(-v 显示 copied 加 removed,见 3.3),数据被重新写了一份,耗时与文件大小成正比。
Q3:-i、-f、-n 同时写在一条命令里谁生效?
只有最后一个生效(帮助原文明确,见 2.2);实测 -i -f 无提示直接覆盖(3.6)。写脚本时只保留一个覆盖策略参数。
Q4:mv 覆盖已有文件有提示吗?
没有,默认直接覆盖(见 3.2)。人工操作建议加 -i,脚本建议 -n(拒绝覆盖)或 -b(覆盖前留底)。
Q5:mv -i 回答 n 之后退出码是多少?
0。拒绝覆盖不视为错误,命令静默跳过(见 3.4 的 exit=0 实测)。
Q6:为什么 mv 文件到已存在的目录时,文件"钻进"目录里了?
目标已是目录时 mv 的语义就是移进该目录。若这不符合预期,用 -T 把目标当普通文件处理,会直接报错而不是进目录(见 3.11 的对照实测)。
Q7:mv -u 适合什么场景?
"仅当源比目标新(或目标不存在)才移动"的归集与同步,比如把新版本文件推到发布目录而不冲掉更新的目标(见 3.10 的正反两个实测)。
Q8:mv 移动过程中断电会丢数据吗? 同文件系统 rename 是原子操作,要么完成要么没发生;跨文件系统是复制加删除,中断时可能出现目标已建、源未删的中间态,重跑 mv 即可补完(见 3.3 的语义分析)。
六、参考链接
- GNU coreutils 官方手册(mv 章节):https://www.gnu.org/software/coreutils/manual/html_node/mv-invocation.html
- 在线文档:https://www.gnu.org/software/coreutils/mv
- 本机帮助:
mv --help、man mv(Ubuntu 22.04 内置) - 关联手册页:
man 2 rename(同文件系统改名系统调用)、man cp(跨文件系统复制语义)