3

According to man ssh and this previous answer, ssh should propagate the exit status of whatever process it ran on the remote server. I seem to have found a mystifying exception!

$ ssh myserver exit 34 ; echo $?
34

Good...

$ ssh myserver 'exit 34' ; echo $?
34

Good...

$ ssh myserver bash -c 'exit 34' ; echo $?
0

What?!?

$ ssh myserver
ubuntu@myserver $ bash -c 'exit 34' ; echo $?
34

So the problem does not appear to be either ssh or bash -c in isolation, but their combination does not behave as I would expect.

I'm designing a script to be run on a remote machine that needs to take an argument list that's computed on the client side. For the sake of argument, let's say it fails if any of the arguments is not a file on the remote server:

ssh myserver bash -c '
    for arg ; do
        if [[ ! -f "$arg" ]] ; then
            exit 1
        fi
    done
' arg1 arg2 ...

How can I run something like this and effectively inspect its return status? The test above seems to suggest I cannot.

jsharp
  • 565
  • 2
  • 14
  • 2
    As a general rule, don't rely on `ssh` to assemble multiple arguments into a single command. *Always* pass a single argument containing the command, i.e. `ssh myserver 'bash -c "exit 34"'` instead of `ssh myserver bash -c "exit 34"`. – chepner Jan 25 '19 at 20:37
  • It's not *immediately obvious* that these questions are the same, but the root cause of this one is (as described in detail by Barmar) the issue explicitly asked about in the other; thus, answers given there apply here as well. – Charles Duffy Jan 25 '19 at 20:45

3 Answers3

5

The problem is that the quoting is being lost. ssh simply concatenates the arguments, it doesn't requote them, so the command you're actually executing on the server is:

bash -c exit 34

The -c option only takes one argument, not all the remaining arguments, so it's just executing exit; 34 is being ignored.

You can see a similar effect if you do:

ssh myserver bash -c 'echo foo'

It will just echo a blank line, not foo.

You can fix it by giving a single argument to ssh:

ssh myserver "bash -c 'exit 34'"

or by doubling the quotes:

ssh myserver bash -c "'exit 34'"
Barmar
  • 741,623
  • 53
  • 500
  • 612
1

Insofar as your question is how to run a command remotely while passing it on ssh's command line without it getting in a mangle that triggers the bug in question, printf '%q ' can be used to ask the shell to perform quoting on your behalf, to build a string which can then be passed to ssh:

printf -v cmd_str '%q ' bash -c '
    for arg ; do
        if [[ ! -f "$arg" ]] ; then
            exit 1
        fi
    done
' arg1 arg2 ...
ssh "$host" "$cmd_str"

However, this is only guaranteed to work correctly if the default shell for the remote user is also bash (or, if you used ksh's printf %q locally, if the remote shell is ksh). It's much safer to pass your script text out-of-band, as on stdin:

printf -v arg_str '%q ' arg1 arg2 ...
ssh "$host" "bash -s $arg_str" <<'EOF'
for arg; do
  if [[ ! -f "$arg" ]]; then
    exit 1
  fi
done
EOF

...wherein we still depend on printf %q to generate correct output, but only for the arguments, not for the script itself.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
0

Try wrapping in quotes:

╰─➤  ssh server "bash -c 'exit 34' "; echo $?
34
PaulProgrammer
  • 16,175
  • 4
  • 39
  • 56