First echo missing when using bash -c over SSH - bash

While debugging a script that runs various commands remotely, I noticed some problems getting output from echo.
I realize that the bash -c isn't necessary here, but it still has me wondering.
In my shell:
> bash -c "echo hello && echo hi"
hello
hi
But, if I bring SSH into the picture:
> ssh ${myhost} bash -c "echo hello && echo hi"
hi
Yet, date outputs, even though that first echo didn't:
> ssh ${myhost} bash -c "date && echo hi"
Thu Jun 3 21:15:26 UTC 2021
hi
What's going on here?

When you run a command via ssh like this, it's parsed twice: first on the local computer (before it's passed to the ssh command as arguments), then again on the remote computer before it's actually executed. Each time it's parsed, the shell will apply and remove quotes and escapes. That means the double-quotes you have around the command get applied and removed by the local shell, before the command is sent to the remote shell. So what looks like this command:
bash -c "echo hello && echo hi"
Turns into this by the time the remote shell sees it:
bash -c echo hello && echo hi
...which is two separate commands, bash -c echo hello and echo hi. The second one, echo hi, works as you expect, but the first may not.
With bash -c, the argument immediately after that is taken as the command string to execute, and any further arguments are assigned to $0, $1, etc as it runs. So bash -c echo hello just runs echo with $0 set to "hello". So it prints a blank line.
If you want the command to be executed as you expect, you need two layers of quotes and/or escapes, one to be applied and removed by the local shell and another to be applied and removed by the remote shell. Any of these will work:
# Single-quotes for local shell, double for remote
ssh ${myhost} 'bash -c "echo hello && echo hi"'
# Double-quotes for local shell, single for remote
ssh ${myhost} "bash -c 'echo hello && echo hi'"
# Double-quotes for local shell, escaped doubles for remote
ssh ${myhost} "bash -c \"echo hello && echo hi\""
# Single-quotes for local shell, escaped characters for remote
ssh ${myhost} 'bash -c echo\ hello\ \&\&\ echo\ hi'
...and many more possibilities. Note that if the command string contains anything like variable references or command substitutions, you need to pay attention to whether you want them to expand on the local or remote computer, and make sure the quoting/escaping method you use accomplishes that.
BTW, in this case since you're running a command with bash -c, there's actually a third layer of parsing done by the shell invoked by bash -c. If that command has anything that needed quoting/escaping, keeping the levels straight will be even more complex.

The command that arrives to the server is : bash -c echo hello && echo hi
ie without quote
and if you run this cde locally, it produces the same output
If you want the good result
ssh mm 'bash -c "echo hello && echo hi"'

Could also try using ";" as a separator
ssh ${myserver} "echo hello; echo hi"
testuser#mymac ~ % ssh ${myserver} "echo hello; echo hi"
hello
hi
testuser#mymac ~ %

Related

bash returns different results when commands are passed to bash shell via ssh or HEREDOC [duplicate]

