cp 没有事务,也没有通用的预演模式。一条看似简单的目录复制命令,可能因为目标是否已存在、shell 是否展开通配符、实现来自 GNU 还是 BSD/macOS,以及符号链接或挂载点而得到不同结果。本指南先确定复制意图,再用新暂存目录、验证和可回滚切换降低风险。
本文维护层于 2026 年重写。2011 年原文很短,并含有关于
-r与报错原因的不准确结论;它已完整保存在文末的惰性归档中,不作为当前操作建议。
Table of Contents
先确认工具和目标
不要把“Linux 上见过的行为”当成所有 Unix 的规则。先在要执行复制的那台机器上记录实现和手册:
command -V cp
cp --version
man cp
uname -a
cp --version 是 GNU 风格;BSD/macOS 可能只会报告不支持该选项,因此以本机 man cp 为准。开始前还要写清:复制目录本身还是目录内容、目标必须不存在还是允许合并、是否允许覆盖、是否跨文件系统、是否保留链接和元数据、谁批准最终切换。
目录本身、目录内容与末尾斜杠
以下路径只是语义示例,不应直接粘贴到有数据的目录:
cp -R -- source new-copy
mkdir -p -- existing
cp -R -- source existing/
cp -R -- source/. existing/
目标存在与否
| 意图 | 典型结果 | 边界 |
|---|---|---|
source 复制到不存在的 new-copy |
创建 new-copy,其内容对应 source |
父目录必须存在并可写 |
source 复制到已存在的 existing/ |
通常创建或合并 existing/source |
可能覆盖同名内容,不是干净克隆 |
source/. 复制到已存在的 existing/ |
把内容合并到 existing,包括点文件 |
目标应当是新建的空暂存目录 |
同一文件、硬链接别名、指向同处的符号链接,以及把目录复制到自己的后代目录,都应视为停止条件。实现通常会识别一部分同文件或自复制情形,但不能把该检测当成完整的安全边界。
cp 不是 rsync
末尾 / 没有一个可跨实现套用的 cp 口诀:
- GNU
cp通常不会仅因普通目录源末尾多了/就切换成“只复制内容”;但末尾斜杠要求路径解析为目录,也可能改变命令行符号链接的解析。GNU 还提供--strip-trailing-slashes。 - 当前 FreeBSD 和 Apple 开源
cp手册明确规定:配合-R时,源以/结尾表示复制目录内容,而不是目录本身。 rsync则明确把source与source/区分为“包含目录名”与“目录内容”。不要把这条rsync规则反推给 GNUcp。
当脚本需要表达“复制内容并包括点文件”时,source/. 比 source/* 更明确;仍应在目标系统的可丢弃目录中验证。
通配符由 shell 展开
在 cp -R source/ staging/ 中, 在 cp 启动前由 shell 展开。Bash 默认不匹配 .env 等点文件;无匹配项时,默认会把字面模式留给命令。nullglob 会删除无匹配模式,failglob 会中止该命令,dotglob 会让 * 包含点文件,但仍排除 . 和 ..。
如果确实需要 Bash 通配符,可以把选项限制在子 shell 中,避免污染当前会话:
(
shopt -s dotglob failglob
cp -R -- source/* staging/
)
这仍然不如 source/. 简单。引号也要放对:"source data"/ 会保护空格,同时保留 的展开;"source data/*" 会把星号变成普通字符。-- 表示选项结束,可避免名为 -report 的源被当成选项;在脚本中也可用 ./-report。先确认目标实现支持所用语法。
选择递归、属性和链接策略
| 选项 | 含义与限制 |
|---|---|
-R |
POSIX 的递归目录复制形式,跨系统脚本优先使用 |
-r |
GNU 目前把它视为 -R;FreeBSD/macOS 的历史兼容行为可能隐含跟随链接,手册也不鼓励依赖它 |
-p |
尽权限允许保留一组基本元数据;具体集合和失败报告因实现而异 |
-a |
GNU 中等于递归、保留链接并请求保留全部属性;BSD/macOS 的展开和属性范围不同,不是完全相同的合同 |
-H / -L / -P |
分别跟随命令行链接、跟随所有链接、不跟随链接;与递归复制一起显式选择 |
-T / -t |
GNU 的“目标不是目录”与“显式目标目录”扩展,不属于 POSIX,也不应假设 BSD/macOS 支持 |
-x |
GNU、FreeBSD 和当前 macOS 可用于不穿越文件系统边界,但不是 POSIX;被跳过的挂载内容必须记录 |
GNU 可用 -T 消除“目标已经存在时多套一层目录”的歧义,也可用 -t 明确多个源的目标目录:
cp -aT -- source new-copy
cp -a -t staging -- source-a source-b
这两条是 GNU 专用写法。即使使用 -T,也不要把现有生产目录当作 new-copy;合并和覆盖仍然可能发生。
符号链接和特殊文件
先决定是保留链接本身还是复制链接指向的内容。对不可信目录使用 -L 可能把树外文件带入复制,也可能遇到循环。设备节点、FIFO、套接字和不断变化的文件需要应用一致性方案;不要以管理员身份盲目复制,也不要用 --copy-contents 读取设备内容。
硬链接也不能只凭 -a 名称推断。GNU -a 请求保留链接关系;当前 FreeBSD/macOS cp -R 手册说明硬链接可能变成独立文件。需要硬链接一致性时,先用目标平台支持并验证过的归档或同步工具。
属性、ACL、扩展属性和稀疏文件
内容相同不代表副本等价。权限、所有者、时间戳、ACL、扩展属性、安全标签、资源叉、文件标志、硬链接和稀疏洞都可能受权限与文件系统能力限制。GNU -a 是 --preserve=all 的便捷形式,但仍要检查诊断和退出码;BSD/macOS -a 的定义不同。
GNU cp 默认尝试识别稀疏文件,另有 --sparse=auto|always|never;macOS 有不同的稀疏文件选项。跨文件系统复制后,逻辑大小可能相同而实际占用块数不同。不要只比较文件大小。
文件系统边界
递归树可能包含独立挂载点、绑定挂载、网络卷、容器挂载或云盘。默认穿越它们可能复制远超预期的数据;使用受支持的 -x 又会有意漏掉子挂载。先清单化每个边界、空间配额、大小写规则、最大文件限制和允许保留的属性。暂存目录应位于最终目标同一文件系统,才可能用同文件系统重命名完成切换。
无损预检
cp 没有通用 --dry-run。先做只读清单;输出文件名和扩展属性可能含敏感信息,只保留在授权位置,分享前脱敏:
find source -mindepth 1 -maxdepth 2 -print
du -sk source
findmnt -R --target source
find source -xdev -print
前两条较常见;findmnt 和这里的 find -xdev 用法应按本机手册确认。还要检查源在复制期间是否会变化、目标可用空间和 inode、读写权限、挂载状态,以及路径经过物理解析后是否重叠。不要解析普通换行分隔的 find 输出来驱动批量删除或覆盖。
一次可丢弃的演练
下面的演练只写入由 mktemp 创建的 /tmp/cp-guide.*,并在删除前检查路径前缀。它在本工作站的 GNU coreutils 9.4 与 Bash 5.2 上验证过;其他系统先阅读本机手册,truncate 尤其不是 POSIX 工具。
work=$(mktemp -d /tmp/cp-guide.XXXXXX)
mkdir -p -- "$work/source/sub" "$work/existing"
printf visible > "$work/source/visible"
printf hidden > "$work/source/.hidden"
ln -s -- visible "$work/source/link"
truncate -s 16M "$work/source/sparse.bin"
cp -R -P -- "$work/source" "$work/clone"
cp -R -P -- "$work/source"/. "$work/existing"/
find "$work" -maxdepth 3 -print
du -h "$work/source/sparse.bin" "$work/existing/sparse.bin"
确认输出后,只清理刚才记录的临时路径:
case "$work" in
/tmp/cp-guide.*) find "$work" -depth -delete ;;
*) printf 'Refusing unexpected path' >&2; exit 1 ;;
esac
不要把 /tmp 演练直接改成生产路径。演练验证的是当前工具的语义,不验证生产数据的一致性、权限或容量。
分阶段复制、验证和回滚
复制到新的暂存目录
在备份或快照可用、写入方已按应用要求暂停后,于最终目标的父目录创建全新的暂存目录。下面是 GNU 示例,路径是占位符:
stage=$(mktemp -d -- /srv/import/.cp-stage.XXXXXX)
cp -a -- /srv/source/. "$stage"/
检查 cp 的退出状态和全部诊断。任何 I/O、权限、空间、属性或源变更错误都应停止流程;不要在错误后把部分副本投入使用。
验证
先比较结构和内容,再按业务要求比较元数据。以下第一组是 GNU/Linux 示例,工具可能需要另行安装:
diff -qr -- /srv/source "$stage"
getfacl -R -p /srv/source
getfacl -R -p "$stage"
getfattr -R -d -m - /srv/source
getfattr -R -d -m - "$stage"
stat --format='%n size=%s blocks=%b' /srv/source/sparse.bin "$stage/sparse.bin"
macOS 可从内容比较与本机属性显示开始:
diff -qr /srv/source "$stage"
ls -le@ /srv/source "$stage"
diff -qr 不证明所有元数据相同;ACL、扩展属性、链接关系、稀疏占用和应用级一致性必须分别验收。大型或受监管数据可增加经过批准的清单与校验和,但不要把含敏感路径的报告发到公开渠道。
切换与回滚
只有最终路径不存在、暂存目录与其在同一文件系统、验证通过且写入方仍处于受控状态时,才考虑重命名切换:
test ! -e /srv/import/release
mv -- "$stage" /srv/import/release
这不是对正在变化的数据做事务快照。切换前记录旧版本和恢复点;切换后做只读与应用验收。在新位置尚未接收写入时,可按预先批准的方案把名称切回;一旦两边都可能有新写入,应停止并使用应用级恢复流程,不要用第二次 cp 猜测合并。不要把删除旧目录当作回滚。
停止条件
- 源仍在变化,却没有应用一致性、快照或停写方案。
- 目标存在且覆盖、合并或名称冲突策略未获批准。
- 发现路径重叠、自复制、意外符号链接、挂载点或特殊文件。
- 可用空间、inode、权限、ACL、扩展属性或稀疏能力不满足要求。
cp、验证工具或文件系统报告任何无法解释的错误。- 需要提升到 root 才能“让它工作”,但权限模型与审计尚未设计。
此时应保留日志和未投入使用的暂存副本,修正计划后重新演练,而不是加入 -f、-L 或管理员权限强行继续。
参考资料
- GNU Coreutils:`cp` 调用
- GNU Coreutils:末尾斜杠
- GNU Bash:文件名展开
- POSIX.1-2024:`cp`
- FreeBSD `cp(1)` 手册
- Apple 开源 `cp(1)` 手册源文件
- rsync 官方手册
历史原文归档
以下是 source_export 中 2011 年可见正文的完整惰性归档。未作内容删改,也没有需要规范化的行尾空白;其中旧站内链接和技术结论仅为历史记录,不是维护层建议。
终于把cp的各种情况做了个总结。文件夹就是一种特殊的文件,但还是有一些不同的地方。
注:[^]表示空格
假设/a目录下有文件1、2、3
Table of Contents
Toggle
- [cp^-R^/a/*^/b](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rab)
- [cp^-R^/a^/b/c](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rabc)
- [如果/b/c不存在](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#%E5%A6%82%E6%9E%9Cbc%E4%B8%8D%E5%AD%98%E5%9C%A8)
- [如果/b/c存在](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#%E5%A6%82%E6%9E%9Cbc%E5%AD%98%E5%9C%A8)
- [cp^-R^/a/^/b](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rab-2)
- [cp^-R^/a/^/b/](https://blog.lazying.art/en/html/computer_internet/unix_linux/command_shell_software/740/linux%e5%91%bd%e4%bb%a4cp%e6%96%87%e4%bb%b6%e5%a4%b9%e6%97%b6%e6%b7%bb%e5%8a%a0%e6%ad%a3%e6%96%9c%e6%9d%a0%e5%92%8c%e9%80%9a%e9%85%8d%e7%ac%a6%e7%9a%84%e5%90%84%e7%a7%8d%e7%94%a8%e6%b3%95%e6%80%bb.html/#cp-Rab-3)
## cp^-R^/a/*^/b
等同于cp^-R^/a/*^/b/
这相当于把/a目录下所有文件拷贝到/b目录下,如果要包括/a目录下所有的子目录和文件,请加-R选项或-r选项,-r选项的不同于-R之处在于尝试打开目的地文件前先删除己存在的目的地文件。
## cp^-R^/a^/b/c
分两种情况讨论
### 如果/b/c不存在
则创建。并且把/a中所有的文件和子目录都复制到/b/c中。相当于克隆了一个目录。
### 如果/b/c存在
将把/a文件夹本身复制到/b/c中,复制成功后,目录结构为/b/c/a。
## cp^-R^/a/^/b
出错,提示:
cp: omitting directory ‘/a/’
## cp^-R^/a/^/b/
出错,提示:
cp: omitting directory ‘/a/’
