7

I have two identically-named directories:

$ ls mydir
file1 file2

$ ls other/mydir
file3 file4

If I copy mydir to other, the two mydirs get merged:

$ cp -r mydir other

$ ls other/mydir
file1 file2 file3 file4

Where in the man (or info) page for cp does it say it does this by default?

The same thing happens if I use cp -rn mydir other.

I would prefer it if cp asked me whether I want to merge the two mydirs; so that if I copy mydir to other while forgetting that there is already a different mydir in other, I could abort the operation. Is this possible?

  • The man page of cp says that the -n flag avoids overwriting existing files, it says nothing about directories. Like cp many other commands have flags to avoid overwriting existing files. Examples of such commands are tar (-k) and rsync (--ignore-existing). Unfortunately I know no command line tool which avoids merging directories at recursive copying. – Henrik Carlqvist Dec 19 '15 at 22:33
  • 2
    Not sure if this is what you are looking for but you could add a little test before running cp command. if [ ! -d other/mydir] ; then; cp -r mydir other; else; echo "Directory already exists at destination"; fi – Munir Dec 19 '15 at 23:41
  • 1
    @munircontractor, I think that is indeed the best solution. It could be cleaned up a little but the truth is that the core answer to the exact title question is: "You can't prevent cp from doing that unless you check first, so make sure you check first." – Wildcard Dec 20 '15 at 6:49
  • @munircontractor - Thank you for a simple solution. The only problem is that it's easier to check manually using ls rather than edit this script each time. – EmmaV Dec 21 '15 at 0:06
5

I don't see this documented in the manual of GNU coreutils. It is specified by POSIX:

2. If source_file is of type directory, the following steps shall be taken:

[snip steps that don't apply in recursive mode when the target file is an existing directory]

    f. The files in the directory source_file shall be copied to the directory dest_file […]

cp -rn doesn't help because the -n option only says “don't overwrite”, but merging directories doesn't overwrite anything.

I don't see any option to rsync or pax that would help you.

You can get this behavior with a wrapper around cp. Parsing the command line options is fiddly though. Untested code. Known issue: this doesn't support abbreviated long options.

function cp {
  typeset source target=
  typeset -a args sources
  args=("$@") sources=()
  while [[ $# -ne 0 ]]; do
    case "$1" in
      --target|-t) target=$2; shift args;;
      --target=*) target=${1#*=};;
      -t?*) target=${1#??};;
      --no-preserve|--suffix|-S) shift;;
      --) break;;
      -|[^-]*) if [ -n "$POSIXLY_CORRECT" ]; then break; else sources+=($1); fi;;
    esac
    shift
  done
  sources+=("$@")
  if [[ -z $target && ${#sources[@]} -ne 0 ]]; then
    target=${sources[-1]}
    unset sources[-1]
  fi
  for source in "${sources[@]}"; do
    source=${source%"${source##*[^/]}"}
    if [ -e "$target/${source##*/}" ]; then
      echo >&2 "Refusing to copy $source to $target/${source##*/} because the target already exists"
      return 1
    fi
  done
  command cp "$@"
}
  • Wait, what, shift args? – Arthur2e5 Dec 20 '15 at 6:24
  • @Arthur2e5 Sorry, zshism. I fixed that and a few other errors. – Gilles 'SO- stop being evil' Dec 20 '15 at 15:20
  • Thank you. I think I'm obliged to accept this because of the huge amount of effort you've obviously put into it. – EmmaV Dec 21 '15 at 0:01
1

You could create a wrapper script for copying directories (cpDirs) that'll check if any merges would occur:

#!/bin/sh
test -d "$1" && test -d "$2" || { >&2 echo "Not directories"; exit 1; }

conflicts="`for d in "$1" "$2"; do (cd "$d"; find -mindepth 1 -type d); done | 
            sort |uniq -d`"
if [ -n "$conflicts" ]; then
  >&2 printf 'The following directories would be merged:\n%s\n' "$conflicts"
  exit 1
else
  cp -r "$@"
fi
0
cd /src/path &&
find .  -type d ! -name . -prune \
\(      -exec   test -e /tgt/path/{} \; \
        \(      -ok     echo cp -rt /tgt/path {} \; \
             -o -exec   printf 'not copied:\t%s\n' {} \; \
\)      \) -o ! -name . -exec echo cp -rt /tgt/path {} +

find's -ok primitive works like -exec except that it first prompts to its stderr with a description of the command it is about to run and awaits an affirmative or negative response (like y or n) followed by enter. The above find script will prompt for confirmation if a directory in /src/path also exists in /tgt/path before copying it, but all found files in /src/path are copied without prompting.

(you'll have to remove the echos to make it do anything more than pretend to work, though)

Another find script which calls a shell for first-level directories below /src/path might look like:

cd /src/path &&
find . ! -name . -prune -exec sh -c '
    [ -t 0 ] && 
    trap "stty $(stty -g;stty -icanon)
          trap - 0 1 2;  exit" 0 1 2 
    for f
    do    [ -e "$0/$f" ] &&
          case $(printf "%b:\n%s\n" >&2 \
                     \\nsource "$(ls -ld -- "$PWD/$f")" \
                     \\ntarget "$(ls -ld -- "$0/$f")"   \
                     "copy source over target?(\"y\"es|a\"no\"ther key): \c"
                 dd count=1 2>/dev/null
                 echo >&2) in ([yY]) ! :
          esac|| set -- "$@" "$f"; shift
    done; cp -r "$@" "$0"
'   /tgt/path {} +

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.