8

I have an array:

CATEGORIES=(one two three four)

I can prepend to each array member using parameter expansion:

echo ${CATEGORIES[@]/#/foo }

I can append to each array member the same way:

echo ${CATEGORIES[@]/%/ bar}

How can I do both? None of these work:

echo ${CATEGORIES[@]/(.*)/foo \1 bar}
echo ${CATEGORIES[@]/(.*)/foo $1 bar}
echo ${CATEGORIES[@]/(.*)/foo ${BASH_REMATCH[1]} bar}
8

Depending on what your ultimate aim is, you could use printf:

$ a=(1 2 3)
$ printf "foo %s bar\n" "${a[@]}"
foo 1 bar
foo 2 bar
foo 3 bar

printf re-uses the format string until all the arguments are used up, so it provides an easy way to apply some formatting to a set of strings.

  • This works for my purposes, although it would be nice to have a pure bash solution. – Big McLargeHuge May 24 '15 at 17:23
  • @Koveras bash -c 'help printf'. – muru May 24 '15 at 17:24
  • 1
    Oh, so it is pure bash :) – Big McLargeHuge May 24 '15 at 17:25
  • Note that if you pass an empty array this results in foo bar instead of an empty string, which might be unexpected. – Griddo Nov 8 '20 at 20:46
8

For the record, with zsh, there's the ${^array} operator that turns on brace-like expansion on the elements of the array. So:

$ a=(one two three)
$ b=('foo '${^a}' bar')
$ printf '<%s>\n' $b
<foo one bar>
<foo two bar>
<foo three bar>

Search and replace also works in zsh.

$ printf '<%s>\n' ${a//(#m)*/foo $MATCH bar}
<foo one bar>
<foo two bar>
<foo three bar>

As well as printf -v on an array:

$ b=(); printf -v b 'foo %s bar' "$a[@]"
$ printf '<%s>\n' $b
<foo one bar>
<foo two bar>
<foo three bar>

Your echo ${CATEGORIES[@]/(.*)/foo \1 bar} would work in ksh93 if written as:

$ printf '<%s>\n' "${CATEGORIES[@]/@(.*)/foo \1 bar}"
<foo one bar>
<foo two bar>
<foo three bar>
5
p='* "foo  '
s='  bar $USER' 
CATEGORIES=(one two three four)
CATEGORIES=("${CATEGORIES[@]/#/$p}")
CATEGORIES=("${CATEGORIES[@]/%/$s}")

paste <(printf '[%s]\n' "${!CATEGORIES[@]}") \
      <(printf '%s\n'    "${CATEGORIES[@]}")

Output:

[0] * "foo  one  bar $USER
[1] * "foo  two  bar $USER
[2] * "foo  three  bar $USER
[3] * "foo  four  bar $USER
1

Here's a special case solution which is suitable when the appended (or prepended) string is a single character, and you don't need the values in a new array:

array=( aa bb cc )
IFS="]"                        # or, IFS="["
echo "${array[*]/#/ [}$IFS"    # or, echo "$IFS${array[*]/%/] }

which produces output [aa] [bb] [cc].

  • quoted form "${array[*]}" adds separator ] between each pair (the first character of IFS, this is where the constraints arise)
  • ${array[*]/#/ [} prepends [ to each element (or /%/ form to append)
  • finally add a trailing ] (from IFS) to the expanded value

If these steps are applied one by one you would get:

aa]bb]cc
 [aa] [bb] [cc
 [aa] [bb] [cc]

(You could also trivially recover the data as a new array if the values contain no whitespace.)

You can do the distinct prefix/postfix operations in a one-liner:

for ii in "${array[@]/#/foo }"; do echo "${ii/%/ bar}"; done

This is a more robust printf solution that copies to a new array:

mapfile -d '' newarray < <(printf "foo %s bar\0" "${array[@]}")

albeit at the expense of a subshell (bash-4.4 required for mapfile -d)

Finally a loop variation that copies to a new array, and also handles sparse and associative arrays, if needed.

declare -a array newarray   # -a for indexed array, -A for associative
array=( one two three )
for ii in "${!array[@]}"; do
  printf -v "newarray[$ii]" "foo %s bar" "${array[$ii]}"
done

(printf is not required, you could assign directly, but it's clearer IMHO. bash doesn't (yet!) support printing into an array, but zsh does, giving you a loop free copy and transform one-liner, see Stéphane's answer above.)


What would be useful here is if bash supported the common & (like $MATCH in zsh) as a place-holder for the matched string in expansions. The code is there (and has been for a long time), but sadly it's not yet enabled (see shouldexp_replacement() in subst.c). If you enable it (two #if 0 changes, and recompile), this works as hoped:

array=( aa bb cc )
newarray=( "${array[@]/*/foo & bar}" )

No matter, perhaps available in the next version...

compgen has a prefix/suffix operation (and supports &, but not in a way we can use here). The best we can do with it isn't as good a muru's printf solution:

compgen -P "foo " -S " bar" -W "${array[*]}"

(note -W takes only a single option, so the array is flattened, and this causes problems with values with spaces or anything in IFS)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.