echo $(command) gets a different result with the output of the command - bash

The Bash command I used:
$ ssh user#myserver.com ps -aux|grep -v \"grep\"|grep "/srv/adih/server/app.js"|awk '{print $2}'
6373
$ ssh user#myserver.com echo $(ps -aux|grep -v \"grep\"|grep "/srv/adih/server/app.js"|awk '{print $2}')
8630
The first result is the correct one and the second one will change echo time I execute it. But I don't know why they are different.
What am I doing?
My workstation has very limited resources, so I use a remote machine to run my Node.js application. I run it using ssh user#remotebox.com "cd /application && grunt serve" in debug mode. When I command Ctrl + C, the grunt task is stopped, but the application is is still running in debug mode. I just want to kill it, and I need to get the PID first.

The command substitution is executed by your local shell before ssh runs.
If your local system's name is here and the remote is there,
ssh there uname -n
will print there whereas
ssh there echo $(uname -n) # should have proper quoting, too
will run uname -n locally and then send the expanded command line echo here to there to be executed.
As an additional aside, echo $(command) is a useless use of echo unless you specifically require the shell to perform wildcard expansion and whitespace tokenization on the output of command before printing it.
Also, grep x | awk { y } is a useless use of grep; it can and probably should be refactored to awk '/x/ { y }' -- but of course, here you are reinventing pidof so better just use that.
ssh user#myserver.com pidof /srv/adih/server/app.js
If you want to capture the printed PID locally, the syntax for that is
pid=$(ssh user#myserver.com pidof /srv/adih/server/app.js)
Of course, if you only need to kill it, that's
ssh user#myserver.com pkill /srv/adih/server/app.js

Short answer: the $(ps ... ) command substitution is being run on the local computer, and then its output is sent (along with the echo command) to the remote computer. Essentially, it's running ssh user#myserver.com echo 8630.
Your first command is also probably not doing what you expect; the pipes are interpreted on the local computer, so it's running ssh user#myserver.com ps -aux, piping the output to grep on the local computer, piping that to another grep on the local computer, etc. I'm guessing that you wanted that whole thing to run on the remote computer so that the result could be used on the remote computer to kill a process.
Long answer: the order things are parsed and executed in shell is a bit confusing; with an ssh command in the mix, things get even more complicated. Basically, what happens is that the local shell parses the command line, including splitting it into separate commands (separated by pipes, ;, etc), and expanding $(command) and $variable substitutions (unless they're in single-quotes). It then removes the quotes and escapes (they've done their jobs) and passes the results as arguments to the various commands (such as ssh). ssh takes its arguments, sticks all the ones that look like parts of the remote command together with spaces between them, and sends them to a shell on the remote computer which does this process over again.
This means that quoting and/or escaping things like $ and | is necessary if you want them to be parsed/acted on by the remote shell rather than the local shell. And quotes don't nest, so putting quotes around the whole thing may not work the way you expect (e.g. if you're not careful, the $2 in that awk command might get expanded on the local computer, even though it looks like it's in single-quotes).
When things get messy like this, the easiest way is sometimes to pass the remote command as a here-document rather than as arguments to the ssh command. But you want quotes around the here-document delimiter to keep the various $ expansions from being done by the local shell. Something like this:
ssh user#myserver.com <<'EOF'
echo $(ps -aux|grep -v "grep"|grep "/srv/adih/server/app.js"|awk '{print $2}')
EOF
Note: be careful with indenting the remote command, since the text will be sent literally to the remote computer. If you indent it with tab characters, you can use <<- as the here-document delimiter (e.g. <<-'EOF') and it'll remove the leading tabs.
EDIT: As #tripleee pointed out, there's no need for the multiple greps, since awk can do the whole job itself. It's also unnecessary to exclude the search commands from the results (grep -v grep) because the "/" characters in the pattern need to be escaped, meaning that it won't match itself.. So you can simplify the pipeline to:
ps -aux | awk '/\/srv\/adih\/server\/app.js/ {print $2}'
Now, I've been assuming that the actual goal is to kill the relevant pid, and echo is just there for testing. If that's the case, the actual command winds up being:
ssh user#myserver.com <<'EOF'
kill $(ps -aux | awk '/\/srv\/adih\/server\/app.js/ {print $2}')
EOF
If that's not right, then the whole echo $( ) thing is best skipped entirely. There's no reason to capture the pipeline's output and then echo it, just run it and let it output directly.
And if pkill (or something similar) is available, it's much simpler to use that instead.

Related

How to escape commands in Makefile for killing process remotely?

server_stop:
ssh $(SERVER_USERNAME)#$(SERVER_HOSTNAME) \
"kill $$(ps aux | grep '[p]ython abc-server' | awk '{print $$2}')"
This gives
bash: line 0: kill: (60403) - No such process
bash: line 1: 60364: command not found
I believe the brackets around p are not escaped correctly. How do I do this?
If you know the command line, you don't need to use ps + grep. Use instead, pgrep.
server_stop:
ssh $(SERVER_USERNAME)#$(SERVER_HOSTNAME) \
'kill $$(pgrep -f "[p]ython abc-server")'
The -f allows you to pass the full command line to be found.
In order to avoid the shell evaluation to the command $$(pgrep -f "[p]ython abc-server"), surround it with single quotes, so the evaluation will happen in the target server.
Note: If possible, keep a start/stop script inside your server, so your ssh command will only call the script, avoiding the current issue.

Using sed command on remote system

I am using the below command on the local machine and it gives me the expected result:
sed -n 's/^fname\(.*\)".*/\1/p' file.txt
When I use the same command(only changed ' to ") to a same file present in the remote system, I do not get any output.
ssh remote-system "sed -n "s/^fname\(.*\)".*/\1/p" file.txt"
Please help me to get this corrected. Thanks for your help.
" and ' are different things in bash, and they are not interchangeable (they're not interchangeable in many languages, however the differences are more subtle) The single quote means 'pretend everything inside here is a string'. The only thing that will be interpreted is the next single quote.
The double quote allows bash to interpret stuff inside
For example,
echo "$TERM"
and
echo '$TERM'
return different things.
(Untested) you should be able to use single quotes and escape the internal single quotes :
ssh remote-system 'sed -n \'s/^fname(.)"./\1/p\' file.txt'
Looks like you can send a single quote with the sequence '"'"' (from this question)
so :
ssh remote-machine 'sed -n '"'"'s/^fname\(.*\)".*/\1/p'"'"' file.txt'
This runs on my machine if I ssh into localhost, there's no output because file.txt is empty, but it's a proof-of-concept.
Or - can you do the ssh session interactively/with a heredoc?
ssh remote-system
[sed command]
exit
or (again untested, look up heredocs for more info)
ssh remote-system <<-EOF
[sed command]
EOF

Bash while loop stops after first iteration when subshell is called [duplicate]

This question already has answers here:
While loop stops reading after the first line in Bash
(5 answers)
Closed 6 years ago.
This contrived bash script demonstrates the issue.
#!/bin/bash
while read -r node ; do
echo checking $node for Agent;
PID=$(ssh $node ""ps -edf | grep [j]ava | awk '{print $2}'"")
echo $PID got to here.
done < ~/agents_master.list
agents_master.list contains 1 server per line:
server1
server2
server3
Which only outputs the following:
checking server1 for Agent
Authorized use only
25176 got to here
Server 2 and 3 aren't even echoed out to screen by the line echo checking $node...
If I comment out the line PID=$(.... then the while completes the whole agents_master.list file correctly...
checking server1 for Agent
got to here
checking server2 for Agent
got to here
checking server3 for Agent
got to here
From the googling I've done, it sounds like this is related to the subshell that $(...) creates, but I don't understand why it is causing the loop to stop at the first server, server1.
Yes, this code could be re-written but I'm keen to understand this behaviour of bash and why this is happening for future.
The problem -- one of the problems -- is that ssh is forwarding stdin to the remote server. As it happens, the command you are running on the remote server (ps -edf, see below) doesn't use its standard input, but ssh will still forward what it reads, just in case. As a consequence, nothing is left for read to read, so the loop ends.
To avoid that, use ssh -n (or redirect input to /dev/null yourself, which is what the -n option does).
There are a couple of other issues which are not actually interfering with your scripts execution.
First, I have no idea why you use "" in
ssh $node ""ps -edf | grep [j]ava | awk '{print $2}'""
The "" "expands" to an empty string, so the above is effectively identical to
ssh $node ps -edf | grep [j]ava | awk '{print $2}'
that means that the grep and awk commands are being run on the local host; the output from the ps command is forwarded back to the local host by ssh. That doesn't change anything, although it does make the brackets in [j]ava redundant, since the grep won't show up in the process list, as it is not running on the host where the ps is executed. In fact, it's a good thing that the brackets are redundant, since they might not be present in the command if there happens to be a file named java in your current working directory. You really should quote that argument.
I presume that what you intended was to run the entire pipeline on the remote machine, in which case you might have tried:
ssh $node "ps -edf | grep [j]ava | awk '{print $2}'"
and found that it didn't work. It wouldn't have worked because the $2 in the awk command will be expanded to whatever $2 is in your current shell; the $2 is not protected by interior single-quotes. As far as bash is concerned, $2 is just part of a double quoted string. (And it also would shift the issue of the argument to grep not being quoted to the remote host, so you'll have problems if there is a file named java in the home directory on the remote host.
So what you actually want is
ssh -n $node 'ps -edf | grep "[j]ava" | awk "{print \$2}"'
Finally, don't use PID as the name of a shell variable. Variable names in all upper case are generally reserved, and it is perilously close to BASHPID and PPID, which are specific bash variables. Your own shell variables should have lower-case names, as in any other programming language.

Combining pipes and here documents in the shell for SSH

Okay, so I've recently discovered the magic of here documents for feeding stdin style lines into interactive commands. However, I'm trying to use this with SSH to execute a bunch of commands on a remote server, but I also need to pipe in some actual input, before executing the extra commands, to confound matters further I also need to get some results back ;)
Here's what I'm trying to use:
#!/bin/sh
RESULT=$(find -type f "$PATH" | gzip | ssh "$HOST" <<- 'REMOTE_SYNC'
cat > "/tmp/.temp_file"
# Do something with /tmp/.temp_file
REMOTE_SYNC
Is this actually correct? Part of the problem I'm having as well is that I need to pipe the data to that file in /tmp, but I should really be generating a randomly named temp file, but I'm not sure how I could do that, assign the name to a variable (so I can get back to it) and still send stdin into it.
I may also extract the find | gzip part to a separate command run locally first, as the gzipped file will likely be small enough that sending it when ready will result in a much shorter SSH connection then sending it as it's generated, but it still doesn't get around the fact that I need to be able to provide both stdin and my extra commands to SSH.
No, you can't do it like this. Both heredoc and the piped input compete for stdin, and only one wins. Look at this example:
echo test | cat << EOF
TEST
EOF
What will this print? test, TEST or both? It prints TEST, so the heredoc wins (at least in bash).
You don't really need this anyway. Luckily ssh takes a command argument, which will be passed on to the shell on the remote host, so you can just use your command as a string here. So something like this:
echo TEST | ssh user#host 'cat > tempfile; cat tempfile; rm tempfile'
would work (althoug it doesn't make much sense), the output of the left side commands is piped through ssh to the remote host and supplied as stdin there.
If you want the data to be compressed when sending it through ssh, you can just enable compression using the -C option.
edit:
Using linebreaks inside a string is perfectly fine, so this works fine too:
echo TEST | ssh user#host '
cat > tempfile
cat tempfile
rm tempfile
'
The only difference to a heredoc would be that you have to escape quotes.
If you use something like echo TEST | ssh user#host "$(<script.sh)" you can write everything into a file...

pgrep prints a different pid than expected

I wrote a small script and for some reason I need to escape any spaces passed in parameters to get it to work.
I read numerous other articles about people with this issue and it is typically due to not quoting $#, but all of my variables are quoted within the script and the parameters quoted on the command line as well. Also, if I run the script in debug mode the line that is returned can be run successfully via copy paste but fails when executed from within the script.
CODE:
connections ()
{
args="$#"
pid="$(pgrep -nf "$args")"
echo $pid
# code that shows TCP and UDP connections for $pid
}
connections "$#"
EXAMPLE:
bash test.sh "blah blah"
fails and instead returns the pid of the currently running shell
bash test.sh "blah\ blah"
succeeds and returns the pid of the process you are searching for via pgrep
Your problem has nothing to do with "$#".
If you add a -l option to pgrep, you can see why it's matching the current process.
The script you're running also includes what you're trying to search for in its own arguments.
It's like doing this, and seeing grep:
$ ps -U $USER -o pid,cmd | grep gnome-terminal
12410 grep gnome-terminal
26622 gnome-terminal --geometry=180x65+135+0
The reason the backslash makes a difference? pgrep thinks backslash+space just means space. It doesn't find your script, because that contains blah\ blah, not blah blah.

Resources