Safe Directory Copies with cp on Linux and Unix: Slashes, Globs, Attributes, and Verification

cp is not transactional and has no universal preview mode. A simple-looking directory copy can change meaning according to whether the destination exists, whether the shell expands a glob, whether the implementation is GNU or BSD/macOS, and whether the tree contains links or mount points. This guide defines the intended result first, then reduces risk with a new staging directory, verification, and a reversible cutover.

This maintained layer was rewritten in 2026. The short 2011 source contains inaccurate claims about -r and the cause of an error. Its complete text remains in the inert archive at the end and is not current operational advice.

Identify the tool and the intended result first

Do not treat behavior once seen on Linux as a rule for every Unix. Record the implementation and its manual on the machine that will perform the copy:

command -V cp
cp --version
man cp
uname -a

cp --version is GNU-style; BSD/macOS may simply reject that option, so the local man cp is authoritative for the installed command. Before starting, also write down whether you want the directory or its contents, whether the destination must be absent or may be merged, whether overwrites are allowed, whether filesystem boundaries may be crossed, which link and metadata policy applies, and who approves the final cutover.

Directory, contents, and trailing slash

These paths illustrate semantics only; do not paste them into a directory containing real data:

cp -R -- source new-copy
mkdir -p -- existing
cp -R -- source existing/
cp -R -- source/. existing/

Whether the destination exists

IntentTypical resultBoundary
Copy source to absent new-copyCreates new-copy whose contents correspond to sourceIts parent must exist and be writable
Copy source into existing existing/Usually creates or merges existing/sourceIt may overwrite names and is not a clean clone
Copy source/. into existing existing/Merges the contents, including dotfiles, into existingThe destination should be a newly created empty staging directory

The same file, a hard-link alias, a symlink resolving to the same place, and a directory copied into its own descendant are all stop conditions. Implementations detect some same-file and self-copy cases, but that detection is not a complete safety boundary.

cp is not rsync

There is no cross-implementation rule for a trailing / on a cp source:

  • GNU cp normally does not switch an ordinary directory source to “contents only” merely because it ends in /; however, a trailing slash requires the path to resolve to a directory and can change command-line symlink resolution. GNU also provides --strip-trailing-slashes.
  • Current FreeBSD and Apple open-source cp manuals explicitly say that, with -R, a source ending in / copies the directory contents rather than the directory itself.
  • rsync explicitly distinguishes source from source/ as “include the directory name” versus “copy the directory contents.” Do not project that rsync rule onto GNU cp.