Below is an example of a ssh script using a heredoc (the actual script is more complex). Is it possible to use both local and remote variables within an SSH heredoc or command?
FILE_NAME is set on the local server to be used on the remote server. REMOTE_PID is set when running on the remote server to be used on local server. FILE_NAME is recognised in script. REMOTE_PID is not set.
If EOF is changed to 'EOF', then REMOTE_PID is set and `FILE_NAME is not. I don't understand why this is?
Is there a way in which both REMOTE_PID and FILE_NAME can be recognised?
Version 2 of bash being used. The default remote login is cshell, local script is to be bash.
FILE_NAME=/example/pdi.dat
ssh user#host bash << EOF
# run script with output...
REMOTE_PID=$(cat $FILE_NAME)
echo $REMOTE_PID
EOF
echo $REMOTE_PID
You need to escape the $ sign if you don't want the variable to be expanded:
$ x=abc
$ bash <<EOF
> x=def
> echo $x # This expands x before sending it to bash. Bash will see only "echo abc"
> echo \$x # This lets bash perform the expansion. Bash will see "echo $x"
> EOF
abc
def
So in your case:
ssh user#host bash << EOF
# run script with output...
REMOTE_PID=$(cat $FILE_NAME)
echo \$REMOTE_PID
EOF
Or alternatively you can just use a herestring with single quotes:
$ x=abc
$ bash <<< '
> x=def
> echo $x # This will not expand, because we are inside single quotes
> '
def
remote_user_name=user
instance_ip=127.0.0.1
external=$(ls /home/)
ssh -T -i ${private_key} -l ${remote_user_name} ${instance_ip} << END
internal=\$(ls /home/)
echo "\${internal}"
echo "${external}"
END

script doesn't see arg in '$ ssh bash script arg'

I'd like to see both commands print hello
$ bash -l -c "/bin/echo hello"
hello
$ ssh example_host bash -l -c /bin/echo hello
$
How can hello be passed as a parameter in the ssh command?
The bash -l -c is needed, so login shell startup scripts are executed.
Getting ssh to start a login shell would solve the problem too.
When you pass extra args after -c, they're put into the argv of the shell while that command is executing. You can see that like so:
bash -l -c '/bin/echo "$0" "$#"' hello world
...so, those arguments aren't put on the command line of echo (unless you go out of your way to make it so), but instead are put on the command line of the shell which you're telling to run echo with no arguments.
That is to say: When you run
bash -l -c /bin/echo hello
...that's the equivalent of this:
(exec -a hello bash -c /bin/echo)
...which puts hello into $0 of a bash which runs only /bin/echo. Since running /bin/echo doesn't look at $0, of course it's not going to print hello.
Now, because executing things via ssh means you're going through two steps of shell expansion, it adds some extra complexity. Fortunately, you can have the shell handle that for you automatically, like so:
printf -v cmd_str '%q ' bash -l -c '/bin/echo "$0" "$#"' hello world
ssh remote_host "$cmd_str"
This tells bash (printf %q is a bash extension, not available in POSIX printf) to quote your command such that it expands to itself when processed by a shell, then feeds the result into ssh.
All that said -- treating $0 as a regular parameter is bad practice, and generally shouldn't be done absent a specific and compelling reason. The Right Thing is more like the following:
printf -v cmd '%q ' /bin/echo hello world # your command
printf -v cmd '%q ' bash -l -c "$cmd" # your command, in a login shell
ssh remotehost "$cmd" # your command, in a login shell, in ssh

is it possible to use variables in remote ssh command?

I'd like to execute several commands in sequence on a remote machine, and some of the later commands depend on earlier ones. In the simplest possible example I get this:
ssh my_server "echo this is my_server; abc=2;"
this is my_server
abc=2: Command not found.
ssh my_server "echo this is my_server; abc=2; echo abc is $abc"
abc: undefined variable
For a bit of background info, what I actually want to do is piece together a path and launch a java application:
ssh my_server 'nohup sh -c "( ( echo this is my_server; jabref_exe=`which jabref`; jabref_dir=`dirname $jabref_exe`; java -jar $jabref_dir/../jabref.jar` $1 &/dev/null ) & )"' &
jabref_dir: Undefined variable.
That way, whenever jabref gets updated to a new version on the server, I won't have to manually update the path to the jar file. The jabref executable doesn't take arguments, but launching it with java -jar does, which is why I have to juggle the path a bit.
At the moment I have the list of commands in a separate script file and call
ssh my_server 'nohup sh -c "( ( my_script.sh &/dev/null ) & )"' &
which works, but since the ssh call is already inside one script file it would be nice to have everything together.
In this example
ssh my_server "echo this is my_server; abc=2;"
abc is set on the remote side, so it should be clear why it is not set on your local machine.
In the next example,
ssh my_server "echo this is my_server; abc=2; echo abc is $abc"
your local shell tries to expand $abc in the argument before it is ever sent to the remote host. A slight modification would work as you expected:
ssh my_server 'echo this is my_server; abc=2; echo abc is $abc'
The single quotes prevent your local shell from trying to expand $abc, and so the literal text makes it to the remote host.
To finally address your real question, try this:
jabref_dir=$( ssh my_server 'jabref_exe=$(which jabref); jabref_dir=$(dirname $jabref_exe);
java -jar $jabref_dir/../jabref.jar > /dev/null; echo $jabref_dir' )
This will run the quoted string as a command on your remote server, and output exactly one string: $jabref_dir. That string is captured and stored in a variable on your local host.
With some inspiration from chepner, I now have a solution that works, but only when called from a bash shell or bash script. It doesn't work from tcsh.
ssh my_server "bash -c 'echo this is \$HOSTNAME; abc=2; echo abc is \$abc;'"
Based on this, the code below is a local script which runs jabref on a remote server (although with X-forwarding by default and passwordless authentication the user can't tell it's remote):
#!/bin/bash
if [ -f "$1" ]
then
fname_start=$(echo ${1:0:4})
if [ "$fname_start" = "/tmp" ]
then
scp $1 my_server:$1
ssh my_server "bash -c 'source load_module jdk; source load_module jabref; java_exe=\$(which java); jabref_exe=\$(which jabref); jabref_dir=\$(echo \${jabref_exe%/bin/jabref});eval \$(java -jar \$jabref_dir/jabref.jar $1)'" &
else
echo input argument must be a file in /tmp.
else
echo this function requires 1 argument
fi
and this is the 1-line script load_module, since modulecmd sets environment variables and I couldn't figure out how to do that without sourcing a script.
eval `/path/to/modulecmd bash load $1`;
I also looked at heredocs, inspired by How to use SSH to run a shell script on a remote machine? and http://tldp.org/LDP/abs/html/here-docs.html. The nice part is that it works even from tcsh. I got this working from the command line, but not inside a script. That's probably easy enough to fix, but I've got a solution now so I'm happy :-)
ssh my_server 'bash -s' << EOF
echo this is \$HOSTNAME; abc=2; echo abc is \$abc;
EOF

Run 'export' command Over SSH

When I run the following from my bash shell:
bash -c '(export abc=123 && echo $abc)'
The output is "123". But when I run it over ssh:
ssh remote-host "bash -c '(export abc=123 && echo $abc)'"
There is no output. Why is this? Is there a way around this? That is, is there a way to set an environment variable for a command I run over ssh?
Note: When I replace echo $abc with something standard like echo $USER the ssh command prints out the username on the remote machine as expected since it is already set.
I am running RHEL 5 Linux with OpenSSH 4.3
That is because when using
ssh remote-host "bash -c '(export abc=123 && echo $abc)'"
the variable gets expanded by the local shell (as it is the case with $USER) before ssh executes. Escape the $ by using \$ and it should do fine
ssh remote-host "bash -c '(export abc=123 && echo \$abc)'"
On a side note:
You don't need to export just for this.
You don't need to wrap it in ()
Like so:
ssh remote-host "bash -c 'abc=123 && echo \$abc'"
Heck, you can even leave out the bash -c ... stuff, as the ssh manpage states:
If command is specified, it is executed on the remote host instead of a login shell.
But these may be specific to your task ;)

