10

I have an if statement to calculate files and delete all except the latest three files. But I want to run this command remotely. How can I combine ssh with an if condition?

I tried this but no success.

#!/bin/bash

ssh -t  [email protected] "cd /var/www/test.com/backup ;

if [ $(ls  | wc -l) -lt 3  ]
then
    echo "Less"
else [ $(ls -t *.tgz|awk 'NR >3'|xargs rm -f) ]
    echo "deleted"
fi"

The error I got:

ls: cannot access *.tgz: No such file or directory

3
  • Then how can I customize this?
    – Janith
    Dec 3, 2018 at 5:30
  • @user68186 Why's that? The command is in quotes. Dec 3, 2018 at 11:12
  • 2
    The $( ) part of the command is executed by the local shell before it even starts the ssh command. That's true both when $( ) stands alone as well as when it is enclosed by "s. However if $( ) was inside 's it would not be executed by the local shell.
    – kasperd
    Dec 3, 2018 at 16:07

4 Answers 4

12

Yes you can exexute complex scripts via ssh

#!/bin/bash -e

ssh_cmd="$(cat <<-EOF
    cd /var/www/test.com/backup;

    if [ \$(ls  | wc -l) -lt 3  ]; then
        echo "less"
    elif [ \$(ls -t *.tgz|awk 'NR >3'|xargs rm -f) ]; then
        echo "deleted"
    else
        echo "something else"
    fi
EOF
)"

ssh -t [email protected] "$ssh_cmd"

This example uses a bash here document to generate the command string. In any case, passing scripts via ssh is error prone, because the quoting and escaping of variables is difficult (mind the backslash before the commands). If the script gets too complex, it is better to copy it via scp and then execute it on the target host.

I did not try to fix your script, but here is an example on how counting and deleting on a remote host could work:

#!/bin/bash -e

tmp_dir="$(mktemp -d)"

