2

I read How can I copy a file and create the target directories at the same time?

But the solution there only works if you want to copy one file.

How would I copy multiple files into a directory, that doesn't exist yet and auto-create it with one command?

for example:

cd /tmp/
mkdir a/
cd a/
touch x1 x2
cp --magic * ../b/c/d/

so in the end the two files x1 and x2 are ordered like this:

/tmp/b/c/d/x1
/tmp/b/c/d/x2
2

Use:

$ copy() { mkdir -p -- "${@: -1}" && cp -- "$@" ; }
$ copy * ../b/c/d/

Note: it works with bash, ksh93 and zsh.

"${@: -1}" corresponds to the ${parameter:offset} form: all parameters starting at the offset. The offset -1 corresponds to the last parameter, since negative offsets count from the end. The space is necessary, otherwise it would be the ${parameter:-word} form. See the bash manual for more information.

"$@" expands to the list of parameters, which is what is wanted for cp.

Note 2: As suggested by mikeserv, you can use

eval 'mkdir -p -- "${'"$#"'}"'

which is more portable (POSIX), instead of

mkdir -p -- "${@: -1}"

Concerning the performance, the eval solution is much faster with bash 4.3.30, but the other solution is faster with ksh 93u+ and zsh 5.0.6 (ksh93 and zsh being much faster than bash in both cases); and dash 5.7 with the eval solution (the other one is not supported) is a bit faster than ksh93 with the "${@: -1}" solution. So, depending on your case (script, interactive use, support for specific shell features, etc.), make your choice... Note however that in the context of cp (which will take most of the time), these performance differences won't be noticeable.

The script I used for the test:

i=50000
while [ $i -ne 0 ];
do
  # : "${@: -1}"
  eval ': "${'"$#"'}"'
  i=$((i-1))
done

(comment out the eval line and uncomment the previous line for the other solution), called with:

time sh ./tst `seq 1000`
time bash ./tst `seq 1000`
time ksh93 ./tst `seq 1000`
time zsh ./tst `seq 1000`
  • It could be more variable like cp if you would like to use this command as a complete ceplacement for cp: for example, if you copy only one file and the target does not end with a slash, then it should copy and rename the file to the target instead of creating a folder with the target name – rubo77 Oct 8 '14 at 23:54
  • You can do roughly the same thing POSIXLY like eval 'mkdir -p -- "${'"$#"'}"' && cp -- "$@". Doing it that way also saves the shell from having to negatively index - and likely therefore iterate entirely through - the positional array which can be be computationally costly. – mikeserv Oct 9 '14 at 5:47
  • Thinking twice, it is probably worth testing for at least one arg though, like - ..."${'"$(($#?$#:1))"'}"... which would save accidental creation of directories named $0 - which your solution will not do. – mikeserv Oct 9 '14 at 5:54
  • @mikeserv If one wants to detect errors on the number of parameters, it's probably better to do it the usual way (if [ $# -lt 2 ]; then ...) and output an error message and return with a non-zero exit status. I think that anyone knows how to do this. There are various other ways to modify the command, e.g. to support the usual options, but this is getting more complex. – vinc17 Oct 9 '14 at 9:19
  • That does a non-zero exit status in the event the function is called without arguments - its return comes from mkdir -p -- "$1". I dont know why you'd put a shell parser reserved word-type complex command like if on a single line like that. Another way might be fn() { ${2+:} return 1;...} or as a simpler means of doing the same the math expression does above: ...${2+"${'"$#"'}"}... – mikeserv Oct 9 '14 at 9:29

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.