Search code examples
bashsshpass

How to properly iterate through a list using sshpass with a single ssh-login


Situation: we're feeding a list of filenames to an sshpass and it iterates correctly through a remote folder to check whether files with the given names actually exists, then build an updated list containing only the files that do exist, which is reused later in the bash script.

Problem: The list comprises sometimes tens of thousands of files, which means tens of thousands of ssh logins, which is harming performance and sometimes getting us blocked by our own security policies.

Intended solution: instead of starting the for-loop and calling sshpass each time, do it otherwise and pass the loop to an unique sshpass call.

I've got to pass the list to the sshpass instruction in the example test below:

#!/bin/bash

all_paths=(`/bin/cat /home/user/filenames_to_be_tested.list`)
existing_paths=()

sshpass -p PASSWORD ssh -n USER@HOST bash  -c "'
for (( i=0; i<${#all_paths[@]}; i++ ))
do
  echo ${all_paths[i]}
  echo \"-->\"$i
  if [[ -f ${all_paths[i]} ]]
  then
    echo ${all_paths[i]}
    existing_paths=(${all_paths[i]})
  fi
done
'

printf '%s\n' "${existing_paths[@]}"

The issue here is that it appears to loop (you see a series of echoed lines), but in the end it is not really iterating the i and is always checking/printing the same line.

Can someone help spot the bug? Thanks!


Solution

  • The problem is that bash first parses the string and substitutes the variables. That happens before it's sent to the server. If you want to stop bash from doing that, you should escape every variable that should be executed on the server.

    #! /bin/bash
    
    all_paths=(rootfs.tar derp a)
    
    read -sp "pass? " PASS
    echo
    
    sshpass -p $PASS ssh -n $USER@$SERVER  "
    files=(${all_paths[@]})
    existing_paths=()
    for ((i=0; i<\${#files[@]}; i++)); do
            echo -n \"\${files[@]} --> \$i\"
            if [[ -f \${files[\$i]} ]]; then
                    echo \${files[\$i]}
                    existing_paths+=(\${files[\$i]})
            else
                    echo 'Not found'
            fi
    
    done
    printf '%s\n' \"\${existing_paths[@]}\"
    

    This becomes hard to read very fast. However, there's an option I personally like to use. Create functions and export them to the server to be executed there to omit escaping a lot of stuff.

    #! /bin/bash
    
    all_paths=(rootfs.tar derp a)
    
    function files_exist {
            local files=($@)
            local found=()
            for file in ${files[@]}; do
                    echo -n "$file --> "
                    if [[ -f $file ]]; then
                            echo "exist"
                            found+=("$file")
                    else
                            echo "missing"
                    fi
            done
            printf '%s\n' "${found[@]}"
    }
    
    read -sp "pass? " PASS
    echo
    
    sshpass -p $PASS ssh -n $USER@$SERVER  "
    $(typeset -f files_exist)
    files_exist ${all_paths[@]}
    "