1

I wanted to capture some data from various servers using ssh and run some commands with some conditions.

I don't want to do:

if ssh $host test -f /file; then
  # If file exists
  var=$(ssh $host long pipeline)
else
  # if it doesn't
  var=$(ssh $host another long pipeline)
fi

because it will make the process longer. I want to do the if else on the remote machine instead.


I've tried several approaches but I got no luck.

var=$(ssh $host if [ -f /file ]\; then long pipeline1 \; else long pipeline2 \; fi)

Based on this answer, it works but the last command of pipeline1 assumes that else and the rest of pipeline2 as its arguments.

command: can't read else: No such file or directory
...
command: can't read fi: No such file or directory

Then I tried this

var=$(ssh $host test -f /file \&\& pipeline1 \|\| pipeline2)

Again, last command of pipeline1 considered || as its argument.


I also tried below (based on this), which is working:

do_this () {
  if [ -f /file ]; then
    pipeline1
  else
    pipeline2
  fi
}
var=$(ssh $host "$(set); do_this")

Yet it prints unwanted error messages which don't affect my variable, but it's ugly for my script.

bash: line 1: BASHOPTS: readonly variable
bash: line 8: BASH_VERSINFO: readonly variable
bash: line 24: EUID: readonly variable
bash: line 71: PPID: readonly variable
bash: line 82: SHELLOPTS: readonly variable
bash: line 92: UID: readonly variable

Any suggestions?


Update

I think I have to include what my pipeline is, basically it just a bunch of text processing:

cat file | grep "something" | sed 's/.*="\(.*\)"/\1/' | tr ' ' '-'

As per answer from Jetchisel, in short, i had to wrap my commands with single quote.

