A copy nests if the destination exists¶
Level: 201 · you have run cp -R and found a copy inside the copy, or a src/ inside the folder you meant to fill
One line: cp -R src dst has no fixed meaning: it asks whether dst already exists and copies into it if so, which makes the command non-idempotent — run it twice and the second run buries a copy at dst/src. A trailing slash on the source is where the two machines part: BSD cp reads src/ as the contents of src and keeps filling dst, while GNU cp ignores the slash and nests exactly as it does without one. Each system has its own spelling for "never nest", and neither one works on the other.
This is the copy equivalent of a race you cannot see: the first run succeeds, looks right, and teaches you the wrong rule. The second run — a retry after an interruption, a script run twice, a cron job — is the one that builds dst/src/src. rsync made the trailing slash meaningful precisely because cp did not, and a habit carried from rsync to cp, or from a Mac to Linux, silently changes what the command does.
Measured¶
Seven commands, on a directory holding two files. The same script, run on both machines, and the keys differ — which is the lesson.
Verified output of cp_nests_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ find src | sort
src
src/a.txt
src/b.txt
$ cp -R src dst; find dst | sort
dst
dst/a.txt
dst/b.txt
$ cp -R src dst; find dst | sort
dst
dst/a.txt
dst/b.txt
dst/src
dst/src/a.txt
dst/src/b.txt
$ cp -R src/ slash; find slash | sort
slash
slash/a.txt
slash/b.txt
$ cp -R src/ slash; find slash | sort
slash
slash/a.txt
slash/b.txt
slash/src
slash/src/a.txt
slash/src/b.txt
$ cp -RT src tgt; find tgt | sort
tgt
tgt/a.txt
tgt/b.txt
$ cp -RT src tgt; find tgt | sort
tgt
tgt/a.txt
tgt/b.txt
Verified output of cp_nests_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ find src | sort
src
src/a.txt
src/b.txt
$ cp -R src dst; find dst | sort
dst
dst/a.txt
dst/b.txt
$ cp -R src dst; find dst | sort
dst
dst/a.txt
dst/b.txt
dst/src
dst/src/a.txt
dst/src/b.txt
$ cp -R src/ slash; find slash | sort
slash
slash/a.txt
slash/b.txt
$ cp -R src/ slash; find slash | sort
slash
slash/a.txt
slash/b.txt
$ cp -RT src tgt; find tgt | sort
cp: illegal option -- T
usage: cp [-R [-H | -L | -P]] [-fi | -n] [-aclpSsvXx] source_file target_file
cp [-R [-H | -L | -P]] [-fi | -n] [-aclpSsvXx] source_file ... target_directory
find: tgt: No such file or directory
$ cp -RT src tgt; find tgt | sort
cp: illegal option -- T
usage: cp [-R [-H | -L | -P]] [-fi | -n] [-aclpSsvXx] source_file target_file
cp [-R [-H | -L | -P]] [-fi | -n] [-aclpSsvXx] source_file ... target_directory
find: tgt: No such file or directory
The first cp -R src dst creates dst as the copy. The second, identical command finds dst present and copies src inside it. Nothing warns you; both runs exit 0.
On a Mac¶
Two differences, and they point the same way: BSD cp gives the trailing slash a meaning GNU cp does not, and withholds the flag GNU uses instead.
cp -R src/ dstkeeps fillingdst. BSDcptreats the slash as the contents of, so repeating the command is safe. On Linux the slash is discarded and the second run nests, exactly as it would without it. A script written and tested on a Mac can therefore builddst/src/the first time it runs on Linux.cp -Tdoes not exist. GNU's--no-target-directoryis the explicit "dstIS the copy, never treat it as a folder to copy into". BSDcpanswersillegal option -- Tand prints its usage, so a portable script cannot reach for it.ditto src dstnever nests, with or without a slash, whether or notdstexists. It is macOS's own copy tool, it preserves resource forks, ACLs and extended attributes, and its idempotence is why it is the safe choice for a script that may be re-run. There is nodittoon Linux.
The portable rule that survives both: delete the destination first, or name it something that cannot already exist. rm -rf dst && cp -R src dst means one thing on every system.
In zsh and fish¶
Nothing changes, and that is worth stating rather than assuming: cp is an external program, so the shell contributes nothing to the decision. The zsh and fish scripts run the same seven commands and print, on a given machine, the same bytes as bash. Only the helper differs — fish needs $argv and end where bash and zsh use $* and }.
The split in the keys is between Linux and macOS, never between the shells.
| bash | zsh | fish | |
|---|---|---|---|
does cp -R nest into an existing dst? |
yes | yes | yes |
does a trailing src/ change that? |
on the OS, not the shell | same | same |
| a scratch directory | work=$(mktemp -d) |
work=$(mktemp -d) |
set work (mktemp -d) |
cd or give up |
cd "$work" \|\| exit 1 |
cd "$work" \|\| exit 1 |
cd $work; or exit 1 |
If you are coming from another library¶
- Python.
shutil.copytree↗ takes the opposite position by default: it raisesFileExistsErrorrather than nesting, anddirs_exist_ok=Trueis the opt-in. The standard library chose to make the ambiguous case an error instead of a guess. - Rust.
fd,rg,bat: what the Rust rewrites actually bought ↗ — the same theme of a rewritten Unix tool choosing different defaults from the one it replaces.
See also¶
- The first letter of
ls -lis the type — a directory is a type, and that is whatcpis testing for when it decides to nest - What a dot means to the shell — the other punctuation that changes a path's meaning, and where the shell is responsible
- coreutils: cp invocation ↗ — GNU's
-T/--no-target-directory, and why it exists - macOS
cp(1)↗ — the BSD options, with no-Tamong them - macOS
ditto(1)↗