Fix ssh command quoting

Posted on 2013-01-30

SSH only takes a simple string as command to send to the remote end 1. In other words, ssh has to concatenate all arguments with a space as separator.

Example:

1
2
$ ssh stbuehler.de echo Hello World
Hello World

In this case, my local ssh program gets the command ['echo', 'Hello', 'World'] from the system, build the command string 'echo Hello World' from, sends it to my server, and the ssh process there will give this command string to the shell, which will expand it into the 3 separate parts again.

You can see the command string if you give ssh the -v option and have a look for the line with debug1: Sending command:.

If I run the “same” command on my local system, I get the same output:

1
2
$ echo Hello World
Hello World

Now lets try something else:

1
2
$ ssh stbuehler.de 'echo Hello World'
Hello World

1
2
$ 'echo Hello World'
bash: echo Hello World: command not found

What did happen now?
With ssh, my local ssh program got ['echo Hello World'] – but the command it sent was the same, so the server printed the same line as before.

But my local shell still sees the quotes around, and won’t split it – that is what quotes are for.

This behaviour allows tricks like this one:

1
2
$ ssh stbuehler.de 'echo *'
[list of the (visible) files in the home directory on the server]

On my local system '*' won’t get expanded as it is quoted, but the remote end doesn’t have the quotes anymore, and the shell will expand it.

So what is the problem?

1
2
$ printf '%-10s %s\n' Hello World
Hello      World

printf is a nice tool to output stuff in a formatted way. Now lets try that with ssh:

1
2
$ ssh stbuehler.de printf '%-10s %s\n' Hello World; echo X
%sn       Hello     World     X

I added the echo X so you can see that my server didn’t even print a newline.

This is not what I wanted to see though – how did this happen?

1
2
3
4
5
6
$ ssh -v stbuehler.de printf '%-10s %s\n' Hello World
[...]
debug1: Sending command: printf %-10s %s\\n Hello World
[...]
$ printf %-10s %s\\n Hello World; echo X
%s\n      Hello     World     X

As it looses the quotes around my arguments (which contained spaces), it breaks the first argument up, which leads to a completely different result.

I think this is a bug – you would expect that a command with ssh works the same way as it does local. For this the command should have been designed as a list of strings in the SSH Protocol. Nobody will fix this now ofc, so we will have to work around that.

At stackoverflow someone had the same problem, and the answers show how to workaround it. But I wanted a new “ssh” program, that would fix this for me. I named it sshsystem, and it works:

1
2
$ sshsystem stbuehler.de printf '%-10s %s\n' Hello World
Hello      World

Yes!

Other fun you can have:

1
2
3
4
5
6
7
8
9
$ ssh stbuehler.de echo Foo ';' echo Bar
Foo
Bar
$ ssh stbuehler.de echo Foo $'\n' echo Bar
Foo
Bar
$ sshsystem stbuehler.de echo Foo $'\n' echo Bar
Foo 
 echo Bar

(The shell on the other side can execute more than one command – either split them with ; or \n – quoted ofc, otherwise your local shell will interpret them)

Source is available at gist.github.com/4672115, stackoverflow and below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/bin/bash

# quote command in ssh call to prevent remote side from expanding any arguments
# uses bash printf %q for quoting - no idea how compatible this is with other shells.
# http://stackoverflow.com/questions/6592376/prevent-ssh-from-breaking-up-shell-script-parameters

sshargs=()

while (( $# > 0 )); do
    case "$1" in
    -[1246AaCfgKkMNnqsTtVvXxYy])
        # simple argument
        sshargs+=("$1")
        shift
        ;;
    -[bcDeFIiLlmOopRSWw])
        # argument with parameter
        sshargs+=("$1")
        shift
        if (( $# == 0 )); then
            echo "missing second part of long argument" >&2
            exit 99
        fi
        sshargs+=("$1")
        shift
        ;;
    -[bcDeFIiLlmOopRSWw]*)
        # argument with parameter appended without space
        sshargs+=("$1")
        shift
        ;;
    --)
        # end of arguments
        sshargs+=("$1")
        shift
        break
        ;;
    -*)
        echo "unrecognized argument: '$1'" >&2
        exit 99
        ;;
    *)
        # end of arguments
        break
        ;;
    esac
done


# user@host
sshargs+=("$1")
shift

# command - quote
if (( $# > 0 )); then
    # no need to make COMMAND an array - ssh will merge it anyway
    COMMAND=
    while (( $# > 0 )); do
        arg=$(printf "%q" "$1")
        COMMAND="${COMMAND} ${arg}"
        shift
    done
    sshargs+=("${COMMAND}")
fi

exec ssh "${sshargs[@]}"

1 The Secure Shell (SSH) Connection Protocol, Starting a Shell or a Command

Generated using nanoc and bootstrap - Last content change: 2013-08-16 14:47