12

Is there a way to make cp (from GNU coreutils on Linux) return a nonzero value in case the target file does already exist?
Or is there any other small utility which is commonly available and which provides this functionality?

This would be useful in shell scripts, to atomically copy a file without accidentially loosing anything, and without user interaction. A similar result can be obtained by comparing the copy against the original, but I would have hoped for a simpler solution.

4 Answers 4

6

Some cp implementations, including GNU cp, have a non-standard -n switch to not clobber a file if it exists. but GNU cp returns 0 either way.

you could use an if statement in your shell script to test the existence of the file before you copy something to it:

if [ -e /path/to/file ]
  then 
   exit 1
  else
   cp file /path/to/file
fi

or if you want a function you could use something like this:

function cpa(){ 
 if [ -e "$2" ]
  then 
   exit 1
  else 
   /bin/cp "$1" "$2"
  fi 
}
6
  • This doesn't "return a nonzero value in case the target file does already exist", which is what the OP asked for, and it isn't a replacement for cp...
    – Chris Down
    Nov 14, 2012 at 2:22
  • 1
    @ChrisDown (s)He asked for an alternative for shell scripts. Which is what I provided
    – h3rrmiller
    Nov 14, 2012 at 2:29
  • I've actually solved my current occurrence in this way, thanks. The function should better use return instead of exit. What worries me is the lack of atomicity, i.e. there might be race conditions where some other process creates the file between this script doing the check and it doing the copy.
    – MvG
    Nov 14, 2012 at 7:04
  • You want [ -e "$file" ] || [ -L "$file" ] if you want to consider symlinks to unexistent/inaccessible files. And note that there's potential for a race condition there. Nov 14, 2012 at 9:48
  • Just thinking out loud about how to avoid the race condition. If target exists, fail early. Else copy the file to some reserved-for-current-process location on the same filesystem as the desired target. Get the inode of that tempfile. Do a mv -n tempfile target. Verify that inode of target matches the one you read from tempfile, else fail. Suggested improvements?
    – dubiousjim
    Nov 14, 2012 at 11:26
3

The following shell snippet copies a file only if the target doesn't exist, and returns a nonzero status if the target exists.

(set -C; : >"$target" && cp -- "$source" "$target")

This is not atomic: if a file is created after the attempt to clobber $target and before the copy starts, it will be overwritten. Also, cp is not atomic: another process can observe the file being written. This does, however, work correctly if all concurrent writers use the same strategy.

The following more complex snippet first makes a temporary copy, then moves it into place. There is still a race condition: there is a small window of time during which an unrelated program that doesn't play ball could create the target file.

( set -C
  tmp=$(mktemp -p "$(dirname -- "$target")")
  cp -- '$source" "$tmp"
  if : >"$target"; then
    mv -i -- "$tmp" "$target"
  else
    rm -- "$tmp"
    false
  fi
)
0
2

Expanding on my comment to h3rrmiller's answer, and following the pattern of Gilles's snippet, here is another method of making a temporary copy, then moving it into place. I believe this method is not vulnerable to any race conditions. It does require the -n flag on mv, which is not part of the POSIX spec. However, it does seem to be widely available. (Verified on the GNU implementation, also BusyBox, FreeBSD, OS X.)

(
  tmp=$(mktemp "${target}.XXXXXX") && # "mktemp -p" doesn't work on BSD
  cp -- "$source" "$tmp" && # you may want to add -p flag
  inode=$(ls -di "$tmp" | cut -d' ' -f1) && # see comment below
  mv -n -- "$tmp" "$target" && # won't overwrite, but doesn't then exit non-0
  if [ "$(ls -di "$target" | cut -d' ' -f1)" != "$inode" ]; then
    rm -- "$tmp"; false
  fi
)

In Linux, you can get the inode using stat -c'%i' "$tmp", but that's not portable.

Instead of verifying the inode, another idea would be to check whether a file still exists at path $tmp. If the mv -n ... succeeded then there shouldn't be. (Unless some other process has freshly created $tmp before you checked to see whether it was missing.)

For efficiency, it'd make sense to add a check if $target exists at the start of the routine. If we see that it does exist, we can fail immediately, rather than making the copy, trying to move it, then seeing that that failed so removing the copy. This is left as an (easy) exercise.

1

You could do:

(set -C &&  cat < /path/to/src > /path/to/dest)

It won't copy anything but the content of the file though (not the permissions, ownership or sparseness as some cp implementations do).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .