1

The Bash command I used:

  1. $ ssh user@myserver.com ps -aux|grep -v \"grep\"|grep "/srv/adih/server/app.js"|awk '{print $2}'

    6373
    
  2. $ 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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Bai Yang
  • 394
  • 2
  • 5
  • 17

2 Answers2

5

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
tripleee
  • 175,061
  • 34
  • 275
  • 318
  • There are platform differences between `pidof` and `pgrep` and `pkill` implementations. You might need to pass in `-f` to examine the full argument list of the matched process. – tripleee Dec 05 '17 at 06:18
2

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.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • 1
    You didn't fix either of the big fat ugly uselessnesses in the OP's code. You really want `ssh user@myserver.com <<<"ps -aux | awk '/\/srv\/adih\/server\/app\.js/ { print \$2 }'"` (refactored to a here string to make it working code in a comment). – tripleee Dec 05 '17 at 06:33
  • Something is not as what you said. The result I got in the first command is exactly the pid on remote machine. – Bai Yang Dec 05 '17 at 06:42
  • $ ssh user@remote.com ps -aux|grep -v \"grep\"|grep "/srv/adih/server/app.js"|awk '{print $2}' **6373** and I executed it on remote computer: $ ps aux | grep "/srv/adih/server/app.js" git **6373** 0.0 0.4 1650012 37164 pts/1 Sl+ 13:40 0:00 /usr/local/bin/node --debug /srv/adih/server/app.js git 9224 0.0 0.0 118488 2204 pts/2 S+ 14:33 0:00 grep --color=auto /srv/adih/server/app.js – Bai Yang Dec 05 '17 at 06:45
  • 1
    Yes yes, the PID is correct, but the `grep` and Awk are being run on your local host on the entire `ps` output from the remote server. (In most cases this is not really a problem -- if the output is huge, you probably want to avoid passing it over a thin remote pipe, and/or if your local system is really strapped on resources, it would be nice if all the filtering ran on the remote). – tripleee Dec 05 '17 at 06:45
  • @tripleee I've been assuming that the idea is to use `kill` instead of `echo` (and that `pkill` isn't available), so that the "useless use of `echo`" is just a test case. But I'll edit to include more options (and a simplified pipeline). – Gordon Davisson Dec 05 '17 at 06:50
  • It seems that __/bin/bash__ is needed after remote machine's name or it will fail. ssh user@remote.com /bin/bash <<'EOF' kill $(ps -aux | awk '/\/srv\/adih\/server\/app.js/ {print $2}') EOF – Bai Yang Dec 05 '17 at 07:17
  • And kill also not works somehow. It prompts that __"kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]"__ – Bai Yang Dec 05 '17 at 07:18