65

I would like to know the following;

  1. Why the given non-working example doesn't work.
  2. If there are any other cleaner methods than those given in working example.

Non-working example

> ids=(1 2 3 4);echo ${ids[*]// /|}
1 2 3 4
> ids=(1 2 3 4);echo ${${ids[*]}// /|}
-bash: ${${ids[*]}// /|}: bad substitution
> ids=(1 2 3 4);echo ${"${ids[*]}"// /|}
-bash: ${"${ids[*]}"// /|}: bad substitution

Working example

> ids=(1 2 3 4);id="${ids[@]}";echo ${id// /|}
1|2|3|4
> ids=(1 2 3 4); lst=$( IFS='|'; echo "${ids[*]}" ); echo $lst
1|2|3|4

In context, the delimited string to be used in a sed command for further parsing.

4
  • 2
    ${${ids[*]}// /|} is a syntax error, that's all. Dunno what you're trying to achieve here. Commented Nov 20, 2012 at 9:47
  • 1
    Trying to achieve variable substitution in 1 hop, it was never going to work... Commented Nov 21, 2012 at 3:20
  • 1
    Related: How can I join elements of an array in Bash?. Commented Nov 18, 2018 at 6:23
  • You can use paste. Commented Feb 22, 2023 at 4:23

4 Answers 4

77

Array to strings in bash

1. Array to string, by using $IFS

Because parentheses are used to delimit an array, not a string:

ids="1 2 3 4";echo ${ids// /|}
1|2|3|4

Some samples: Populating $ids with two strings: a b and c d

ids=("a b" "c d" e\ f)

echo ${ids[*]// /|}
a|b c|d e|f

IFS='|';echo "${ids[*]}";IFS=$' \t\n'
a b|c d|e f

... and finally:

IFS='|';echo "${ids[*]// /|}";IFS=$' \t\n'
a|b|c|d|e|f

Where array is assembled, separated by 1st char of $IFS, but with space replaced by | in each element of array.

When you do:

id="${ids[@]}"

you transfer the string build from the merging of the array ids by a space to a new variable of type string.

Note: when "${ids[@]}" give a space-separated string, "${ids[*]}" (with a star * instead of the at sign @) will render a string separated by the first character of $IFS.

what man bash says:

man -Len -Pcol\ -b bash | sed -ne '/^ *IFS /{N;N;p;q}'
   IFS    The  Internal  Field  Separator  that  is used for word splitting
          after expansion and to split  lines  into  words  with  the  read
          builtin command.  The default value is ``<space><tab><newline>''.

Playing with $IFS:

printf "%q\n" "$IFS"
$' \t\n'

Literally a space, a tabulation and (meaning or) a line-feed. So, while the first character is a space. the use of * will do the same as @.

But:

{
    IFS=: read -a array < <(echo root:x:0:0:root:/root:/bin/bash)
    
    echo 1 "${array[@]}"
    echo 2 "${array[*]}"
    OIFS="$IFS" IFS=:
    echo 3 "${array[@]}"
    echo 4 "${array[*]}"
    IFS="$OIFS"
}
1 root x 0 0 root /root /bin/bash
2 root x 0 0 root /root /bin/bash
3 root x 0 0 root /root /bin/bash
4 root:x:0:0:root:/root:/bin/bash

Note: The line IFS=: read -a array < <(...) will use : as separator, without setting $IFS permanently. This is because output line #2 present spaces as separators.

1.1 Using function, localize $IFS

To just print array

printArry() {
    local IFS="$1"
    shift
    echo "$*"
}        

printArry @ "${ids[@]}"
a b@c d@e f

Or to merge array in place.

mergeArry() {
    local IFS="$1"
    local -n _array_to_merge=$2
    _array_to_merge=("${_array_to_merge[*]}")
}

declare -p ids
declare -a ids=([0]="a b" [1]="c d" [2]="e f")

mergeArry '#' ids
declare -p ids
declare -a ids=([0]="a b#c d#e f")

2. Array of strings to array of strings ([@] vs [*])

There is a notable difference between:

  • "$@" and "${var[@]}" result in an array of strings
  • "$*" and "${var[*]}" result in an unique string

Read carefully: man '-Pless +/Special\ Parameters' bash

For this, I will quote each argument in order to not be splitted by $IFS at command line expansion, using double-quotes to permit variable expansion.

ids=('a b c' 'd e f' 'g h i')
printf '<< %s >>\n' "${ids[@]// /|}"
<< a|b|c >>
<< d|e|f >>
<< g|h|i >>
printf '<< %s >>\n' "${ids[*]// /|}"
<< a|b|c d|e|f g|h|i >>

Where:

  • All spaces where replaced by pipes, in each strings
  • All string where merged into one string, separated by 1st $IFS character.
( IFS='@'; printf '<< %s >>\n' "${ids[*]// /|}" )
<< a|b|c@d|e|f@g|h|i >>

Note: ${var// /something} will replace every spaces by something, but ${var[*]} will merge array by using only one 1st character:

( IFS='FOO'; printf '<< %s >>\n' "${ids[*]// / BAR }" )
<< a BAR b BAR cFd BAR e BAR fFg BAR h BAR i >>

And yes: by using ${var// / ... }, you could replace 1 space by anthing you want, including more spaces.

3. Array to string, by using printf

As we see, using $IFS is limited to only 1 character. If you need to use more characters to be inserted between your fields. You have to use printf:

ids=("a b" "c d" e\ f)
sep=" long separator "
printf -v string "%s$sep" "${ids[@]}"
echo "${string%$sep}"
a b long separator c d long separator e f

Note: this syntax work but is something limited, see further!

3.1 Array to string, by using printf, into a function

In order to support special characters as % or * in separator, the function have to prevents

  • % to be interpreted by printf (printf '%%' will render a %) and
  • * to be interpreted by parameter expansion, for this $sep have to be double quoted.
printArry() {
    local sep=$1 string
    shift
    printf -v string "%s${sep//%/%%}" "$@"
    echo "${string%"$sep"}"
}        
printArry ' long separator ' "${ids[@]}"
a b long separator c d long separator e f

printArry '*' "${ids[@]}"
a b*c d*e f

printArry '%' "${ids[@]}"
a b%c d%e f

Or to merge array in place.

mergeArry() {
    local sep=$1 string
    local -n _array_to_merge=$2
    printf -v string "%s${sep//%/%%}" "${_array_to_merge[@]}"
    _array_to_merge=("${string%"$sep"}")
}

ids=("a b" "c d" e\ f)
mergeArry ' another separator ' ids
declare -p ids
declare -a ids=([0]="a b another separator c d another separator e f")

ids=("a b" "c d" e\ f)
mergeArry '*' ids
declare -p ids
declare -a ids=([0]="a b*c d*e f")

3.2 Array to string, but submitting array as argument:

Instead of using nameref for array variable, you could use:

MergeWithSep() {
    if [[ $1 == -v ]]; then local outvar=$2 sep=$3 string; shift 3
    else local outvar sep=$1 string; shift
    fi
    printf -v string "%s${sep//%/%%}" "$@"
    if [[ -n $outvar ]]; then
        printf -v $outvar "%s" "${string%"$sep"}"
    else
        echo "${string%"$sep"}"
    fi
}

ids=("a b" "c d" e\ f)
MergeWithSep ' || ' "${ids[@]}"
a b || c d || e f

MergeWithSep $'\n - ' " - "Hello\ world. 'This is a sentence.'
 - Hello world.
 - This is a sentence.

MergeWithSep -v var ', '  {A..Z}
echo $var.
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z.

4. Merge array to string using bash's parameter expansion

Yet another way TMTOWTDI: But as for working, we have to empty $IFS, I prefer to use this in a function for localize $IFS.

printArry () { 
    local -n _array_to_print=$2
    local IFS=
    local _string_to_print="${_array_to_print[*]/#/"$1"}"
    echo "${_string_to_print/#"$1"}"
}

Note you could replace # by % as

  • "${_array_to_merge[*]/#/$1}" will replace begin of string by $1, while
  • "${_array_to_merge[*]/%/$1}" will replace end of string by $1, then
  • "${_array_to_merge/#"$1"}" will replace $1 located at begin of string by nothing, or
  • "${_array_to_merge/%"$1"}" will replace $1 located at end of string by nothing.
mergeArry () { 
    local -n _array_to_merge=$2
    local IFS=
    _array_to_merge=("${_array_to_merge[*]/#/"$1"}")
    _array_to_merge=("${_array_to_merge/#"$1"}")
}

4.1 Little variant, based on length of separator:

printArry () { 
    local -n _array_to_print=$2
    local IFS=
    local _string_to_print="${_array_to_print[*]/#/"$1"}"
    echo "${_string_to_print:${#1}}"
}
mergeArry () { 
    local -n _array_to_merge=$2
    local IFS=
    _array_to_merge=("${_array_to_merge[*]/#/"$1"}")
    _array_to_merge=("${_array_to_merge:${#1}}")
}

Or

printArry () { 
    local -n _array_to_print=$2
    local IFS=
    local _string_to_print="${_array_to_print[*]/%/"$1"}"
    echo "${_string_to_print::-${#1}}"
}

4.1 Then

MergeWithSep() {
    if [[ $1 == -v ]]; then local outvar=$2 sep=$3 string; shift 3
    else local outvar sep=$1 string; shift
    fi
    local IFS=
    string=${@/#/"$sep"}
    if [[ -n $outvar ]]; then
        printf -v outvar %s "${string/#"$sep"}"
    else
        echo "${string/#"$sep"}"
    fi
}

5. Comparison

To do...

See also

Sign up to request clarification or add additional context in comments.

1 Comment

So it's both a typeof and variable substitution error given ${ is expecting a var of type string but receives neither. Thank you for the detailed explanation.
31

You can use printf too, without any external commands or the need to manipulate IFS:

ids=(1 2 3 4)                     # create array
printf -v ids_d '|%s' "${ids[@]}" # yields "|1|2|3|4"
ids_d=${ids_d:1}                  # remove the leading '|'

2 Comments

This was simplest for me, as I had >1 character string to put in between the array elements, and in my case I didn't need to remove the "extra" either. Works delightfully! printf -v strng "'%s',\n" ${thearray[*]} :)
Agree, simplest to use. Additionally, it's also the user-friendlier option if you are using IDE with syntax highlighting, which won't pick up eval'd syntax as in gniourf_gniourf's answer.
17

Your first question is already addressed in F. Hauri's answer. Here's canonical way to join the elements of an array:

ids=( 1 2 3 4 )
IFS=\| eval 'lst="${ids[*]}"'

Some people will cry out loud that eval is evil, yet it's perfectly safe here, thanks to the single quotes. This only has advantages: there are no subshells, IFS is not globally modified, it will not trim trailing newlines, and it's very simple.

Comments

7

An utility function to join arguments array into a delimited string:

#!/usr/bin/env bash

# Join arguments with delimiter
# @Params
# $1: The delimiter string
# ${@:2}: The arguments to join
# @Output
# >&1: The arguments separated by the delimiter string
array::join() {
  (($#)) || return 1 # At least delimiter required
  local -- delim="$1" str IFS=
  shift
  str="${*/#/$delim}" # Expands arguments with prefixed delimiter (Empty IFS)
  printf '%s\n' "${str:${#delim}}" # Echo without first delimiter
}

declare -a my_array=( 'Paris' 'Berlin' 'London' 'Brussel' 'Madrid' 'Oslo' )

array::join ', ' "${my_array[@]}"
array::join '*' {1..9} | bc # 1*2*3*4*5*6*7*8*9=362880 Factorial 9

declare -a null_array=()

array::join '== Ultimate separator of nothing ==' "${null_array[@]}"

Output:

Paris, Berlin, London, Brussel, Madrid, Oslo
362880

Now with Bash 4.2+'s nameref variables, using sub-shells output capture is no longer needed.

#!/usr/bin/env bash

if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[0] < 2)))
then
  printf 'Bash version 4.2 or above required for nameref variables\n' >&2
  exit 1
fi

# Join arguments with delimiter
# @Params
# $1: The variable reference to receive the joined output
# $2: The delimiter string
# ${@:3}: The arguments to join
# @Output
array::join_to() {
  (($# > 1)) || return 1 # At least nameref and delimiter required
  local -n out="$1"
  local -- delim="$2" str IFS=
  shift 2
  str="${*/#/$delim}" # Expands arguments with prefixed delimiter (Empty IFS)
  # shellcheck disable=SC2034 # Nameref variable
  out="${str:${#delim}}" # Discards prefixed delimiter
}

declare -g result1 result2 result3
declare -a my_array=( 'Paris' 'Berlin' 'London' 'Brussel' 'Madrid' 'Oslo' )

array::join_to result1 ', ' "${my_array[@]}"
array::join_to result2 '*' {1..9}
result2=$((result2)) # Expands arythmetic expression

declare -a null_array=()

array::join_to result3 '== Ultimate separator of nothing ==' "${null_array[@]}"

printf '%s\n' "$result1" "$result2" "$result3"

5 Comments

Very handy, thanks! Perhaps join would be a more appropriate name, though, in line with how, e.g., Perl and Python's join functions work?
@TheDudeAbides Could not name it directly join because this conflict with an existing command name that join lines of files.
*facepalm* - ah yes, of course. I forgot about that.
Nice tool! permitting variable length delimiter! But consider using nameref to pass result as a variable in order to prevent forks...
@F.Hauri now with a nameref version

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.