1

When I run:

ssh devops_staging@127.0.0.1 bash -c "/home/devops_staging/deployJob.sh example"

I encounter the following error:

/home/devops_staging/deployJob.sh: line 4: $1: unbound variable

If I run it without the bash -c part, it works as expected.

ssh devops_staging@127.0.0.1 /home/devops_staging/deployJob.sh example
deploy success

Why does this happen?

This is quite unexpected as I seem to recall always using this syntax of ssh ... bash -c "commands param1 param2" without any issue.

The script in question is super simple all I'm doing at line 4 is assigning a variable from $1 (which should be the first parameter):

#!/usr/bin/env bash
set -euo pipefail

CI_PROJECT_NAME="$1" ...

Debugging with the bash -x -c ... I see these suspcicous following lines :

 + '[' -z '' ']'
 + return
 + case $- in
 + return
 + /home/devops_staging/deployJob.sh
/home/devops_staging/deployJob.sh: line 4: $1: unbound variable

Z0OM
  • 3,149

1 Answers1

3

I think it's a duplicate of the following question: ssh command with quotes. It had been noted, but the author here stated:

after reading that I'm still not sure why it works like this

therefore this answer tries to explain the issue specifically in the context of the code used in the current question.

The most important information from this good answer to the linked question is:

SSH executes the remote command in a shell. It passes a string to the remote shell, not a list of arguments. The arguments that you pass to the ssh commands are concatenated with spaces in between.

If you locally run

ssh devops_staging@127.0.0.1 /home/devops_staging/deployJob.sh example

then the arguments ssh recognizes as code to be passed to the remote side Will be: /home/devops_staging/deployJob.sh, example. The string from concatenation of the arguments will be

/home/devops_staging/deployJob.sh example

and this will be the shell code to run on the remote side. It so happens it's the string you want.

But if you locally run

ssh devops_staging@127.0.0.1 bash -c "/home/devops_staging/deployJob.sh example"

then the arguments will be: bash, -c, /home/devops_staging/deployJob.sh example and the string for the remote shell will be

bash -c /home/devops_staging/deployJob.sh example

(as if the arguments were: bash, -c, /home/devops_staging/deployJob.sh, example) and this is not the shell code you want to run on the remote side. Here example will not belong to the option-argument to -c (it will be like the second sh in this another question).

If you want exactly this string as remote code:

bash -c "/home/devops_staging/deployJob.sh example"

then the easiest method is to pass the string to a local ssh as a single argument:

ssh devops_staging@127.0.0.1 'bash -c "/home/devops_staging/deployJob.sh example"'

The single-quoted argument contains all the shell code you want to pass to a shell started by the SSH server on the remote side.

Note you could even do this locally:

ssh devops_staging@127.0.0.1 'bash -c "/home/devops_staging/deployJob.sh' 'example"'

where double-quotes (for the remote shell) belong to two separate local arguments, the resulting string for a remote shell will be the same.


Note how many tools interpret and digest the command until the code in your (remote) deployJob.sh runs:

  1. The local shell does word splitting, quote removal (and in general few other things). In the result ssh may get one or more arguments it interprets as code to be passed to the remote side.

  2. ssh concatenates these arguments with spaces in between, so a single string is passed to a remote shell.

  3. The remote shell performs word splitting, quote removal (and in general few other things) on its own. It runs some command(s).

  4. If the command is bash -c … then yet another (remote) shell will parse the code being the option argument to -c. Word splitting, quote removal and other things will be performed by this shell as well.

  5. And if deployJob.sh contains a sane shebang (or none) then there will be yet another (remote) shell that will in turn interpret the file.

In general you need to predict and mastermind what tool gets what arguments after all the previous tools digested their arguments; and what arguments it will pass to the next tool. You need to design your local command, so the ultimate tool gets exactly what you want it to get.