When a script must express “copy the contents, including dotfiles,” source/. is clearer than source/*; still verify it in a disposable directory on the target system.

The shell expands globs

In cp -R source/ staging/, the shell expands before cp starts. Bash does not match dotfiles such as .env by default; with no matches, its default is to leave the literal pattern for the command. nullglob removes an unmatched pattern, failglob aborts that command, and dotglob makes * include dotfiles while still excluding . and ...

If a Bash glob is genuinely required, limit its settings to a subshell so they do not leak into the current session:

(
  shopt -s dotglob failglob
  cp -R -- source/* staging/
)

This is still less simple than source/.. Quote placement matters too: "source data"/ protects the space while leaving available for expansion; "source data/*" makes the asterisk literal. -- ends option processing so a source named -report is not read as an option; ./-report is another script-friendly form. Confirm that the target implementation supports the syntax you use.

Choose recursion, attribute, and link policy

OptionMeaning and limits
-RPOSIX recursive directory form; prefer it in cross-system scripts
-rGNU currently treats it as -R; historical compatibility on FreeBSD/macOS may imply following links, and their manuals discourage depending on it
-pPreserves a basic metadata set as permissions allow; the exact set and failure reporting differ by implementation
-aGNU shorthand for recursion, preserving links, and requesting all attributes; the BSD/macOS expansion and attribute coverage are not the same contract
-H / -L / -PFollow command-line links, follow every link, or follow no links; choose explicitly with recursive copies
-T / -tGNU extensions for “destination is not a directory” and an explicit target directory; neither is POSIX and BSD/macOS support must not be assumed
-xGNU, FreeBSD, and current macOS can avoid crossing filesystem boundaries, but it is not POSIX; every skipped mount must be documented

GNU can use -T to remove the “one extra directory level if the destination exists” ambiguity and -t to state the target for multiple sources:

cp -aT -- source new-copy
cp -a -t staging -- source-a source-b

These two forms are GNU-specific. Even with -T, never use an existing production directory as new-copy; merging and overwriting can still occur.

Symbolic links and special files

Decide whether to preserve each link or copy what it points to. Using -L on an untrusted tree can pull in files from outside the tree and may encounter cycles. Device nodes, FIFOs, sockets, and files changing during the run require an application consistency plan. Do not copy blindly as an administrator, and do not use --copy-contents to read device contents.

Do not infer hard-link behavior from the name -a alone. GNU -a requests link preservation; current FreeBSD/macOS cp -R manuals say hard links may become independent files. If hard-link identity matters, use an archive or synchronization tool supported and tested on the target platform.

Attributes, ACLs, extended attributes, and sparse files

Equal content does not make two trees equivalent. Permissions, ownership, timestamps, ACLs, extended attributes, security labels, resource forks, file flags, hard links, and sparse holes are all constrained by privileges and filesystem capabilities. GNU -a is shorthand for --preserve=all, but diagnostics and exit status still require checking; BSD/macOS define -a differently.

GNU cp attempts sparse-file detection by default and also offers --sparse=auto|always|never; macOS has different sparse-file controls. Across filesystems, logical sizes can match while allocated blocks differ. File size alone is not enough verification.

Filesystem boundaries

A recursive tree can contain separate mount points, bind mounts, network volumes, container mounts, or cloud-backed filesystems. Crossing them by default can copy far more data than intended; a supported -x deliberately omits child mounts. Inventory every boundary, quota, case rule, maximum-file limit, and preservable attribute first. Put the staging directory on the same filesystem as the final destination if the cutover is expected to use a same-filesystem rename.

Non-mutating preflight

cp has no universal --dry-run. Start with a read-only inventory. File names and extended attributes can reveal sensitive information, so keep output in an authorized location and redact it before sharing:

find source -mindepth 1 -maxdepth 2 -print
du -sk source
findmnt -R --target source
find source -xdev -print

The first two forms are broadly familiar; confirm findmnt and this use of find -xdev in the local manuals. Also check whether the source changes during the copy, free bytes and inodes, read and write permissions, mount state, and whether physically resolved paths overlap. Do not parse ordinary newline-separated find output to drive bulk deletion or overwriting.

A disposable rehearsal

This rehearsal writes only under /tmp/cp-guide.* created by mktemp, then checks that prefix before deletion. It was verified on this workstation with GNU coreutils 9.4 and Bash 5.2. Read local manuals elsewhere; truncate, in particular, is not 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"

After inspecting the output, remove only the recorded temporary path:

case "$work" in
  /tmp/cp-guide.*) find "$work" -depth -delete ;;
  *) printf 'Refusing unexpected path' >&2; exit 1 ;;
esac

Do not turn the /tmp rehearsal directly into a production command. It tests this tool's semantics, not production consistency, privileges, or capacity.

Stage, verify, and roll back

Copy into a new staging directory

After a backup or snapshot exists and writers are paused as the application requires, create a brand-new staging directory under the final destination's parent. This GNU example uses placeholder paths:

stage=$(mktemp -d -- /srv/import/.cp-stage.XXXXXX)
cp -a -- /srv/source/. "$stage"/

Check the cp exit status and every diagnostic. Any I/O, permission, capacity, attribute, or source-change error stops the process. Never place a partial copy into service after an error.

Verify

Compare structure and content first, then metadata required by the application. The first group below is for GNU/Linux and may require additional packages:

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"

On macOS, start with content comparison and the local attribute display:

diff -qr /srv/source "$stage"
ls -le@ /srv/source "$stage"

diff -qr does not prove that all metadata matches. Accept ACLs, extended attributes, link identity, sparse allocation, and application-level consistency separately. Large or regulated datasets may add an approved manifest and checksums, but reports containing sensitive paths must not be sent to public channels.

Cutover and rollback

Only consider a rename cutover when the final path is absent, staging is on the same filesystem, verification passed, and writers remain controlled:

test ! -e /srv/import/release
mv -- "$stage" /srv/import/release

This is not a transactional snapshot of changing data. Record the old version and recovery point before cutover, then perform read-only and application acceptance checks. If the new location has received no writes, an approved plan may rename it back. Once both sides may contain new writes, stop and use application-level recovery instead of guessing at a merge with another cp. Deleting the old directory is not rollback.

Stop conditions

  • The source is changing and there is no application-consistent snapshot or write pause.
  • The destination exists but overwrite, merge, and name-conflict policy is not approved.
  • Paths overlap, or an unexpected symlink, mount point, special file, or self-copy is found.
  • Free space, inodes, permissions, ACLs, extended attributes, or sparse support do not meet requirements.
  • cp, a verification tool, or the filesystem reports an unexplained error.
  • Root access appears necessary merely to “make it work,” but privileges and auditing were not designed.

Keep the logs and the unserved staging copy, correct the plan, and rehearse again. Do not force progress by adding -f, -L, or administrator privileges.

References

Historical source archive

The following is the complete inert archive of the visible 2011 body in source_export. No content was changed and there was no trailing whitespace to normalize. Old internal links and technical claims inside it are historical evidence, not maintained guidance.


终于把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