2

I was able to transfer files with scp and expect, now I tried to upload several files at once:

#!/usr/bin/expect -f
# Escapes spaces in a text
proc esc text {
    return [regsub -all {\ } $text {\\&}]
}

# Uploads several files to a specified server
proc my_scp_multi {ACCOUNT SERVER PW files newfolder} {
    set timeout 30
    send_user -- "\n"
    spawn scp $files $ACCOUNT@$SERVER:[esc $newfolder]
    match_max 100000
    # Look for password prompt
    expect {
    -re ".*Connection closed.*" {
        sendError "\n\n\nUpload failed!\nPlease check the errors above and start over again.\nThis is most likely induced by too many wrong password-attempts and will last quite a time!"
    }
    -re ".*Permission denied.*" {
        sendError "\n\n\nUpload failed!\nPlease check the errors above and start over again.\nYou entered most likely a wrong password!"
    }
    -re ".*Are.*.*yes.*no.*" {
        send "yes\n"
        exp_continue
        #look for the password prompt
    }
    -re ".*sword.*" {
        # Send password aka $PW
        send -- "$PW\r"
        # send blank line (\r) to make sure we get back to gui
        send -- "\r\n"
        exp_continue
    }

    send_user -- "Upload successful!\n"
    }

    set timeout -1
}

When I want to upload several files, the sh command is: scp $a $b $c user@server:$folder, so I called my_scp_multi "ACCOUNT" "SERVER" "PW" "~/testfileA ~/testfileB ~/testfileC" "~/test/". Which also produces this output:

spawn scp ~/testfileA ~/testfileB ~/testfileC user@server:~/test/
user@server's password: 
~/testfileA ~/testfileB ~/testfileC: No such file or directory

It seems to see "~/testfileA ~/testfileB ~/testfileC" as one file. But when I copy-paste scp ~/testfileA ~/testfileB ~/testfileC user@server:~/test/ to the console it works fine!

What am I doing wrong? I've tried "\"~/testfileA\" \"~/testfileB\" \"~/testfileC\"" and such things, but nothing did work at all.

Any ideas or suggestions?


EDITS

P.S.: I'm transferring rather small files. Building up a connection is the biggest part of the transfer. This is the reason I want it to be done in ONE scp.

P.P.S.: I played around a little and came up with:

my_scp_multi3 "user" "server" "pw" "~/a\ b/testfileA, ~/a\\ b/testfileB, ~/a\\\ b/testfileC" "~/test"

with your first solution but {*}[split $files ","] and

my_scp_multi2 "user" "server" "pw" "~/a b/testfileA" "~/a\ b/testfileB" "~/a\\ b/testfileC" "~/test"

with your second solution. This prints:

~/a b/testfileA: No such file or directory
~/a\ b/testfileB: No such file or directory
~/a\ b/testfileC: No such file or directory

and

~/a b/testfileA: No such file or directory
~/a b/testfileB: No such file or directory
~/a\ b/testfileC: No such file or directory

(BTW: I of course moved the files :) )


Thanks to all the answers, here my Solution:

using \n \0 (nullbyte) as separator, because it is the only symbol except / and \ which may not be used in filenames.

#!/usr/bin/expect -f

    # Escapes spaces in a text
    proc esc text {
        return [regsub -all {\ } $text {\\&}]
    }

# Returns the absolute Filepath
proc makeAbsolute {pathname} {
    file join [pwd] $pathname
}

proc addUploadFile {files f} {
    if {$files != ""} {
        set files "$files\0"
    }
    return "$files[makeAbsolute $f]"
}

#Counts all files from an upload-list
proc countUploadFiles {s} {
        set rc [llength [split $s "\0"]] 
        incr rc -1
        return $rc
 }

# Uploads several files from a list (created by addUploadFile) to a specified server
proc my_scp_multi {ACCOUNT SERVER PW files newfolder} {
    foreground blue
    set nFiles [countUploadFiles $files]
    set timeout [expr $nFiles * 60]
        send_user -- "\n"
        spawn scp -r {*}[split $files "\0"] $ACCOUNT@$SERVER:[esc $newfolder]
        match_max 100000
        # Look for password prompt
        expect {
        -re ".*Connection closed.*" {
            sendError "\n\n\nUpload failed!\nPlease check the errors above and start over again.\nThis is most likely induced by too many wrong password-attempts and will last quite a time!"
        }
        -re ".*Permission denied.*" {
            sendError "\n\n\nUpload failed!\nPlease check the errors above and start over again.\nYou entered most likely a wrong password!"
        }
        -re ".*Are.*.*yes.*no.*" {
            send "yes\n"
            exp_continue
            #look for the password prompt
        }
        -re ".*sword.*" {
            # Send password aka $PW
            send -- "$PW\r"
            # send blank line (\r) to make sure we get back to gui
            send -- "\r\n"
            exp_continue
        }

        send_user -- "Upload successful!\n"
        }

        set timeout -1
    }



set fls [addUploadFile "" "a b/testfileA"]
set fls [addUploadFile $fls "a b/testfileB"]
set fls [addUploadFile $fls "a b/testfileC"]

my_scp_multi "user" "server" "pw" $fls "~/test"
BenMorel
  • 34,448
  • 50
  • 182
  • 322
