3

I'm trying to write a simple alternative script for uploading files to the transfer.sh service. One of the examples on the website mentions a way of uploading multiple files in a single "session":

$ curl -i -F filedata=@/tmp/hello.txt \
  -F filedata=@/tmp/hello2.txt https://transfer.sh/

I'm trying to make a function that would take any number of arguments (files) and pass them to cURL in such fashion. The function is as follows:

transfer() {
    build() {
        for file in $@
        do
            printf "-F filedata=@%q " $file
        done
    }

    curl --progress-bar -i \
        $(build $@) https://transfer.sh | grep https
}

The function works as expected as long as no spaces are in the filenames. The output of printf "-f filedata=@%q " "hello 1.txt" is -F filedata=@test\ 1.txt, so I expected the special characters to be escaped correctly. However, when the function is called with a filename that includes spaces:

$ transfer hello\ 1.txt

cURL does not seem to interpret the escapes and reports an error:

curl: (26) couldn't open file "test\"

I also tried quoting parts of the command, e.g. printf "-F filedata=@\"%s\" " "test 1.txt", which produces -F filedata=@"test 1.txt". In such case cURL returns the same error: curl: (26) couldn't open file ""test". It seems that it does not care about quotes at all.

I'm not really sure what causes such behaviour. How could I fix the function to also work with filenames that include whitespace?


Edit/Solution

It was possible to solve the issue by using an array, as explained by @meuh. A solution that works in both bash and zsh is:

transfer () {
    if [[ "$#" -eq 0 ]]; then
        echo "No arguments specified."
        return 1
    fi

    local -a args
    args=()
    for file in "$@"
    do
        args+=("-F filedata=@$file")
    done

    curl --progress-bar -i "${args[@]}" https://transfer.sh | grep https
}

The output in both zsh and bash is:

$ ls
test space.txt    test'special.txt
$ transfer test\ space.txt test\'special.txt
######################################################################## 100.0%
https://transfer.sh/z5R7y/test-space.txt
https://transfer.sh/z5R7y/testspecial.txt
$ transfer *
######################################################################## 100.0%
https://transfer.sh/4GDQC/test-space.txt
https://transfer.sh/4GDQC/testspecial.txt

It might be a good idea to pipe the output of the function to the clipboard with xsel --clipboard or xclip on Linux and pbcopy on OS X.


The solution provided by @Jay also works perfectly well:

transfer() {
  printf -- "-F filedata=@%s\0" "$@" \
    | xargs -0 sh -c \ 
    'curl --progress-bar -i "$@" https://transfer.sh | grep -i https' zerop
}
2

One way to avoid having bash word-splitting is to use an array to carry each argument without any need for escaping:

push(){ args[${#args[*]}]="$1"; }
build() {
    args=()
    for file
    do  push "-F"
        push "filedata=@$file"
    done
}
build "$@"
curl --progress-bar -i "${args[@]}" https://transfer.sh | grep https

The build function creates an array args and the push function adds a new value to the end of the array. The curl simply uses the array.


The first part can be simplified, as push can also be written simply as args+=("$1"), so we can remove it and change build to

build() {
    args=()
    for file
    do  args+=("-F" "filedata=@$file")
    done
}
  • Thanks @meuh, this works as intended! It works well in bash 3.2.57. I forgot to mention that I'm using zsh 5.0.8 (mostly plain), I thought it could be used as a drop-in replacement for bash most of the time. However, when I run the function in zsh I get an error: "push: args: assignment to invalid subscript range". Do you have an idea what might have caused it? Is it some scoping issue? – Bruno May 9 '16 at 21:55
  • In zsh you can append to an array simply with args+=("$1"), ie using an array on the right. I forgot you can actually do this with bash too, which simplifies the code, see edited answer. – meuh May 10 '16 at 7:13
3

Give a try to this standard and safe solution:

transfer() {
  printf -- "-F filedata=@%s\0" "$@" | xargs -0 sh -c 'curl --progress-bar -i "$@" https://transfer.sh | grep -i https' zerop
}

"$@" with the Double-Quotes will make the shell keeps the original options unchanged. See 2.5.2 Special Parameters section from The Open Group Base Specifications Issue 6:

@

Expands to the positional parameters, starting from one. When the expansion occurs within double-quotes, and where field splitting (see Field Splitting) is performed, each positional parameter shall expand as a separate field, with the provision that the expansion of the first parameter shall still be joined with the beginning part of the original word (assuming that the expanded parameter was embedded within a word), and the expansion of the last parameter shall still be joined with the last part of the original word. If there are no positional parameters, the expansion of '@' shall generate zero fields, even when '@' is double-quoted.

Using printf this way is a standard way to handle options. You may test it, and see that the result will be to generate as many -F filedata=@ strings as the number of options.

The test with 2 files having a special char in their names:

$ ls -b
f\ rst  s'cond

$ transfer() { printf -- "-F filedata=@%s\0" "$@" | xargs -0 sh -c 'curl --progress-bar -i "$@" https://transfer.sh | grep -i https' zerop ;}

$ transfer *
######################################################################## 100.0%
https://transfer.sh/fnzuh/f-rst
https://transfer.sh/fnzuh/scond
  • Wow, what a concise way, I wasn't aware of it, thanks @jay-jargot! Unfortunately, two errors are reported: curl: (6) 'Could not resolve host: 1.txt 'and 'curl: (26) couldn't open file "test\" '. It seems that it misinterpreted it even more than my first approach, though I can confirm that the printf -- ... "$@" expands absolutely correctly. I tested it in both bash 3.2.57(1) and zsh 5.0.8, with curl 7.43.0. – Bruno May 9 '16 at 21:44
  • @Bruno The answer had been updated with a standard (open group) and safe and tested solution – Jay jargot May 10 '16 at 6:50
  • 1
    @Bruno The same kind of function to zip everything and send the data in once, if you agree to use bashand process susbtitution: ziptransfer() { if [ $# -eq 0 ]; then printf "No arguments specified.\n"; return 1; fi ; curl -X PUT --upload-file <(zip -q9r - "$@") "https://transfer.sh/$(date "+%s").zip" ; } – Jay jargot May 10 '16 at 15:37
  • 1
    This works perfectly well now, thanks @Jay. The ziptransfer function is pretty neat, it works in zsh just fine. – Bruno May 10 '16 at 20:37
  • 1
    @Bruno I found the idea of transfer.sh great !! the ziptransfer function had been forwarded to the transfer.sh web site via the Send us your awesome example box. – Jay jargot May 10 '16 at 20:38

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.