arraysbashspace

Read elements (may include spaces) from a line in a file into a bash array. How?


I'm totally striking out figuring out how to do something that I think should be simple.

The following test code demonstrates the issue I'm bumping up against.

#!/bin/bash

# Declare array with 4 elements
#
ARRAY_1=( 'Debian Linux' 'Redhat Linux' Ubuntu Linux )

# Contents of ARRAY_1
#
echo ARRAY_1: ${ARRAY_1[@]}

# Get number of elements in the array
#
NUM_ELEMENTS=${#ARRAY_1[@]}

# echo each element in array
#
for (( i=0;i<$NUM_ELEMENTS;i++ )); do
    echo ${ARRAY_1[${i}]}
done

echo
echo Now try with reading from a file.
echo The goal is to behave like ARRAY_1
echo Note: Its important that all the elements appear
echo on one line in the file.
echo

tmpFile=/tmp/TEST_ELEMENTS.$$
cat << EOF > $tmpFile
'Debian Linux' 'Redhat Linux' Ubuntu Linux
EOF

# How to get this to behave like ARRAY_1 assignment above?
#
# ARRAY_2=( `eval "cat $tmpFile"` )        # Nope.
# ARRAY_2=( $(cat $tmpFile) )              # Also Nope
# ARRAY_2=( $(xargs < $tmpFile) )          # Also Nope
declare -a ARRAY_2=( $(xargs < $tmpFile) )   # Also Nope

echo ARRAY_2: ${ARRAY_2[@]}
NUM_ELEMENTS=${#ARRAY_2[@]}

for (( i=0;i<$NUM_ELEMENTS;i++ )); do
    echo ${ARRAY_2[${i}]}
done

/bin/rm $tmpFile

The output appears as follow, or worse depending on what assignment line is uncomment and run.

ARRAY_1: Debian Linux Redhat Linux Ubuntu Linux
Debian Linux
Redhat Linux
Ubuntu
Linux

Now try with reading from a file.
The goal is to behave like ARRAY_1
Note: Its important that all the elements appear
on one line in the file.

ARRAY_2: Debian Linux Redhat Linux Ubuntu Linux
Debian
Linux
Redhat
Linux
Ubuntu
Linux

I really can't figure it out, and not for lack of trying! by searching various forums etc. Someone please put me to shame and point out how easy this is!

Thanks kindly for your help on this!


Solution

  • Using xargs is an interesting approach as it has the often-overlooked behaviour of handling single- and double-quoted strings in a way that may be compatible with the behaviour you seem to want. But note the additional backslash escaping:

    If the -0 option is not specified, the application shall ensure that arguments in the standard input are delimited by unquoted <blank> characters, unescaped <blank> characters, or <newline> characters, and quoting characters shall be interpreted as follows:

    • A string of zero or more non-double-quote ('"' ) non-<newline> characters can be quoted by enclosing them in double-quotes.

    • A string of zero or more non-<apostrophe> ('\'') non-<newline> characters can be quoted by enclosing them in <apostrophe> characters.

    • Any unquoted character can be escaped by preceding it with a <backslash>.

    https://pubs.opengroup.org/onlinepubs/9799919799/utilities/xargs.html

    As you require that the file contains only a single line, this allows use of xargs to split into one argument per line and then mapfile to load each line:

    unset ARRAY_2
    mapfile -t ARRAY_2 < <(xargs -r -n1 < "$tmpFile")
    

    If your bash is too old to have mapfile (e.g. macOS), then a read loop can be used:

    unset ARRAY_2
    while IFS= read -r; do
        ARRAY_2+=("$REPLY")
    done < <(xargs -r -n1 < "$tmpFile")