4

Using Bash's arrays, I can store a list into an array variable and iterate over the elements while keeping quoted strings containing spaces or special characters together:

LIST=(--foo --bar --baz "test 1 2 3")
for item in "${LIST[@]}"; do
    echo "$item"
done

Output is:

--foo
--bar
--baz
test 1 2 3

I have a script that uses this feature and unfortunately need to port it to use Busybox's ash (which doesn't support arrays). I'm trying to figure out a good way to store a list in which some items may spaces, while still preserving the correct number of elements of the list.

This doesn't work, for instance (incorrectly splits test 1 2 3 into separate items):

LIST='--foo --bar --baz "test 1 2 3"'
for item in $LIST; do
    echo "$item"
done

Output is:

--foo
--bar
--baz
"test
1
2
3"

One idea I found on the Busybox mailing list is to use set -- to replace the positional parameters:

set -- --foo --bar --baz "test 1 2 3"
for item in "$@"; do
    echo "$item"
done

Output is correct:

--foo
--bar
--baz
test 1 2 3

However, this construction clobbers the positional argument list ($@), which this script also uses.

Is there any reasonable way I can have my cake and eat it too, and simulate multiple arbitrary arrays in non-Bash sh variant?

Dan Lenski
  • 76,929
  • 13
  • 76
  • 124

3 Answers3

4

You can declare a variable with \n in it:

list='--foo\n--bar\n--baz\n"test 1 2 3"'

# then iterate it using
echo -e "$list" | while read -r line; do echo "line=[$line]"; done

Output:

line=[--foo]
line=[--bar]
line=[--baz]
line=["test 1 2 3"]
anubhava
  • 761,203
  • 64
  • 569
  • 643
2

The reason most shells provide arrays of some kind is because you cannot safely simulate them using flat strings. If you can isolate the code that needs an array from the rest of your script, you can execute it in a subshell so that the positional parameters can be restored after the subshell exits.

( # Nothing in here can rely on the "real" positional
  # parameters, unless you can fsave them to a fixed set of indiviual
  # parameters first.
  first_arg=$1  # etc
  set -- --foo --bar --baz "test 1 2 3"
  for item in "$@"; do
      echo "$item"
  done
  # Any other parameters you set or update will likewise revert
  # to their former values now.
)
chepner
  • 497,756
  • 71
  • 530
  • 681
0

xargs + subshell

A few years late to the party, but... I did find a way to do it, without clobbering $@, and which can handle even malicious input.

Input:

SSH_ORIGINAL_COMMAND='echo "hello world" foo '"'"'bar'"'"'; sudo ls -lah /; say -v Ting-Ting "evil cackle"'

(I originally had an rm -rf in there, but then I realized that would be a recipe for disaster when testing variations of the script)

Converted perfectly into safe args:

# Note: DO NOT put IFS= on its own line (that would overwrite it: bad)
IFS=$'\r\n' GLOBIGNORE='*' args=($(echo "$SSH_ORIGINAL_COMMAND" | \
  xargs bash -c 'for arg in "$@"; do echo "$arg"; done'))

This gives you an nice ${args[@]} that you can use just like $@:

for arg in "${args[@]}"
do
  echo "$arg"
done

Output:

hello world
foo
bar;
sudo
rm
-rf
/;
say
-v
Ting-Ting
evil cackle

I hope that helps a future onlooker like myself.

coolaj86
  • 74,004
  • 20
  • 105
  • 125