If you use ssh to run commands remotely, you may have run into the problem that you need an extra layer of escaping.

Let's say you have application myapp that for some reason only runs on host myserver. If you have functional ssh keys to log onto myserver it can be helpful to create a myapp wrapper on your desktop. After all, this:

$ myapp myargs

... is far more convenient than doing this:

$ ssh myserver
$ myapp myargs
$ logout

(Especially if you want to do stuff with stdin and stdout.)

The naive approach to /usr/local/bin/myapp is this:

1
2
#!/bin/sh
ssh myserver $*

This has a number of problems:

  • The arguments can interpreted as ssh options if they begin with a - (hyphen). The ssh client I'm dealing with right now plays nice, but it's not a bad idea to add -- before $*.
  • $* is expanded immediately, so every argument with spaces in it is broken up. This needs fixing. And no, passing "$@" does not help. You're right in that now arguments with spaces are passed to ssh as a single argument, but it still needs an extra level of escaping (*).
  • The remote end thinks that there is no tty on the other end, causing password inputs to break, among other things. If you need a tty, you can fix this by adding -t to the list of ssh options, the drawback being that output from stdout and stderr is now combined into stdout.

The proper solution (add -t only if needed):

1
2
3
4
#!/bin/sh
args=""
for arg in "$@"; do args="$args '`echo "$arg" | sed -e "s/'/'\\\\\\\\''/g"`'"; done
ssh myserver -- myapp $args

Yes, it looks like a kludge. But it forces single quotes around all your arguments and works like a charm.

(*) The extra expansion is needed to get things like ssh myserver -- rm '*.png' to work.

code shell bash