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
-rand the cause of an error. Its complete text remains in the inert archive at the end and is not current operational advice.
Table of Contents
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
| Intent | Typical result | Boundary |
|---|---|---|
Copy source to absent new-copy | Creates new-copy whose contents correspond to source | Its parent must exist and be writable |
Copy source into existing existing/ | Usually creates or merges existing/source | It may overwrite names and is not a clean clone |
Copy source/. into existing existing/ | Merges the contents, including dotfiles, into existing | The 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
cpnormally 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
cpmanuals explicitly say that, with-R, a source ending in/copies the directory contents rather than the directory itself. rsyncexplicitly distinguishessourcefromsource/as “include the directory name” versus “copy the directory contents.” Do not project thatrsyncrule onto GNUcp.
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
| Option | Meaning and limits |
|---|---|
-R | POSIX recursive directory form; prefer it in cross-system scripts |
-r | GNU currently treats it as -R; historical compatibility on FreeBSD/macOS may imply following links, and their manuals discourage depending on it |
-p | Preserves a basic metadata set as permissions allow; the exact set and failure reporting differ by implementation |
-a | GNU shorthand for recursion, preserving links, and requesting all attributes; the BSD/macOS expansion and attribute coverage are not the same contract |
-H / -L / -P | Follow command-line links, follow every link, or follow no links; choose explicitly with recursive copies |
-T / -t | GNU extensions for “destination is not a directory” and an explicit target directory; neither is POSIX and BSD/macOS support must not be assumed |
-x | GNU, 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
- GNU Coreutils: `cp` invocation
- GNU Coreutils: trailing slashes
- GNU Bash: filename expansion
- POSIX.1-2024: `cp`
- FreeBSD `cp(1)` manual
- Apple open-source `cp(1)` manual source
- Official rsync manual
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/’
