3

I want to define the function cpfromserver in bash so that when I run

$ cpfromserver xxx yyy zzz

the result is the same as if I had typed

$ scp user@remote.server:"/some/location/xxx/xxx.txt /some/location/xxx/xxx.pdf /some/location/yyy/yyy.txt /some/location/yyy/yyy.pdf /some/location/zzz/zzz.txt /some/location/zzz/zzz.pdf" /somewhere/else/

where it works for any number of arguments.

(That is, the function should copy filename.txt and filename.pdf from the directory /some/location/filename/ on the remote.server to the local directory /somewhere/else/ for every filename I specify as an argument to the function. And do it all in a single ssh connection.)

Currently, I have written a function that works for a single argument, and I just loop over it, but this establishes separate ssh connections for each argument, which is undesirable.

My difficulty is that I only know how to use function arguments individually by their position ($1, $2, etc.) — not how to manipulate the whole list.

[Note that I am writing this function as a convenience tool for my own use only, and so I would prioritize my own ease of understanding over handling pathological cases like filenames with quotation marks or linebreaks in them and whatnot. I know that the filenames I will be using this with are well-behaved.]

  • You can use shift operator with $# to get the last argument. See more at [wiki.bash-hackers.org/commands/builtin/shift] – Tinti Feb 10 '15 at 18:11
  • Tangentially, you may want to look into the ssh ControlMaster option, which can be used to multiplex ssh connections. This would remove much of the overhead of using one connection per file, and might make the whole problem moot. – godlygeek Feb 10 '15 at 19:07
  • @godlygeek No, the reason separate ssh connections for each file are undesirable is because the administrators of the remote server flagged my account for suspicious activity due to my exceeding 200 ssh connections in one day. The overhead wasn't bothering me. – thecommexokid Feb 10 '15 at 19:46
4

Try this way:

cpfromserver () {
    files=''
    for x in "$@"
    do
        files="$files /some/location/$x/$x.txt /some/location/$x/$x.pdf"
    done
    scp user@remote.server:"$files" /somewhere/else/
}

Important caveat from comments: "It's worth noting for posterity that this solution definitely won't work for complicated filenames. If a filename contains a space, or a newline, or quotes, this approach will definitely fail."

  • 2
    You can skip the in $@. for i is a shortcut for for i in $@. – muru Feb 10 '15 at 18:16
  • 1
    I had just finished typing in this exact answer, almost verbatim. I used "arg" instead of "x" and "args" instead of "files", otherwise letter for letter the same. Oh well, +1 – godlygeek Feb 10 '15 at 18:16
  • 1
    @muru, I'd argue it's harder to read for newbs. Just because you can be less verbose doesn't necessarily mean you should. – godlygeek Feb 10 '15 at 18:17
  • 1
    It's worth noting for posterity that this solution definitely won't work for complicated filenames. If a filename contains a space, or a newline, or quotes, this approach will definitely fail. – godlygeek Feb 10 '15 at 18:18
  • 1
    @muru, jw013, yes, it does work that way. You can test it pretty trivially before downvoting... – godlygeek Feb 10 '15 at 18:25
1

Here a simple example:

#!/bin/bash

files_to_copy=''
destination_directory=''

while (("$#")) ; do
  if [[ "$@" = "$1" ]] ; then
    # last argument
    destination_directory="$1"
  else
    # argument
    files_to_copy="$files_to_copy $1"
  fi
  shift
done

scp user@remote.server:"$files_to_copy" $destination_directory;

If you run ./example.sh foo.pdf foo.txt foo.jpg backup/ you should get:

# this will be executed
scp user@remote.server:" foo.pdf foo.txt foo.jpg" backup/
  • 1
    This is overcomplicated because it solves a problem that the OP doesn't seem to have - passing the destination directory along with the source files. If you remove that extra logic, what you're left with is the same as jherran's solution. – godlygeek Feb 10 '15 at 19:37
  • @godlygeek agreed. Indeed I create a more flexible but also overcomplicated solution. – Tinti Feb 11 '15 at 17:36
-1

The main idea is to prepare string with the list of files to operate (even through the loop) than transfer the string to command:

sloc='/some/location'
unset flist
for i in "$@"
do
    flist[${#flist[*]}]="$sloc/$i/$i".pdf
    flist[${#flist[*]}]="$sloc/$i/$i".txt
done
scp user@remote.server:"${flist[@]}" /somewhere/else/
  • "$[@]" should be "$@". The expansion of "${flist[*]}" needs to be quoted to prevent additional word splitting - otherwise the wrong number of arguments are sent. – godlygeek Feb 10 '15 at 18:23
  • @godlygeek ${flist[*]} should not be quoted as all elements is already quoted by printf – Costas Feb 10 '15 at 18:27
  • no. scp is supposed to get called with two arguments, the first being "user@remote.server:/path/to/file1.pdf /path/to/file1.txt" and the second being "/somewhere/else". I've tried your version, and at least with bash 3.2.25 it calls scp with 3 arguments, 1) user@remote.server:"/some/location/file1/file1.pdf" 2) "/some/location/file1/file1.txt" 3) /somewhere/else/ – godlygeek Feb 10 '15 at 18:47
  • @godlygeek I would like to agree with jw013 and add user@remote.server for each argument of scp – Costas Feb 10 '15 at 18:57
  • 1
    That version would work, but it won't solve the OP's original problem: "Currently, I have written a function that works for a single argument, and I just loop over it, but this establishes separate ssh connections for each argument, which is undesirable." Putting multiple "user@remote.server:file" arguments will create multiple ssh connections. – godlygeek Feb 10 '15 at 19:01

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.