Friday, September 28, 2007

Messing with arrays in bash

A couple of days ago, a problem was being discussed in #bash.

`How does one append to every element of an array?'

The usual answer is, run it in a loop. The better answer, however, is a one line parameter expansion thingie. Well, here's your answer:

$ array=( "${array[@]/%/foo}" )

That's it. One line of code to replace an entire for loop.

Here's a working example:

$ array=( foo bar baz )
$ echo "${array[@]}"
foo bar baz
$ array=( "${array[@]/%/foo}" )
$ echo "${array[@]}"
foofoo barfoo bazfoo

So how does it work?

Well, what we have here is actually the pattern matching and replacement operator from the Parameter Expansion facility of bash. The general syntax is as follows:

${parameter/pattern/string}

This way, the pattern to be matched can be replaced with the string in the value of the variable. When applied to an array index of either * or @, it performs the match and replace operation for every element of the array.

One curious feature of this operator is the use of %.

If the pattern to be matched starts with a %, it is matched at the end of the string. In our case, we've simply matched nothing at the end of the string and replaced it with foo. The result is that foo is appended to every element of the array. Cool eh?

Just be mindful of the double quotes in case your array elements have spaces or newlines in the values. :)



Once upon a time, I learnt that the expansion of "$*" or "${array[*]}" results in all the values separated with the first character of the value of $IFS. I always wondered where this feature could be used. Then all of sudden, I ended up using it twice in the space of two days.

The first problem was creating a few directories. Brace expansion was ideal. The values for directory names were coming from an array. Here's how I solved it.

(
DIRNAMES=( foo bar baz )
oIFS="$IFS"
IFS=","
eval mkdir -p /foo/{"${DIRNAMES[*]}"}
IFS="$oIFS"
)

I've put the entire thing in a subshell because I'm changing the value of IFS, which could be dangerous in a script. By using a subshell I'm making sure that rest of the script won't be affected. On top of that I'm saving the original value in $oIFS just to be sure. Yeah, paranoid. :)

eval is necessary because brace expansion happens before the parameter expansion takes place.

Anyway, "${DIRNAMES[*]}" actually expands to:

foo,bar,baz

So the whole command becomes:

$ mkdir -p /foo/{foo,bar,baz}

Nifty. :)

Here's another one.. I needed to feed some values from an array into a regex.

$ EXTENS=( txt c cpp h )
$ IFS="|"
$ awk "\$2 ~ /\.(${EXTENS[*])$/ { foo; }"

The expanded value becomes:

(txt|c|cpp|h)

:)