2

I wrote a Bash script that logs in to multiple remote hosts and runs smartctl (from the smartmontools package) on disks defined in the array DISKS. I'm able to pass the array to the remote hosts, but the script just echos the first element over and over. It seems like it's treating the array as a string, and ignoring all elements after the first.

DISKS=("/dev/sda" "/dev/sdb" "/dev/sdc")
HOSTS=("Ariadne" "Nyx")

for i in "${HOSTS[@]}"; do
    ssh "$i" "bash" << EOF
        for j in "${DISKS[@]}"; do
            echo "$j"
        done
EOF
done

I've tried defining the array on the remote host (before the 2nd for loop): DISKS=("${DISKS[@]}"), but that did not help.

How can I properly pass an array to a remote host via SSH, and then iterate through the array?

2
  • 3
    There is a do missing in your remote for loop.
    – kmkaplan
    Feb 4, 2017 at 22:53
  • The do in the 2nd for loop is present in the actual script. I would get a syntax error if it wasn't. I must have missed it when I copied and pasted. I have added it to the script above. Feb 4, 2017 at 23:01

2 Answers 2

3

You can pass the definition of the array to the remote bash like so (here assuming the login shell of the remote user is bash):

DISKS_definition=$(typeset -p DISKS)
for i in "${HOSTS[@]}"; do
  ssh "$i" "$DISKS_definition"'
    for j in "${DISKS[@]}"; do
      echo "$j"
    done'
done

Or if using the here-document approach:

DISKS_definition=$(typeset -p DISKS)
for i in "${HOSTS[@]}"; do
  ssh "$i" bash <<EOF
    $DISKS_definition
    for j in "\${DISKS[@]}"; do
      echo "\$j"
    done
EOF
done

If your ssh client and server allow passing LC_* variables, you could also do

DISKS_definition=$(typeset -p DISKS)
for i in "${HOSTS[@]}"; do
  LC_DISKS=$DISKS_definition ssh "$i" bash <<'EOF'
    eval "$LC_DISKS"
    for j in "${DISKS[@]}"; do
      echo "$j"
    done
EOF
done

All is in the quoting that determines which of the local or remote shell does the expansions. Remember variables are expanded inside double quotes and here-documents where the delimiter is not quoted (note the <<'EOF' in the last example that prevents expansions inside the here-document while in the earlier examples (with <<EOF) we had to use backslashes for things that we don't want expanded by the local shell).

For arrays that contain non-ASCII data, you'll want to make sure that the locale's character set is the same on the local and remote machine, or at least avoid multi-byte character sets other than UTF-8.

bash-4.3$ locale charmap
ISO-8859-1
bash-4.3$ a=($'foo\xa3$(uname>&2)bar' baz)
bash-4.3$ a_definition=$(typeset -p a)
bash-4.3$ LC_ALL=zh_HK.big5hkscs bash -c "$a_definition"
Linux

By switching to a different character set, the content of $a_definition took a different meaning (here causing the uname>&2 command to be executed).

5
  • I haven't tried the other methods, but the here-doc approach works wonderfully, and it is consistent with the structure of my script, so I only have to make a few changes. I was previously using LC_* variables to pass the variables across, but then I discovered it wasn't necessary, the vars are being passed without an issue. It was then that I realized the array wasn't being passed. The key line is DISKS_definition=$(typeset -p DISKS), that was all I needed to get it working (as well as escaping the $'s.) Feb 5, 2017 at 17:14
  • @AndyForceno, note that passing the variables as ssh host "echo $variable" is dangerous and in the general case bad practice as the variable is expanded as code for the remote shell. For instance if $variable is foo;reboot, that causes the reboot command to be executed on the remote host. When using $(typeset -p), the shell quotes the text as needed to prevent this kind of thing from happening (as long as the charset is the same as discussed). Feb 5, 2017 at 17:23
  • When I try to pass LC_DISKS=$DISKS_definition, I get the error: bash: -a: command not found. Feb 5, 2017 at 19:39
  • @AndyForceno Sorry. Typo. Not sure why you would get that error message though. Feb 5, 2017 at 19:54
  • I figured out the problem, and it was my mistake. I noticed that I had defined LC_DISKS after calling ssh, but in your script (with the quoted here-document, 3rd example above) it's the other way around. Feb 6, 2017 at 0:23
0

Your echo "$j" is evaluated when the ssh command line is evaluated, so inside the for j... for loop it's actually a constant (and undefined, at that) rather than a variable.

Try this

for i in "${HOSTS[@]}"; do
    ssh "$i" "bash" << EOF
        for j in "${DISKS[@]}"; do
            echo "\$j"
        done
EOF
done

Or this, if your $DISKS array contains safe words without whitespace.

for i in "${HOSTS[@]}"; do
    ssh "$i" "for j in ${DISKS[@]}; do echo \"\$j\"; done"
done

My preferred way of solving this kind of problem is to create a script that performs the necessary work and have that reside on each remote server. Calling the script on each server then becomes a straightforward and simple process:

for h in "${HOSTS[@]}"; do ssh "$h" /usr/local/bin/myscript; done

If you're worried about the script becoming out of date, it's not an impossible task to copy the file across from the local system before running it. Something like this would probably work:

for h in "${HOSTS[@]}"
do
    scp -p /local/path/to/myscript "$h":myscript.$$
    ssh "$h" "myscript.$$; rm -f myscript.$$"
done
3
  • The first one outputs the entire array at once, rather than iterating through the elements. The second one works, but it is problematic if you have more code after the 2nd for loop. I have more lines of code that I omitted from the question since they are not relevant. I can certainly add them to the ssh line, but it's going to be messy! Feb 4, 2017 at 23:40
  • @AndyForceno I've offered a different way of solving the problem
    – roaima
    Feb 5, 2017 at 0:12
  • 1
    In the first example ${DISKS[@]} is expanded by the local shells, so unsafe words would also be a problem (like DISKS=('$(reboot)')) and you don't want the quotes around it. Feb 5, 2017 at 1:08

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.