3244611user
  • 224
  • 2
  • 10
  • 1
    technically, the directory separator and the null byte `\0` are the only characters disallowed from a filename. Newline is a valid filename character. Try it in bash: `touch $'a\nfile\nname'`. This very fact causes shell script writers and tool developers plenty of headaches: it's why GNU find has a `-print0` directive, xargs has `-0`, etc, etc – glenn jackman Jan 28 '14 at 19:02
  • This is pain! So you'd suggest just using \0 instead of \n ? – 3244611user Jan 28 '14 at 19:07
  • 1
    why are you constructing a string and then splitting it? Just use a list in the first place: `foreach f {"a b/A" "a b/B" "a b/C"} {lappend fls [makeAbsolute $f]}; ... spawn ... {*}$files ...` – glenn jackman Jan 28 '14 at 19:31
  • Absolutely right. Hmmm works now and I'm too lazy to change it. – 3244611user Jan 28 '14 at 20:32
  • hmm, now you know what the "pain" feels like, you're OK with it? – glenn jackman Jan 28 '14 at 20:45
  • I will need to be okay w/ it :D I think there still is something I don't really get. For instance, when I login to a server via ssh and then `send` a lpr-Command like `lpr a b.pdf` or `lpr a\ b.pdf` or `lpr a\\ b.pdf` or `lpr a\\\ b.pdf` nothing will work. Again, when I c&p the code into the console it'll work? Doesnt tcl just send the command as text? – 3244611user Jan 29 '14 at 14:14
  • Hehe, nice work with `lpr 'a b.pdf'`. But I still don't get why the command does not work, when the copied one does. – 3244611user Jan 29 '14 at 15:12
  • 1
    because Tcl is not sh. Use "double quotes" or {braces} for grouping instead of relying on escaping spaces. – glenn jackman Jan 29 '14 at 17:44

3 Answers3

2

You don't want to send the filenames as a single string. Either do this:

spawn scp {*}[split $files] $ACCOUNT@$SERVER:[esc $newfolder]

And continue to quote the filenames:

my_scp_multi "ACCOUNT" "SERVER" "PW" "~/testfileA ~/testfileB ~/testfileC" "~/test/"

or do this:

proc my_scp_multi {ACCOUNT SERVER PW args} {
    set timeout 30
    send_user -- "\n"
    set files [lrange $args 0 end-1]
    set newfolder [lindex $args end]
    spawn scp {*}$files $ACCOUNT@$SERVER:[esc $newfolder]

And then do not quote the filenames

my_scp_multi "ACCOUNT" "SERVER" "PW" ~/testfileA ~/testfileB ~/testfileC "~/test/"

The splat ({*}) splits the list up into it's individual elements so the spawn command sees several words, not a single word. See http://tcl.tk/man/tcl8.5/TclCmd/Tcl.htm

glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • Thank you! Great answer! This works very good. So the scp here is a tcl-command and not the "normal" shell scp? – 3244611user Jan 28 '14 at 16:03
  • Because scp was looking for a single file named `~/testfileA ~/testfileB ~/testfileC` (with the spaces), and it could not find one. You need to provide scp with individual filenames, and you want this special Tcl syntax to do it. – glenn jackman Jan 28 '14 at 16:08
  • Okay, that's running great! Just one problem remains: spaces in file names; Either one of your solutions does not work. See more in my post. – 3244611user Jan 28 '14 at 17:03
  • 1
    That's because you're inserting a literal backslash into the filename. Just quoting suffices: `my_scp_multi2 "user" "server" "pw" "~/a b/testfileA" "~/a b/testfileB" "~/a b/testfileC" "~/test"` – glenn jackman Jan 28 '14 at 17:38
  • Even `"~/a b/testfileA" "~/a\ b/testfileB" "~/a\\ b/testfileC"` produces the same error for all three files... – 3244611user Jan 28 '14 at 17:43
  • 1
    it may be that the `~` is not being expanded. Do `cd` before spawn, and use `"a b/testfileA" "a b/testfileB" "a b/testfileC"` – glenn jackman Jan 28 '14 at 18:21
  • Yes, it indeed was. Funny, as target (and for single files) it works with ~... Thank you again! – 3244611user Jan 28 '14 at 18:25
1

You could spawn a shell and then run the scp command instead:

spawn bash
send "scp $files $ACCOUNT@$SERVER:[esc $newfolder]\r"

This allows for glob expansion but adds extra housekeeping as you will need to trap when the scp process is completed, as you still have a shell running. You could add below to your expect block:

-re "100%" {
    if { $index < $count } {
        set index [expr $index + 1]
        exp_continue
    }
}

Where index is the # of file being transferred and count the nr of files.

Timmah
  • 2,001
  • 19
  • 18
0

You should be using SSH public key authentication instead of typing in the password with expect. When it's set up properly, scp will work without any human input of passwords while keeping the system very secure. You will be free from all the troubles with expect.

If there's some reason why you cannot use pubkey, you may find sftp useful because it accepts a batch command file as -b batchfile. See man 1 sftp Not a very good solution when expect can actually split the arguments

Community
  • 1
  • 1
nodakai
  • 7,773
  • 3
  • 30
  • 60
  • Thank you! There actually is a reason why I can not (and don't want to) use public key auth. sftp might have been a solution, but glenn's answer is easier, I think. – 3244611user Jan 28 '14 at 16:11