ssh_cmd="$(cat <<-EOF
    cd "$tmp_dir"

    cnt_tgz=(\$(find . -type f -name "*.tgz"))
    if [[ \${#cnt_tgz[@]} -lt 3 ]]; then
        echo "less"
    else
        rm "\${cnt_tgz[@]}"
        echo "deleted"
    fi
EOF
)"

touch "$tmp_dir/1.tgz"
ssh -t localhost "$ssh_cmd"
touch "$tmp_dir/2.tgz" "$tmp_dir/3.tgz"
ssh -t localhost "$ssh_cmd"

The ls -t *.tgz will not work, since the globbing is only happening on the local system. Also using ls for counting files is not a good idea, since it also returns entries like ., .. and directories.

4
  • I got some errors. syntax error near unexpected token elif', elif [ $(ls -t |awk 'NR >3'|xargs rm -f) ]; then'
    – Janith
    Dec 3, 2018 at 8:33
  • There was a ; missing in the fist if-statement. But I did not fix you script! There are many more issues e.g. the ls *.tgz Dec 3, 2018 at 9:17
  • 1
    "The ls -t *.tgz will not work, since the globbing is only happening on the local system." - it will happen on the remote system if you escape it properly. The original version had naked $(...) inside a double-quoted string, so the local shell evaluated it. Escaping it as \$(...) as you showed in the first example, or using single quotes around the whole script, prevents the local shell from expanding it, so it gets sent over to the remote system, it is expanded by the remote shell, and the script works as intended.
    – pt314
    Dec 3, 2018 at 12:46
  • This is the kind of situation where I would highly recommend using single quotes instead of a here document. Even if you have some variables you want to insert, you're better off using a single-quoted string and printf to expand variables -- that's still going to be easier to manage than trying to escape all the shell variables throughout. Also, it would avoid needing to use cat just to capture the here document into a variable! Dec 3, 2018 at 12:56
8

NOTE: there are in fact two layers to the question here. One is 'I want to execute a non-trivial task on a remote server accessible via SSH'. The other is 'I'm trying to pass a complex string to a command, and the argument ends up being different from what I intended.' I'm answering the low-level question without discussing whether the approach used is "right" (convenient, non-error-prone, secure etc.) for solving the high-level one. As indicated by the other answers and comments, it quite possibly isn't.

Your command line is mostly right; you only have to change the quoting a bit.

The main problem is that double-quoted strings are expanded by your local shell, and so the $(...) parts will be evaluated on your local system. To pass them over to the remote system, you have to enclose the script in single quotes.

You also have some embedded quotation marks. In your original script, there are the arguments for the two echos; if you change the outer quotation to single quotes, it will be the awk script. These effecitvely result in the quotation marks being omitted, which doesn't bother the echos, but it will mess up the awk script, as the greater-than sign will become output redirection. So after you change the outer quotation marks to single quotes, change those to double quotes.

This is your script with the quoting fixed. The script may have other issues, I just fixed the syntax.

#!/bin/bash

ssh -t  [email protected] 'cd /var/www/test.com/backup ;

if [ $(ls  | wc -l) -lt 3  ]
then
  echo "Less"
else [ $(ls -t *.tgz|awk "NR >3"|xargs rm -f) ]
  echo "deleted"
fi'
3
  • I want to assign /var/www/test.com/backup into a variable called "BACKUPDEST" and cd $BACKUPDEST Can you tell me how to do this?
    – Janith
    Dec 4, 2018 at 2:48
  • 1
    Where do you assign the value to BACKUPDEST? If that happens on the remote side, just include it in the script normally. If you want to set it locally (e.g. calculate it in the local script, or pass it in as a command line argument), you can change the first line to e.g. "cd $BACKUPDEST"' ; - the shell expands the double-quoted part, keeps the single-quoted part intact, concatenates the two and passes the result as the last argument to ssh.
    – pt314
    Dec 4, 2018 at 9:34
  • Uh, make that "cd '$BACKUPDEST'"' ; in case the path in that variable contains some weirdness (e.g. a space). Again: just saying that it can be done this way, not that it should be done this way.
    – pt314
    Dec 4, 2018 at 9:46
5

I think this whole complicated quoting stuff is proof enough to not use it but use a script instead. If you want to avoid multiple ssh connections, pipe the script over to the other host and let it run there in one command:

Local file, say myscript.sh:

cd /var/www/test.com/backup;

if [ $(ls  | wc -l) -lt 3  ]; then
    echo "less"
elif [ $(ls -t *.tgz|awk 'NR >3'|xargs rm -f) ]; then
    echo "deleted"
else
    echo "something else"
fi

Then:

cat myscript.sh | \
    ssh -t [email protected] \
    "cat - > /tmp/ms.sh && sh /tmp/ms.sh && rm /tmp/ms.sh"

Or (avoiding a useless use of cat):

ssh -t [email protected] \
    "cat - > /tmp/ms.sh && sh /tmp/ms.sh && rm /tmp/ms.sh" \
    < myscript.sh

This pipes the local script myscript.sh to the remote side where it is redirected to a (temporary) file /tmp/ms.sh, executed, and finally removed.

Note: I didn't check the original script for errors but just wanted to show the idea. No error prone quoting is necessary and all commands in the script are executed on the remote side.

4
  • No need to save the script into a temporary file either, just pipe it into a suitable shell, i.e. ssh user@host bash <myscript.sh.
    – pt314
    Dec 3, 2018 at 14:17
  • 2
    Keep in mind, though, that redirection only works when the script itself does not try to read from standard input.
    – chepner
    Dec 3, 2018 at 18:19
  • @pt314 Yes, but see Pipe a script into bash?
    – PerlDuck
    Dec 3, 2018 at 19:04
  • Yes, redirection has its limitations. It also avoids a bunch of security issues associated with writing (and then executing) a predictably-named temporary file. If you do use a temporary script (whether by choice or necessity), create it using mktemp or something similarly safe.
    – pt314
    Dec 4, 2018 at 9:52
3

I would prefer to place the script on the remote instance and just execute it through ssh, but here is my suggestion how this could be done in the way you want:

#!/bin/bash

HOST='[email protected]'
REMOTE_PATH='/var/www/test.com/backup'

COMMAND1="ssh \"$HOST\" 'ls \"$REMOTE_PATH\" | wc -l'"
COMMAND2="ssh \"$HOST\" 'ls -t \"$REMOTE_PATH\"/*.tgz'"
COMMAND3="xargs ssh \"$HOST\" rm -rf"

if [[ $(eval "$COMMAND1") -le 3 ]]
then
    echo "Less"
else
    eval "$COMMAND2" | awk 'NR > 3' | eval "$COMMAND3" && echo "Deleted"
fi

Notes:

  • The conditional expression -lt is replaced by -le.
  • eval - construct command by concatenating arguments.
  • I'm not sure do we really need these extra quotations \" within the $COMMAND{1..3} expression, but I decide to add them.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .