views:

203

answers:

2

I prefer to program my bash scripts to be as procedural as possible. One difficulty I've encountered in trying to do so happens when passing array data between functions, a task that's not well supported in bash.

As an example, it's trivial to initial an array in bash with multiple hard-coded, quoted values, each of which may contain multiple words:

declare -a LINES=( "Hello there" "loyal user" )
echo "Line 0: '${LINES[0]}'"
echo "Line 1: '${LINES[1]}'"
# Line 0: 'Hello there'
# Line 1: 'Loyal user'

However, replacing such hard-coded values with the output of a function seems to not work so well:

getLines() {
    echo "\"Hello there\" \"loyal user\""
}

local LINE_STR=$( getLines )
declare -a LINES=( ${LINE_STR} )
echo "Line 0: '${LINES[0]}'"
echo "Line 1: '${LINES[1]}'"
# Line 0: '"Hello'
# Line 1: 'there"'

I've tried almost every permutation of allowed bash statements to overcome this problem. The one approach that seems to work well is 'eval':

local LINE_STR=$( getLines )
eval declare -a LINES=( ${LINE_STR} )
echo "Line 0: '${LINES[0]}'"
echo "Line 1: '${LINES[1]}'"
# Line 0: 'Hello there'
# Line 1: 'loyal user'

However, this approach is wrought with security concerns, as demonstrated here:

emulateUnsafeInput() {
    echo "\"\`whoami\` just got haxxored\" \"Hahaha!\""
}

local LINE_STR=$( emulateUnsafeInput )
eval declare -a LINES=( "${LINE_STR}" )
echo "Line 0: '${LINES[0]}'"
echo "Line 1: '${LINES[1]}'"
# Line 0: 'root just got haxxored'
# Line 1: 'Hahaha!'

'read -a' seems like a possible solution, although a problematic one because 'read' will operate in a sub-shell when data is piped into it, effectively separating its variable stack from the one of the calling script.

What solutions should I consider to mitigate the security concerns of the 'eval' approach? I've included the following script which demonstrates the myriad of approaches I've tried:

#!/bin/bash

getLines() {
    echo "\"Hello there\" \"loyal user\""
}

emulateUnsafeInput() {
    echo "\"\`whoami\` just got haxxored\" \"Hahaha!\""
}

execute() {
(
    echo Test 01
    declare -a LINES=( "Hello there" "loyal user" )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'Hello there'
    # Line 1: 'loyal user'
);(
    echo Test 02
    local LINE_STR=$( getLines )
    declare -a LINES=( ${LINE_STR} )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: '"Hello'
    # Line 1: 'there"'
);(
    echo Test 03
    local LINE_STR=$( getLines )
    declare -a LINES=( "${LINE_STR}" )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: '"Hello there" "loyal user"'
    # Line 1: ''
);(
    echo Test 04
    local LINE_STR=$( getLines )
    eval declare -a LINES=( ${LINE_STR} )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'Hello there'
    # Line 1: 'loyal user'
);(
    echo Test 05
    local LINE_STR=$( getLines )
    eval declare -a LINES=( "${LINE_STR}" )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'Hello there'
    # Line 1: 'loyal user'
);(
    echo Test 06
    local LINE_STR=$( getLines )
    declare -a LINES=( $( echo ${LINE_STR} ) )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: '"Hello'
    # Line 1: 'there"'
);(
    echo Test 07
    local LINE_STR=$( getLines )
    declare -a LINES=( $( echo "${LINE_STR}" ) )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: '"Hello'
    # Line 1: 'there"'
);(
    echo Test 08
    local LINE_STR=$( getLines )
    declare -a LINES=( $( eval echo ${LINE_STR} ) )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'Hello'
    # Line 1: 'there'
);(
    echo Test 09
    local LINE_STR=$( getLines )
    declare -a LINES=( $( eval echo "${LINE_STR}" ) )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'Hello'
    # Line 1: 'there'
);(
    echo Test 10
    local LINE_STR=$( emulateUnsafeInput )
    eval declare -a LINES=( ${LINE_STR} )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'root just got haxxored'
    # Line 1: 'Hahaha!'
);(
    echo Test 11
    local LINE_STR=$( emulateUnsafeInput )
    eval declare -a LINES=( "${LINE_STR}" )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'root just got haxxored'
    # Line 1: 'Hahaha!'
);(
    echo Test 12
    local LINE_STR=$( emulateUnsafeInput )
    declare -a LINES=( $( eval echo ${LINE_STR} ) )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'root'
    # Line 1: 'just'
);(
    echo Test 13
    local LINE_STR=$( emulateUnsafeInput )
    declare -a LINES=( $( eval echo "${LINE_STR}" ) )
    echo "Line 0: '${LINES[0]}'"
    echo "Line 1: '${LINES[1]}'"
    # Line 0: 'root'
    # Line 1: 'just'
)
}

execute
+1  A: 

For the data function use echo -e and separating data with newlines:

getLines() { echo -e "\"Hello there\"\n\"loyal user\""; }

To read the data, use process substitution and redirection:

i=0
while read -r
do
    arr[i++]=$REPLY
done < <(getLines)
# Line 0: '"Hello there"'
# Line 1: '"loyal user"'

This leaves the quotes around the strings, though.

Based on techniques from here.

Dennis Williamson
A: 

The following handles spaces in array elements correctly:

#! /bin/bash

# $ ./return_an_array.sh 
# ./return_an_array.sh: line 9: declare: returned_array: not found
# declare -a returned_array='([0]="one" [1]="two three")'

return_an_array()
{
    local -a an_array=( 'one' 'two three' )
    declare -p an_array
}

declare -p returned_array
eval $(return_an_array | sed -e 's/^\(declare -a \)[^=]*=/\1 returned_array=/')
declare -p returned_array
Matt McClure