var=$(ssh $host 'if [ -f /file ]; then cat file | grep "something" | sed 's/.*="\(.*\)"/\1/' | tr ' ' '-' ; else cat otherfile | ... ; fi'

I got tr: invalid option -- ';'. tr treated ; as its argument.


It works using heredoc:

var=$(ssh $host <<-EOF
  if [ -f file ]; then
    pipeline1
  else
    pipeline2
  fi
EOF
)

Yet it broke vim's coloring because of the regex I use in sed. I will accept heredoc as the answer for now.


Update 2: I believe my question is not a duplicate of Multiple commands in sshpass, my case is more specific, while the other thread ask it in general.

  • 2
    Does this answer your question? Multiple commands in sshpass – jesse_b Jan 25 at 1:16
  • 1
    @jesse_b I've tried using heredoc, but vim fails to parse it so I assume it's not going to work. After looking to your answer, I just tried to execute it and it works. But, the problem with vim (using heredoc) bothers me as it screws the rest (the color) of my script. Hmm.. – annahri Jan 25 at 1:48
  • You can remove the cat for starters, grep and sed can parse the file just fine. if grep -q PATTERN INPUT; then sed .....; fi Although awk alone can probably do what pipeline... – Jetchisel Jan 25 at 3:12
  • Also you cannot nest single quotes, I suggest you try to describe what are you trying to do with the file before trying to do it via ssh. – Jetchisel Jan 25 at 3:39
  • I think I just over-complicate my case. I will grab the raw data instead and process it after. Thanks for your informations. I will leave this open in case someone has any other ideas. – annahri Jan 25 at 6:27
2

In:

ssh host code

ssh actually runs a shell (the login shell of the target user) to interpret the code that you pass as argument. If more than one argument is given, ssh concatenates them with spaces and again has the user's login shell on host interpret it.

Generally, you want to pass one single code argument to ssh and make sure it's quoted with single quotes to ensure no expansion is done by the local shell.

If you know that the login shell of the remote user is Bourne/POSIX-like, all you have to do is:

var=$(ssh "$host" '
  if [ -f /file ]; then
    pipeline1
  else
    pipeline2
  fi'
)

If the code to be interpreted remotely has to have single quotes in them, you need to insert them as '\'' (leave the single quotes, insert a quoted (with backslash) single quote, reenter the single quotes).

If you can't guarantee the shell of the remote user, and you don't need to pass data to the remote command(s) over its stdin (and none of the remote commands ever reads from its stdin unless its redirected to something other than the ssh connection), you can do instead:

ssh "$host" sh << 'EOF'
  if [ -f /file ]; then
    pipeline1
  else
    pipeline2
  fi
EOF

By quoting the first EOF, we make sure no expansion is done in the here document by the local shell. And we explicitly invoke sh to interpret the code on its stdin so we know what syntax to write the script in.

That approach also avoids having to escape single quotes.

Your

cat file | grep "something" | sed 's/.*="\(.*\)"/\1/' | tr ' ' '-'

Can be written:

<file sed '/something/!d; s/.*="\(.*\)"/\1/; y/ /-/'

So that gives us:

ssh "$host" '
  file=/path/to/some/file
  otherfile=/path/to/some/other/file

  if [ -f "$file" ]; then
    <"$file" sed '\''/something/!d; s/.*="\(.*\)"/\1/; y/ /-/'\''
  else
    <"$otherfile" ...
  fi'

(which won't work if the login shell of the remote user is for instance csh, tcsh, fish, rc, es, akanga whose syntax are different from that Bourne/POSIX-like syntax)

Or:

ssh "$host" sh << 'EOF'
  file=/path/to/some/file
  otherfile=/path/to/some/other/file

  if [ -f "$file" ]; then
    <"$file" sed '/something/!d; s/.*="\(.*\)"/\1/; y/ /-/'
  else
    <"$otherfile" ...
  fi
EOF
|improve this answer|||||
  • How to omit the Pseudo-terminal will not be allocated because stdin is not a terminal. it produces? – annahri Jan 25 at 7:26
  • 1
    @annahri, you'd only get that message if you used -t. You never want to use -t with ssh unless you want to run an interactive visual command like vi/mutt... on the remote host. It would definitely not make sense under the condition "no command ever reads stdin" which I stated. See also Why is this binary file transferred over "ssh -t" being changed? – Stéphane Chazelas Jan 25 at 7:31
  • This works great! Great explanation from you. Sadly I can only up vote once. Thanks! – annahri Jan 25 at 7:42
  • Could you also explain why <"$file" sed '/something/!d; s/.*="\(.*\)"/\1/; y/ /-/' runs successfully when there's no pipe? – annahri Jan 25 at 8:00
  • @annahri, pipes are to interconnect two commands running concurrently with the output of one fed into the input of the other. cat is a command to concatenate files. It doesn't make sense to use it for a single file. Here we make sed's stdin the file open in read-only mode (with <) directly which makes a lot more sense. You may not be used to redirections being put at the start of the command, but redirections can be put anywhere for simple commands: <input sed script > output is the same as sed script < input > output or sed < input script > output – Stéphane Chazelas Jan 25 at 8:03
0

I did something similar to this one some years ago and here is how I did it.

First do test of the exit status.

ssh -t remoteuser@lremotehost 'if [[ -e /etc/fstabs ]]; then exit 0; else exit 127; fi' >/dev/null 2>&1

The check the exit status.

echo $?

The output is 127 since there is no /etc/fstabs but /etc/fstab

Now change /etc/fstabs to /etc/fstab note the there is no trailing s

ssh -t remoteuser@lremotehost 'if [[ -e /etc/fstab ]]; then exit 0; else exit 127; fi' >/dev/null 2>&1

The check the exit status again

echo $?

The output is 0 since there is /etc/fstab in the remote machine.

Then just put it in a variable and check for the exit status and execute your script depending on it. Save the exit status in a variable also.

var=$(ssh -t remoteuse@remotehost 'if [[ -e /etc/fstabs ]]; then exit 0; else exit 127; fi; exec bash -li' >/dev/null 2>&1); pid=$?

case $pid in
   0) echo 'good!';;
   *) echo 'bad!' >&2;;
esac

That's how I survived that scenario, I'm not saying that is a perfect solution but you can check it out. By the way I have the ssh key forwarded to the remotehost so there is no password checking when I login via ssh. I'm also using bash as the login shell on both machines that is why I have the exec bash -li at the end

|improve this answer|||||
  • Basically you just wrap it with single quote. I had a bad experience using double quotes before because my pipeline is broken if I use it, so I avoided it. I think your answer will work for most of the cases, not mine. – annahri Jan 25 at 2:27
  • 1
    foo="bar baz more"; echo "$foo" | sed 's/.*="\(.*\)"/\1/;s/ /-/g' Look no tr – Jetchisel Jan 25 at 3:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.