How to prevent cp from merging two identically-named directories?

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?

Asked By: EmmaV

||

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 "$@"
}
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%sn' {} ; 
)      ) -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%sn" >&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 {} +
Answered By: mikeserv

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%sn' "$conflicts"
  exit 1
else
  cp -r "$@"
fi
Answered By: PSkocik
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.