using ssh with stat command bash [duplicate]

I'd like to execute several commands in sequence on a remote machine, and some of the later commands depend on earlier ones. In the simplest possible example I get this:
ssh my_server "echo this is my_server; abc=2;"
this is my_server
abc=2: Command not found.
ssh my_server "echo this is my_server; abc=2; echo abc is $abc"
abc: undefined variable
For a bit of background info, what I actually want to do is piece together a path and launch a java application:
ssh my_server 'nohup sh -c "( ( echo this is my_server; jabref_exe=`which jabref`; jabref_dir=`dirname $jabref_exe`; java -jar $jabref_dir/../jabref.jar` $1 &/dev/null ) & )"' &
jabref_dir: Undefined variable.
That way, whenever jabref gets updated to a new version on the server, I won't have to manually update the path to the jar file. The jabref executable doesn't take arguments, but launching it with java -jar does, which is why I have to juggle the path a bit.
At the moment I have the list of commands in a separate script file and call
ssh my_server 'nohup sh -c "( ( my_script.sh &/dev/null ) & )"' &
which works, but since the ssh call is already inside one script file it would be nice to have everything together.
In this example
ssh my_server "echo this is my_server; abc=2;"
abc is set on the remote side, so it should be clear why it is not set on your local machine.
In the next example,
ssh my_server "echo this is my_server; abc=2; echo abc is $abc"
your local shell tries to expand $abc in the argument before it is ever sent to the remote host. A slight modification would work as you expected:
ssh my_server 'echo this is my_server; abc=2; echo abc is $abc'
The single quotes prevent your local shell from trying to expand $abc, and so the literal text makes it to the remote host.
To finally address your real question, try this:
jabref_dir=$( ssh my_server 'jabref_exe=$(which jabref); jabref_dir=$(dirname $jabref_exe);
java -jar $jabref_dir/../jabref.jar > /dev/null; echo $jabref_dir' )
This will run the quoted string as a command on your remote server, and output exactly one string: $jabref_dir. That string is captured and stored in a variable on your local host.
With some inspiration from chepner, I now have a solution that works, but only when called from a bash shell or bash script. It doesn't work from tcsh.
ssh my_server "bash -c 'echo this is \$HOSTNAME; abc=2; echo abc is \$abc;'"
Based on this, the code below is a local script which runs jabref on a remote server (although with X-forwarding by default and passwordless authentication the user can't tell it's remote):
#!/bin/bash
if [ -f "$1" ]
then
fname_start=$(echo ${1:0:4})
if [ "$fname_start" = "/tmp" ]
then
scp $1 my_server:$1
ssh my_server "bash -c 'source load_module jdk; source load_module jabref; java_exe=\$(which java); jabref_exe=\$(which jabref); jabref_dir=\$(echo \${jabref_exe%/bin/jabref});eval \$(java -jar \$jabref_dir/jabref.jar $1)'" &
else
echo input argument must be a file in /tmp.
else
echo this function requires 1 argument
fi
and this is the 1-line script load_module, since modulecmd sets environment variables and I couldn't figure out how to do that without sourcing a script.
eval `/path/to/modulecmd bash load $1`;
I also looked at heredocs, inspired by How to use SSH to run a shell script on a remote machine? and http://tldp.org/LDP/abs/html/here-docs.html. The nice part is that it works even from tcsh. I got this working from the command line, but not inside a script. That's probably easy enough to fix, but I've got a solution now so I'm happy :-)
ssh my_server 'bash -s' << EOF
echo this is \$HOSTNAME; abc=2; echo abc is \$abc;
EOF

Resources