cp
cp(copy)是 GNU coreutils 中选项最丰富的命令。表面只是"复制粘贴",内里却藏着符号链接跟随策略、稀疏文件处理、写时复制克隆、尾部斜杠行为分歧、以及 POSIX 与 GNU 之间的微妙差异——每一个坑都足以让一个看似无害的备份脚本悄悄出错。
速查卡片
| 需求 | 命令 |
|---|---|
| 复制文件 | cp src dst |
| 复制到目录 | cp file1 file2 /target/dir/ |
| 递归复制目录 | cp -r src_dir/ dst_dir/ |
| 归档复制(保留全部元数据) | cp -a src/ dst/ |
| 覆盖前确认 | cp -i src dst |
| 不覆盖已存在文件 | cp -n src dst |
| 仅源更新时才复制 | cp -u src dst |
| 覆盖前备份旧文件 | cp -b src dst(旧文件 → dst~) |
| 详细输出 | cp -v src dst |
| 复制符号链接本身(不跟随) | cp -P link dst 或 cp -a |
| 创建硬链接而非复制 | cp -l src dst |
| 创建符号链接而非复制 | cp -s src dst |
| CoW 轻量复制(Btrfs/XFS) | cp --reflink=always src dst |
| 保留源路径层级 | cp --parents a/b/c.txt /tmp/ |
概述
cp 是文件系统操作的核心命令,属于 GNU coreutils。POSIX 定义了基础行为(-R/-i/-f/-p),GNU 在此基础上扩展了大量选项——从 --reflink 写时复制到 --sparse 稀疏文件处理。理解这些选项之间的交互是正确使用 cp 的关键。
命令签名
bash
# 形式一:单文件复制
cp [OPTION]... [-T] SOURCE DEST
# 形式二:多文件复制到目录
cp [OPTION]... SOURCE... DIRECTORY
# 形式三:-t 指定目标目录(GNU 扩展)
cp [OPTION]... -t DIRECTORY SOURCE...-T:强制将 DEST 视为普通文件(即使它是已存在目录也不把源复制进去)-t:将目标目录放到前面,适合配合find ... -exec或xargs批量操作
基础用法
示例一:最简——单文件复制
bash
$ cp file.txt /tmp/file.txt
$ cp file.txt /tmp/ # 目标为目录时自动放入,保留原文件名
$ cp file.txt /tmp/newname.txt # 同时改名示例二:多文件复制 + 递归目录
bash
$ cp a.txt b.txt c.txt /tmp/ # 多文件复制到目录
$ cp -r myproject/ /backup/ # 递归复制整个目录树
$ cp -r dir1 dir2 dir3 /dest/ # 多个目录一起复制示例三:归档模式——最常用的复制方式
bash
$ cp -a /etc/nginx /backup/-a 等价于 -dR --preserve=all,展开即:
| 子选项 | 含义 |
|---|---|
-d | 不跟随符号链接 + 保留链接关系 |
-R | 递归复制目录 |
--preserve=mode | 保留权限位和 ACL |
--preserve=ownership | 保留所有者和属组(需 root 权限) |
--preserve=timestamps | 保留修改时间和访问时间 |
--preserve=links | 保留硬链接关系 |
--preserve=context | 保留 SELinux 安全上下文 |
--preserve=xattr | 保留扩展属性 |
示例四:尾部斜杠——最隐蔽的坑
bash
$ tree src/
src/
├── a.txt
└── sub/
└── b.txt
# /tmp/dst 不存在 → 创建 dst,内容为 src 的副本
$ cp -r src /tmp/dst
$ tree /tmp/dst/
/tmp/dst/
├── a.txt
└── sub/
└── b.txt
# /tmp/dst 已存在 → 复制 src **本身**到 dst 内
$ cp -r src /tmp/dst # → /tmp/dst/src/a.txt ...
# 尾部 / → 始终保持"复制内容"语义
$ cp -r src/ /tmp/dst # → /tmp/dst/a.txt ...(无论 dst 是否已存在)核心规则:
cp -r src /tmp/dst的行为取决于/tmp/dst是否存在——不存在则创建为副本,存在则将 src 放入其中。而cp -r src/ /tmp/dst始终复制内容。脚本中务必显式明确意图。
选项详解
覆盖控制
| 选项 | 行为 |
|---|---|
| (默认) | 静默覆盖,不提示 |
-i / --interactive | 覆盖前提示确认(y/n) |
-n / --no-clobber | 静默跳过已存在文件(已废弃,建议用 --update=none) |
-u / --update | 仅当源比目标新、或目标不存在时才复制 |
--update=none | 不覆盖,静默跳过 |
--update=none-fail | 不覆盖,跳过时输出诊断并返回非零退出码 |
--update=older | -u 的显式等价写法 |
-b / --backup | 覆盖前备份旧文件,后缀默认 ~ |
-f / --force | 目标不可打开时先 unlink() 再重试 |
--remove-destination | 打开目标之前先删除(比 -f 更激进) |
⚠️ POSIX
-f仅"静默覆盖不提示";GNU-f额外增加了"失败时删除重试"的逻辑。如果你在同时兼容 BSD 的脚本里用-f,小心行为差异。
-f vs --remove-destination 的区别
bash
# touch 创建只读文件
$ touch /tmp/readonly && chmod 444 /tmp/readonly
$ cp file.txt /tmp/readonly -f
# → 打开失败后 unlink 重试:成功
$ cp file.txt /tmp/readonly --remove-destination
# → 先 unlink 再创建:也成功,但语义不同
# 关键差异:目标为符号链接时
$ ln -s /important/data /tmp/link
$ cp -f file.txt /tmp/link # → 覆盖 /important/data(跟随链接)
$ cp --remove-destination file.txt /tmp/link # → 删除链接本身,创建新文件符号链接处理
GNU cp 提供三级符号链接控制 + 两种"不复制数据"模式:
| 选项 | 对符号链接的行为 |
|---|---|
-P / --no-dereference | 不跟随——复制链接本身(默认) |
-L / --dereference | 始终跟随——复制链接指向的文件内容 |
-H | 仅命令行参数中跟随——递归时子目录内的链接仍不跟随 |
-l / --link | 不复制数据——创建指向相同 inode 的硬链接 |
-s / --symbolic-link | 不复制数据——创建符号链接 |
bash
$ ln -s /etc/hostname link_to_hostname
$ cp -P link_to_hostname /tmp/ # 复制链接本身
$ ls -l /tmp/link_to_hostname
lrwxrwxrwx ... /tmp/link_to_hostname -> /etc/hostname
$ cp -L link_to_hostname /tmp/ # 复制目标文件内容
$ cat /tmp/link_to_hostname
myhost
$ cp -a dir/ /backup/ # 归档模式:不跟随(= -P + preserve links)高级选项
--preserve —— 精细控制元数据
bash
--preserve=mode # 权限位(含 ACL)
--preserve=ownership # 用户和组(非 root 时组可能丢失)
--preserve=timestamps # 修改/访问时间
--preserve=links # 硬链接关系
--preserve=context # SELinux 安全上下文
--preserve=xattr # 扩展属性
--preserve=all # 以上全部(= -a 的 --preserve 部分)
# 选择性保留
cp --preserve=mode,timestamps src dst--reflink —— 写时复制(CoW)
在 Btrfs、XFS、OCFS2 等现代文件系统上,--reflink 创建共享数据块的克隆——不占用额外空间,修改时按需复制:
bash
cp --reflink=always huge_file.iso snapshot.iso # 秒级完成,零额外空间
cp --reflink=auto huge_file.iso snapshot.iso # 文件系统不支持时回退到普通复制
cp --reflink=never huge_file.iso snapshot.iso # 强制普通复制--sparse —— 稀疏文件处理
稀疏文件包含大量连续的零字节空洞,--sparse 控制如何处理它们:
bash
cp --sparse=auto disk.img /tmp/ # 启发式检测,遇洞留洞(默认)
cp --sparse=always disk.img /tmp/ # 任何足够长的零序列都做成洞
cp --sparse=never disk.img /tmp/ # 逐字节写入,洞被填实--parents —— 保留路径层级
bash
$ cp --parents src/a/b/c.txt /tmp/
$ tree /tmp/
/tmp/
└── src/
└── a/
└── b/
└── c.txt # 自动创建中间目录-x / --one-file-system
bash
cp -ax /home /backup/home # 不跨越文件系统边界
# /home/ 下挂载的其他分区内容不会被复制覆盖控制选项的相互覆盖规则
多个覆盖相关选项同时出现时,后出现的覆盖前面的:
bash
cp -i -n src dst # -n 覆盖 -i,静默跳过
cp -n -i src dst # -i 覆盖 -n,覆盖前提示
cp -f -n src dst # -n 覆盖 -f,-f 被忽略
cp -u -n src dst # -n 覆盖 -u常用命令组合
bash
# 批量复制 + 进度监控
find . -name '*.txt' -print0 | xargs -0 cp -t /dest/ -v
# 仅复制变化的文件(增量式备份)
cp -auv /source/ /backup/
# 完整备份(保留一切)
cp -a /etc /backup/etc-$(date +%Y%m%d)
# 安全复制(不覆盖已有)
cp -n /source/*.conf /etc/
# CoW 快照(btrfs/xfs)
cp --reflink=always /data/vm.qcow2 /data/snapshot.qcow2
# 保留路径层级提取
find src -name '*.h' -exec cp --parents {} /tmp/headers/ \;脚本中的 cp
安全复制原则
bash
# ❌ 危险:覆盖不提示
cp /tmp/build/output.bin /deploy/
# ✅ 安全:仅源更新时复制
cp -u /tmp/build/output.bin /deploy/
# ✅ 安全:不覆盖已有配置
cp -n /tmp/new.conf /etc/app/config.conf
# ✅ 安全:覆盖前备份
cp -b /tmp/new.conf /etc/app/config.conf # 旧文件 → config.conf~常见脚本模式
bash
# 精确控制尾部斜杠——始终显式指定目标文件名或目录
cp -r src/ /backup/src/ # 明确:复制内容到 /backup/src/
cp -r src /backup/ # 明确:复制 src 目录本身到 /backup/
# 批量复制优先用 -t(目标在前)
find . -type f -name '*.log' -exec cp -t /archive/logs/ {} +
# 条件复制:仅目标不存在时
[ -f /etc/app.conf ] || cp /opt/default.conf /etc/app.conf
# 复制后验证(关键数据)
cp important.dat /backup/ && sha256sum -c <<< "$(sha256sum important.dat)"常见报错
| 错误信息 | 原因 | 解决 |
|---|---|---|
cp: omitting directory 'src/' | 复制目录没加 -r | 加 -r 或 -a |
cp: 'src' and 'dst' are the same file | 源和目标是同一文件(如 cp file .) | 检查路径,确认目标不同于源 |
cp: will not overwrite just-created | 同一次批量复制中尝试用同名文件覆盖刚创建的文件 | 源中有重名文件,用 rsync 或检查源结构 |
cp: cannot create regular file: Permission denied | 目标目录无写权限,或文件存在且只读 | chmod 或使用 --remove-destination |
cp: not writing through dangling symlink | 目标是悬挂的符号链接 | 删除悬挂链接或指向有效路径 |
cp: cannot stat 'file': No such file or directory | 源文件不存在 | 检查路径拼写 |
cp: failed to preserve ownership | 非 root 用户尝试保留文件所有者 | 去掉 --preserve=ownership 或用 sudo |
注意事项
cp不保证原子性——复制大文件时如果中断,目标是不完整文件。mv(同文件系统内)是原子的- 默认不保留 ACL 和 xattr——仅
-p不够,需--preserve=xattr或直接用-a cp默认跟随符号链接 指的是目标操作,但-P(默认)控制的是源——两个维度不要混淆- 默认不保留稀疏文件的"空洞"特性——除非源文件被成功检测为稀疏文件(
--sparse=auto的启发式算法),空洞可能在复制过程中"实化" --backup的备份文件可能累积——多次cp -b会创建file~、file.~1~等;需定期清理或使用--backup=numbered
常见误区
| ❌ 误区 | ✅ 正解 |
|---|---|
"cp -r 和 cp -R 有区别" | GNU cp 中完全一致;POSIX 中 -r 行为由实现定义,-R 有明确规定——习惯用 -R |
"cp -f 就是强制覆盖" | POSIX 下仅静默覆盖;GNU 下额外增加 unlink 重试逻辑;最激进的选项是 --remove-destination |
"cp -a src/ dst/ 和 cp -a src dst/ 一样" | 尾部斜杠决定复制的是内容还是目录本身(见示例四) |
"cp 保留所有文件元数据" | 默认不保留时间戳和权限;-p 保留部分;完整保留需 -a 或 --preserve=all |
| "源和目标同名自动跳过" | 默认静默覆盖——需显式加 -n 或 -i |
"cp -l 复制了数据" | -l 仅创建硬链接,不复制任何数据块 |
cp vs rsync 场景选择
| 场景 | 推荐命令 | 原因 |
|---|---|---|
| 本地简单复制 | cp -a | 简单直接 |
| 远程复制 | rsync -avz | cp 不支持远程 |
| 增量备份 | rsync -av --delete | cp -u 无法处理删除/差异传输 |
| CoW 快照 | cp --reflink=always | rsync 不理解 CoW |
| 需要排除模式 | rsync --exclude | cp 无原生排除 |
| 大文件断点续传 | rsync --partial | cp 中断后需从头来 |
find + 批量复制 | cp -t + xargs | 管道友好 |
版本与规范信息
| 事件 | 说明 |
|---|---|
| POSIX 纳入 | Issue 2(基础 cp) |
| GNU coreutils 当前 | 9.11(2026-04) |
| CoW/reflink 支持 | Linux 内核 4.5+ / Btrfs、XFS、OCFS2 |
-n 状态 | 已废弃(deprecated),建议用 --update=none |
macOS 特别说明
- macOS 的
cp来自 BSD,选项集远小于 GNU 版本 - BSD
cp没有-a、--reflink、--parents、-t、-T等 GNU 扩展 - BSD
cp -r行为可能不同于 GNUcp -r(不处理特殊文件) - BSD
cp对尾部斜杠的处理与 GNU 可能不同 - macOS 文件系统(APFS)支持 CoW 克隆,但需使用
cp -c(BSD 标志,非--reflink)
来源
| 来源 | 链接 |
|---|---|
| GNU coreutils cp man page | https://man7.org/linux/man-pages/man1/cp.1.html |
| GNU coreutils 官方文档 | https://www.gnu.org/software/coreutils/cp |
| POSIX cp 规范 | https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cp.html |
| Linuxize cp 实战指南 | https://linuxize.com/post/cp-command-in-linux/ |
| Baeldung: cp 警告排查 | https://www.baeldung.com/linux/find-cp-avoid-warnings |
| 整理时间 | 2026-07-14 |
