Linux cp 安全复制目录指南:斜杠、通配符、隐藏文件与属性验证

cp 没有事务,也没有通用的预演模式。一条看似简单的目录复制命令,可能因为目标是否已存在、shell 是否展开通配符、实现来自 GNU 还是 BSD/macOS,以及符号链接或挂载点而得到不同结果。本指南先确定复制意图,再用新暂存目录、验证和可回滚切换降低风险。

本文维护层于 2026 年重写。2011 年原文很短,并含有关于 -r 与报错原因的不准确结论;它已完整保存在文末的惰性归档中,不作为当前操作建议。

先确认工具和目标

不要把“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 则明确把 sourcesource/ 区分为“包含目录名”与“目录内容”。不要把这条 rsync 规则反推给 GNU cp

当脚本需要表达“复制内容并包括点文件”时,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 或管理员权限强行继续。

参考资料

历史原文归档

以下是 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/’

Leave a Reply