Expand curly braces and wildcards in a bash string variable (without eval)

Viewed 412

Given a string variable (not a string) in bash, how can one expand its curly braces (and glob any file wildcards like asterisks), and then store the resulting expansion in an array?

I know this is possible with eval:

#!/usr/bin/env bash

string="{a,b}{1,2}"
array=( $( eval echo $string ) )
echo "${array[@]}"
#a1 a2 b1 b2

But eval could be dangerous if we don't trust the contents of string (say, if it is an argument to a script). A string with embedded semicolons, quotes, spaces, arbitrary commands, etc. could open the script to attack.

Is there a safer way to do this expansion in bash? I suspect that all of the following are equally unsafe ways of expanding the string:

#!/usr/bin/env bash

eval echo $string

bash <<< "echo $string"

echo echo $string | bash

bash <(echo echo $string)

Is there perhaps a command other than bash that we can pipe the string to, that would do the expansion?

Aside: Note that in csh/tcsh this is easy, since brace expansion happens after variable substitution (unlike in bash):

#!/usr/bin/env csh

set string = "{a,b}{1,2}"
set array = ( $string )
echo "$array"
#a1 a2 b1 b2
1 Answers

The braceexpand Python module, in combination with the standard-library glob module, can be used to accomplish what you're looking for. Wrapped in shell for use from bash, this might look like:

#!/usr/bin/env bash
case $BASH_VERSION in
  ''|[123].*|4.[012].*) echo "ERROR: bash 4.3+ required" >&2; exit 1;;
esac

# store Python code in a string
expand_py=$(cat <<'EOF'
try:
  import sys, glob, braceexpand
except ImportError as e:
  sys.stderr.write("Did you remember to install the braceexpand Python module?\n")
  raise e

for arg in sys.argv[1:]:
  for globexp in braceexpand.braceexpand(arg):
    glob_results = glob.glob(globexp)
    if len(glob_results) == 0:
      sys.stdout.write(globexp)
      sys.stdout.write('\0')
      continue
    else:
      sys.stdout.write(''.join(['%s\0' % result for result in glob_results]))
EOF
)

# shell wrapper for the above Python program
expand() {
  local item
  local -n dest_array=$1; shift

  dest_array=( )
  while IFS= read -r -d '' item; do
    dest_array+=( "$item" )
  done < <(python3 -c "$expand_py" "$@")
  unset -n dest_array
}

# actually demonstrate usage
expand yourArray '{a,b}{1,2}'
declare -p yourArray

...outputs when run the intended array value:

declare -a yourArray=([0]="a1" [1]="a2" [2]="b1" [3]="b2")
Related