573

If I have an array like this in Bash:

FOO=( a b c )

How do I join the elements with commas? For example, producing a,b,c.

miken32
  • 42,008
  • 16
  • 111
  • 154
David Wolever
  • 148,955
  • 89
  • 346
  • 502

34 Answers34

759

A 100% pure Bash function that supports multi-character delimiters is:

function join_by {
  local d=${1-} f=${2-}
  if shift 2; then
    printf %s "$f" "${@/#/$d}"
  fi
}

For example,

join_by , a b c #a,b,c
join_by ' , ' a b c #a , b , c
join_by ')|(' a b c #a)|(b)|(c
join_by ' %s ' a b c #a %s b %s c
join_by $'\n' a b c #a<newline>b<newline>c
join_by - a b c #a-b-c
join_by '\' a b c #a\b\c
join_by '-n' '-e' '-E' '-n' #-e-n-E-n-n
join_by , #
join_by , a #a

The code above is based on the ideas by @gniourf_gniourf, @AdamKatz, @MattCowell, and @x-yuri. It works with options errexit (set -e) and nounset (set -u).

Alternatively, a simpler function that supports only a single character delimiter, would be:

function join_by { local IFS="$1"; shift; echo "$*"; }

For example,

join_by , a "b c" d #a,b c,d
join_by / var local tmp #var/local/tmp
join_by , "${FOO[@]}" #a,b,c

This solution is based on Pascal Pilz's original suggestion.

A detailed explanation of the solutions previously proposed here can be found in "How to join() array elements in a bash script", an article by meleu at dev.to.

Nicholas Sushkin
  • 13,050
  • 3
  • 30
  • 20
  • 11
    Use this for multicharacter separators: function join { perl -e '$s = shift @ARGV; print join($s, @ARGV);' "$@"; } join ', ' a b c # a, b, c – Daniel Patru Jun 19 '14 at 15:06
  • 5
    @dpatru anyway to make that pure bash? – CMCDragonkai Jul 04 '14 at 00:54
  • 5
    @puchu What doesn't work is multi-character separators. To say "space doesn't work" makes it sound like joining with a space doesn't work. It does. – Eric Oct 07 '15 at 18:20
  • 8
    This promotes spawning subshells if storing output to variable. Use `konsolebox` style :) `function join { local IFS=$1; __="${*:2}"; }` or `function join { IFS=$1 eval '__="${*:2}"'; }`. Then use `__` after. Yes, I'm the one promoting use of `__` as a result variable ;) (and a common iteration variable or temporary variable). If the concept gets to a popular Bash wiki site, they copied me :) – konsolebox Nov 05 '15 at 19:43
  • 7
    Don't put the expansion `$d` in the format specifier of `printf`. You think you're safe since you “escaped” the `%` but there are other caveats: when the delimiter contains a backslash (e.g., `\n`) or when the delimiter starts with a hyphen (and maybe others I can't think of now). You can of course fix these (replace backslashes by double backslashes and use `printf -- "$d%s"`), but at some point you'll feel that you're fighting against the shell instead of working with it. That's why, in my answer below, I prepended the delimiter to the terms to be joined. – gniourf_gniourf Feb 03 '16 at 17:06
  • I had a problem using this solution as for some reason it reset the input value for a script run after it. For example I would run . /first_script $1, the . ./second_script $1 and the value passed to the second script would be the last value of the array joined – NSaid Mar 31 '16 at 06:58
  • @NSaid - Are you using the code outside a function definition? Sound like you are shifting the arguments of your script. – Nicholas Sushkin Mar 31 '16 at 19:25
  • @NicholasSushkin I think you are right. I am debugging code again to try and figure it out. – NSaid Apr 01 '16 at 02:24
  • 1
    Can somebody explain what is going on in this piece: printf "%s" "${@/#/$d}". What is "${@/#/$d}"? Why printf "%s" "${array[@]/#/$d}" doesn't work? – facetus Aug 30 '16 at 06:27
  • 1
    note: this seems to work in POSIX sh as well (not just bash). – LLFourn Sep 30 '16 at 11:00
  • We can avoid shelling out by replacing printf with echo: `join_by () { local IFS='' delim=$1; shift; echo -n "$1"; shift; echo -n "${*/#/$delim}"; }` – Stanislav German-Evtushenko Jun 06 '17 at 15:44
  • 1
    Stanislav, like echo, printf is a bash built-in (https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html) – Nicholas Sushkin Jun 06 '17 at 16:21
  • 3
    `join_by() { local d="$1" a="$2"; shift 2; printf %s%s "$a" "${@/#/$d}"; }` is slightly more concise for arbitrary-length delimiters. (Like all the others here) this works in zsh too. – Adam Katz Dec 19 '19 at 17:26
  • Thanks, @AdamKatz, that works in Bash 5.0.9. The `join_by` in the answer strips trailing spaces. – Steven Shaw Feb 17 '20 at 05:26
  • @StevenShaw, I don't think the second version of join_by in the answer strips any spaces. I tested with join_by " - " " a " " b " " c ". Can you give an example? – Nicholas Sushkin Feb 18 '20 at 15:56
  • @NicholasSushkin, you're right. I missed the second version of `join_by` in the answer. – Steven Shaw Feb 18 '20 at 19:55
  • 1
    Be careful when using echo with user input. If the array just contains "-e", "-E", "-n" or any combination of those such as "-neE", then those will be treated as an argument to echo and join_by will not output anything. A better solution would be to use a here string: `join_by() { local IFS="$1"; shift; cat <<< "$*"; }` – Matt Cowell Aug 17 '20 at 18:59
  • @AdamKatz, shift 2 does not work when invoked with only delimiter and no items to join. – Nicholas Sushkin Aug 24 '20 at 14:05
  • 1
    @NicholasSushkin – Correct. I'm not going to put error checking into a code snippet in an SO comment – Adam Katz Aug 24 '20 at 15:29
  • Doesn't work for empty arrays. To make it work: `join_by() { local d=$1; shift; if [ $# -gt 0 ]; then local f=$1; shift; printf %s "$f" "${@/#/$d}"; fi; }`. – x-yuri Dec 30 '20 at 03:41
  • @x-yuri To work with empty arrays, this is a bit simpler: `join_by() { local d="$1" f="$2"; shift 2 && printf %s "$f" "${@/#/$d}"; }`. Of course, if passed an empty array, this fail if `set -e` is set, or it will return an errcode of 1. To avoid that, use: `join_by() { local d="$1" f="$2"; shift 2 || return 0 ; printf %s "$f" "${@/#/$d}"; }` – Ross Smith II Feb 24 '21 at 18:26
  • @RossSmithII Indeed I had `-eu` in mind, because that's what I'm usually using. And even if a script with your code won't terminate, there would be a warning. So I'd say, it wouldn't hurt to add an `if` or something. – x-yuri Feb 24 '21 at 19:58
  • @x-yuri What do you mean by "doesn't work for empty arrays"? Current version seems to work with just the delimiter or with no arguments and it produces no output. – Nicholas Sushkin Mar 06 '21 at 00:56
  • https://gist.github.com/x-yuri/034340109d81003cd6e3d834decd582e – x-yuri Mar 06 '21 at 01:42
  • Join by function doesn't work inside a shell script file (.sh). Is there a way to get it working? – Patlatus Nov 28 '21 at 17:27
  • @Patlatus I don't know which shell you use to interpret your script. This function works in BASH. – Nicholas Sushkin Dec 02 '21 at 16:39
  • @NicholasSushkin I am not sure which shell I use, I just tried to put this code into .sh file on mac and execute it in terminal. If I execute it directly in shell, it seems to be working, but when I put the script into a file and execute the file by command ./file.sh, the script doesn't work – Patlatus Dec 05 '21 at 10:33
  • This function doesn't work for me on Bash 5.2 running on MacOS. – Jon Anderson Jan 19 '23 at 04:11
  • I've just came up with [another solution](https://stackoverflow.com/a/75373857/52499). Feel free to add it to your answer. – x-yuri Feb 07 '23 at 13:13
280

Yet another solution:

#!/bin/bash
foo=('foo bar' 'foo baz' 'bar baz')
bar=$(printf ",%s" "${foo[@]}")
bar=${bar:1}

echo $bar

Edit: same but for multi-character variable length separator:

#!/bin/bash
separator=")|(" # e.g. constructing regex, pray it does not contain %s
foo=('foo bar' 'foo baz' 'bar baz')
regex="$( printf "${separator}%s" "${foo[@]}" )"
regex="${regex:${#separator}}" # remove leading separator
echo "${regex}"
# Prints: foo bar)|(foo baz)|(bar baz
AnyDev
  • 435
  • 5
  • 16
doesn't matters
  • 2,817
  • 2
  • 15
  • 2
268
$ foo=(a "b c" d)
$ bar=$(IFS=, ; echo "${foo[*]}")
$ echo "$bar"
a,b c,d
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Pascal Pilz
  • 2,697
  • 1
  • 13
  • 2
  • 3
    The outer double quotes and the double quotes around the colon are not necessary. Only the inner double quotes are necessary: `bar=$( IFS=, ; echo "${foo[*]}" )` – ceving Sep 11 '12 at 09:52
  • 22
    +1 for the most compact solution which does not need loops, which does not need external commands and which does not impose additional restrictions on the character set of the arguments. – ceving Sep 11 '12 at 10:04
  • 35
    i like the solution, but it only works if IFS is one character – Jayen May 29 '13 at 01:47
  • 14
    Any idea why this doesn't work if using `@` instead of `*`, as in `$(IFS=, ; echo "${foo[@]}")` ? I can see that the `*` already preserves the whitespace in the elements, again not sure how, since `@` is usually required for this sake. – haridsv Apr 15 '14 at 07:33
  • 17
    I found the answer for my own question above. The answer is that IFS is only recognized for `*`. In bash man page, search for "Special Parameters" and look for the explanation next to `*`: – haridsv Apr 16 '14 at 06:26
  • 6
    On the `"${foo[@]}"` vs `"${foo[*]}"` see also ["Error code SC2145"](https://github.com/koalaman/shellcheck/wiki/SC2145) of [Shellcheck](https://www.shellcheck.net/). – David Tonhofer Oct 28 '17 at 13:12
  • @Jayen or better to say, the solution only works with **one** or **zero** arguments. – VasiliNovikov Aug 30 '19 at 14:31
  • 2
    If you need delimiter with multiple characters, you can pipe to sed: `... | sed 's/,/ , /g'` – wisbucky Nov 25 '19 at 23:48
  • 2
    IMO, the best solution (for the single-character delimiter case), as it's conceptually the simplest. – Marcus Dec 05 '19 at 10:07
  • How can I get an output with double quotes? "a","b c","d" – tot Nov 29 '22 at 22:24
  • 1
    I think you might want to first save the old value of IFS and set it back to that so you don't alter the default. Using local might work and keep it in one line. – Kaleb Coberly Jan 27 '23 at 20:16
  • 1
    This one uses a lot of magic that get confused when nested! Still a nice solution to the problem. – Samuel Åslund Feb 03 '23 at 15:52
  • 1
    @KalebCoberly You don't need to save `IFS` here, since it's called in the `$()` scope. Very elegant solution. – Marco Sulla Feb 14 '23 at 14:28
78

Maybe, e.g.,

SAVE_IFS="$IFS"
IFS=","
FOOJOIN="${FOO[*]}"
IFS="$SAVE_IFS"

echo "$FOOJOIN"
Fritz
  • 1,293
  • 15
  • 27
martin clayton
  • 76,436
  • 32
  • 213
  • 198
  • 1
    Hrm… For some reason, my IFS doesn't want to change: $ IFS=","; $ echo -$IFS- ==> - - – David Wolever Oct 06 '09 at 18:08
  • 3
    If you do that, it thinks that IFS- is the variable. You have to do `echo "-${IFS}-"` (the curly braces separate the dashes from the variable name). – Dennis Williamson Oct 06 '09 at 18:57
  • 1
    Still got the same result (I just put the dashes in to illustrate the point… `echo $IFS` does the same thing. – David Wolever Oct 06 '09 at 19:59
  • 53
    That said, this still seems to work… So, like most things with Bash, I'll pretend like I understand it and get on with my life. – David Wolever Oct 06 '09 at 20:06
  • 6
    A "-" is not a valid character for a variable name, so the shell does the right thing when you use $IFS-, you don't need ${IFS}- (bash, ksh, sh and zsh in linux and solaris also agree). – Idelic Oct 06 '09 at 21:21
  • 2
    @David the difference between your echo and Dennis's is that he has used double quoting. The content of IFS is used 'on input' as a declaration of word-separator characters - so you'll always get an empty line without quotes. – martin clayton Oct 06 '09 at 21:52
  • 3
    @DennisWilliamson: bash doesn't consider `-` as part of a variable name, whether you use brackets or not. – raphink Jan 20 '12 at 11:22
  • @Raphink: My error. Actually, the problem with the snippet in the first comment by @DavidWolever is that the variable isn't quoted. `IFS=","; echo "-$IFS-"` works correctly. All this was covered in the earlier comments. – Dennis Williamson Jan 20 '12 at 15:11
  • @DennisWilliamson Yes indeed. – raphink Jan 20 '12 at 16:23
  • Because it caught me out, I'd like to add my $0.02: the important step is to unset $IFS before printing $FOOJOIN If you fail to do so, you still get whitespace separation. – sanmiguel Jan 26 '12 at 13:51
  • @sanmiguel - I can't reproduce what you said. I did: `foo=(a b c); OLD_IFS=$IFS; IFS=","; foojoin="${foo[*]}"; IFS=$OLD_IFS; echo $foojoin;` and got `a,b,c`. Am I missing something? I don't think you'd ever want to UNSET the $IFS, perhaps you meant RESET, but as shown above, I can't reproduce what you claim even in that scenario. – dwanderson Feb 05 '15 at 16:23
  • @dwanderson you're right - I meant *reset*. If you skip the `IFS=$OLD_IFS` step, `echo $foojoin` will give you `a b c`. – sanmiguel Mar 04 '15 at 12:57
  • 1
    For the confused, like me, `IFS` is a Bash internal variable _internal field seperator_, so this script just temporarily changes it to `,` before evaluating and assigning the `FOO` array to `FOOJOIN` – TonyH May 24 '17 at 19:38
  • Note that this works only for a **single** character delimiter. But it's a very elegant solution :) – famzah Mar 20 '22 at 13:56
48

Using no external commands:

$ FOO=( a b c )     # initialize the array
$ BAR=${FOO[@]}     # create a space delimited string from array
$ BAZ=${BAR// /,}   # use parameter expansion to substitute spaces with comma
$ echo $BAZ
a,b,c

Warning, it assumes elements don't have whitespaces.

codeforester
  • 39,467
  • 16
  • 112
  • 140
Nil Geisweiller
  • 4,377
  • 1
  • 16
  • 6
  • 16
    If you don't want to use an intermediate variable, it can be done even shorter: `echo ${FOO[@]} | tr ' ' ','` – jesjimher May 24 '16 at 09:31
  • 8
    I don't understand the negative votes. It's a much compact and readable solution than others posted here, and it's clearly warned that it doesn't work when there're spaces. – jesjimher May 24 '16 at 09:32
  • This is what I needed. I had the need to have a space after comma, so in the BAZ step, with this solution, I could do that `/, }` – Neil Gaetano Lindberg Sep 30 '22 at 15:16
  • I used a riff on this solution in a shell script to join a base network interface with a list of VLAN IDs to create subinterface names. Here's what I came up with: DHCP_VLAN="1 10 20 30"; DHCP_IF="eth1"; DHCP_IPFS=${DHCP_IF}.${DHCP_VLAN// / ${DHCP_IF}.} – SirNickity Jul 05 '23 at 22:04
39

This simple single-character delimiter solution requires non-POSIX mode. In POSIX mode, the elements are still joined properly, but the IFS=, assignment becomes permanent.

IFS=, eval 'joined="${foo[*]}"'

A script executed with the #!bash header respected executes in non-POSIX mode by default, but to help make sure a script runs in non-POSIX mode, add set +o posix or shopt -uo posix at the beginning of the script.


For multi-character delimiters, I recommend using a printf solution with escaping and indexing techniques.

function join {
    local __sep=${2-} __temp
    printf -v __temp "${__sep//%/%%}%s" "${@:3}"
    printf -v "$1" %s "${__temp:${#__sep}}"
}

join joined ', ' "${foo[@]}"

Or

function join {
    printf -v __ "${1//%/%%}%s" "${@:2}"
    __=${__:${#1}}
}

join ', ' "${foo[@]}"
joined=$__

This is based on Riccardo Galli's answer with my suggestion applied.

konsolebox
  • 72,135
  • 12
  • 99
  • 105
32

This isn't all too different from existing solutions, but it avoids using a separate function, doesn't modify IFS in the parent shell and is all in a single line:

arr=(a b c)
printf '%s\n' "$(IFS=,; printf '%s' "${arr[*]}")"

resulting in

a,b,c

Limitation: the separator can't be longer than one character.


This could be simplified to just

(IFS=,; printf '%s' "${arr[*]}")

at which point it's basically the same as Pascal's answer, but using printf instead of echo, and printing the result to stdout instead of assigning it to a variable.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
  • I'm using this for join with a long delimiter, like this: `printf '%s\n' "$((IFS="⁋"; printf '%s' "${arr[*]}") | sed "s,⁋,LONG DELIMITER,g"))"`. The `⁋` is used as placeholder for replacement and can be any single character that can't occur in the array value (hence the uncommon unicode glyph). – Guss Oct 26 '20 at 12:06
  • You can just use `echo` in the subshell, without having to invoke printf there – Treviño Apr 26 '21 at 16:10
  • 1
    @Treviño I actually don't remember exactly why I used nested `printf`s, but I wouldn't switch the inner one to `echo` to avoid the ambiguities that come with using `echo` – but I could probably simplify to `(IFS=,; printf -- '%s\n' "${arr[*]}")` – Benjamin W. Apr 26 '21 at 16:30
  • I'm actually going to leave it as is – if I made the change, it would basically become a copy of [Pascal's answer](https://stackoverflow.com/a/9429887/3266847), with the only difference being `printf` vs. `echo`. – Benjamin W. Apr 27 '21 at 14:09
  • Or I'll just add the simplification and note the similarity. – Benjamin W. Apr 27 '21 at 14:10
  • This is the best one in my opinion. Except you don't need a subshell. Just `IFS=, printf '%s' "${arr[*]}"` – mattalxndr Aug 01 '21 at 23:50
  • @mattalxndr That doesn't work; setting `IFS` must happen in a separate command in this case, hence the subshell and `;` between the commands. – Benjamin W. Aug 02 '21 at 00:03
25

Here's a 100% pure Bash function that does the job:

join() {
    # $1 is return variable name
    # $2 is sep
    # $3... are the elements to join
    local retname=$1 sep=$2 ret=$3
    shift 3 || shift $(($#))
    printf -v "$retname" "%s" "$ret${@/#/$sep}"
}

Look:

$ a=( one two "three three" four five )
$ join joineda " and " "${a[@]}"
$ echo "$joineda"
one and two and three three and four and five
$ join joinedb randomsep "only one element"
$ echo "$joinedb"
only one element
$ join joinedc randomsep
$ echo "$joinedc"

$ a=( $' stuff with\nnewlines\n' $'and trailing newlines\n\n' )
$ join joineda $'a sep with\nnewlines\n' "${a[@]}"
$ echo "$joineda"
 stuff with
newlines
a sep with
newlines
and trailing newlines


$

This preserves even the trailing newlines, and doesn't need a subshell to get the result of the function. If you don't like the printf -v (why wouldn't you like it?) and passing a variable name, you can of course use a global variable for the returned string:

join() {
    # $1 is sep
    # $2... are the elements to join
    # return is in global variable join_ret
    local sep=$1 IFS=
    join_ret=$2
    shift 2 || shift $(($#))
    join_ret+="${*/#/$sep}"
}
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • 2
    Your last solution very good, but could be made cleaner by making `join_ret` a local variable, and then echoing it at the end. This allows join() to be used in the usual shell scripting way, e.g. `$(join ":" one two three)`, and doesn't require a global variable. – James Sneeringer Mar 27 '15 at 18:27
  • 1
    @JamesSneeringer I purposely used this design so as to avoid subshells. In shell scripting, unlike in many other languages, global variables used that way are not necessarily a bad thing; especially if they are here to help avoiding subshells. Moreover, `$(...)` trims trailing newlines; so if the last field of the array contains trailing newlines, these would be trimmed (see demo where they are not trimmed with my design). – gniourf_gniourf Mar 27 '15 at 18:47
  • This works with multi-character separators, which makes me happy ^_^ – spiffytech Nov 18 '15 at 03:53
  • To address the "why wouldn't you like printf -v?": In Bash, local variables aren't truly function-local, so you can do things like this. (Call function f1 with local variable x, which in turn calls function f2 which modifies x - which is declared local within f1's scope) But it's not really how local variables ought to work. If local variables really are local (or are assumed to be, for instance in a script that must work on both bash and ksh) then it causes problems with this whole "return a value by storing it in the variable with this name" scheme. – tetsujin Sep 06 '16 at 18:40
  • It's not 100% pure bash; you're calling `/usr/bin/printf`. – Mark Pettit Jul 17 '20 at 21:11
  • @MarkPettit Nope, `printf` is a Bash builtin. – gniourf_gniourf Sep 12 '20 at 09:50
20

I would echo the array as a string, then transform the spaces into line feeds, and then use paste to join everything in one line like so:

tr " " "\n" <<< "$FOO" | paste -sd , -

Results:

a,b,c

This seems to be the quickest and cleanest to me !

Yanick Girouard
  • 4,711
  • 4
  • 19
  • 26
  • `$FOO` is just the first element of the array, though. Also, this breaks for array elements containing spaces. – Benjamin W. Jan 20 '19 at 20:44
  • Instead print every element separated by a null character: `printf '%s\0' "${FOO[@]}" | paste -zsd ","` By that it supports array elements which contain spaces and new lines. – mgutt Jul 28 '23 at 11:08
10

With re-use of @doesn't matters' solution, but with a one statement by avoiding the ${:1} substition and need of an intermediary variable.

echo $(printf "%s," "${LIST[@]}" | cut -d "," -f 1-${#LIST[@]} )

printf has 'The format string is reused as often as necessary to satisfy the arguments.' in its man pages, so that the concatenations of the strings is documented. Then the trick is to use the LIST length to chop the last sperator, since cut will retain only the lenght of LIST as fields count.

Valise
  • 117
  • 1
  • 3
9

x=${arr[*]// /,}

This is the shortest way to do it.

Example,

# ZSH:
arr=(1 "2 3" 4 5)
x=${"${arr[*]}"// /,}
echo $x  # output: 1,2,3,4,5

# ZSH/BASH:
arr=(1 "2 3" 4 5)
a=${arr[*]}
x=${a// /,}
echo $x  # output: 1,2,3,4,5
user31986
  • 1,558
  • 1
  • 14
  • 29
  • 4
    This does not work correctly for string with spaces:`t=(a "b c" d); echo ${t[2]} (prints "b c"); echo ${"${t[*]}"// /,} (prints a,b,c,d) – kounoupis Feb 14 '19 at 18:31
  • 1
    Non-array application (for a space-delimted string): ```RESULT=$(echo "${INPUT// /,")``` This also works with multi-char delimiters. – Akom Jun 07 '21 at 15:49
  • 1
    Note that there is a syntax error in the bash version (at least when I tested on Mac). When I print `x` I get `1[*]`, which is not desired. To fix the second line needs to be wrapped with braces, like `a=${arr[*]}` – rv.kvetch Sep 15 '21 at 16:24
7
s=$(IFS=, eval 'echo "${FOO[*]}"')
Zombo
  • 1
  • 62
  • 391
  • 407
eel ghEEz
  • 1,186
  • 11
  • 24
  • 10
    You should flesh out your answer. – joce Mar 26 '13 at 01:45
  • The very best one. Thanks!! – peter pan gz Sep 09 '16 at 08:26
  • 6
    I wish I could downvote this answer because it opens a security hole and because it will destroy spaces in elements. – eel ghEEz Dec 16 '16 at 00:56
  • I recommend @Riccardo Galli's answer using printf's automatic concatenation of arguments. http://stackoverflow.com/a/16937679/80772 – eel ghEEz Dec 16 '16 at 00:58
  • @eel ghEEz not from my observation on bash 4.4.5: ```foo=(a b 'c d') ; IFS=, eval 'echo "${foo[*]}"'``` results in ```a,b,c d``` – bxm Jan 18 '19 at 11:19
  • 1
    @bxm indeed, it seems to preserve spaces and it does not allow escaping from the echo arguments context. I figured that adding `@Q` could escape the joined values from misinterpreting when they have a joiner in them: `foo=("a ," "b ' ' c" "' 'd e" "f " ";" "ls -latr"); s=$(IFS=, eval 'echo "${foo[*]@Q}"'); echo "${s}"` outputs `'a ,','b '\'' '\'' c',''\'' '\''d e','f ',';','ls -latr '` – eel ghEEz Jan 18 '19 at 17:12
  • 1
    Avoid solutions that make use of subshells unless necessary. – konsolebox Sep 25 '19 at 00:28
7

printf solution that accept separators of any length (based on @doesn't matters answer)

#/!bin/bash
foo=('foo bar' 'foo baz' 'bar baz')

sep=',' # can be of any length
bar=$(printf "${sep}%s" "${foo[@]}")
bar=${bar:${#sep}}

echo $bar
Riccardo Galli
  • 12,419
  • 6
  • 64
  • 62
  • This produces output with a leading comma. – Mark Renouf Aug 21 '13 at 15:13
  • The last bar=${bar:${#sep}} removes the separator. I just copy and pasted in a bash shell and it does work. What shell are you using? – Riccardo Galli Aug 22 '13 at 14:57
  • 3
    Any `printf` _format specifier_ (eg. `%s` unintentionally in `$sep` will cause problems. – Peter.O May 22 '15 at 17:55
  • 2
    `sep` can be sanitized with `${sep//\%/%%}`. I like your solution better than `${bar#${sep}}` or `${bar%${sep}}` (alternative). This is nice if converted to a function that stores result to a generic variable like `__`, and not `echo` it. – konsolebox Sep 25 '19 at 00:39
  • `function join_by { printf -v __ "${1//\%/%%}%s" "${@:2}"; __=${__:${#1}}; }` – konsolebox Sep 25 '19 at 00:57
7

Shorter version of top answer:

joinStrings() { local a=("${@:3}"); printf "%s" "$2${a[@]/#/$1}"; }

Usage:

joinStrings "$myDelimiter" "${myArray[@]}"
Camilo Martin
  • 37,236
  • 20
  • 111
  • 154
  • 1
    A longer version, but no need to make a copy of a slice of the arguments to an array variable: `join_strings () { local d="$1"; echo -n "$2"; shift 2 && printf '%s' "${@/#/$d}"; }` – Rockallite Feb 12 '17 at 07:22
  • Yet Another Version: `join_strings () { local d="$1"; echo -n "$2"; shift 2 && printf '$d%s' "${@}"; }` This works with usage: `join_strings 'delim' "${array[@]}"` or unquoted: `join_strings 'delim' ${array[@]}` – Cometsong Sep 06 '18 at 15:25
7

Thanks @gniourf_gniourf for detailed comments on my combination of best worlds so far. Sorry for posting code not thoroughly designed and tested. Here is a better try.

# join with separator
join_ws() { local d=$1 s=$2; shift 2 && printf %s "$s${@/#/$d}"; }

This beauty by conception is

  • (still) 100% pure bash ( thanks for explicitly pointing out that printf is a builtin as well. I wasn't aware about this before ... )
  • works with multi-character delimiters
  • more compact and more complete and this time carefully thought over and long-term stress-tested with random substrings from shell scripts amongst others, covering use of shell special characters or control characters or no characters in both separator and / or parameters, and edge cases, and corner cases and other quibbles like no arguments at all. That doesn't guarantee there is no more bug, but it will be a little harder challenge to find one. BTW, even the currently top voted answers and related suffer from such things like that -e bug ...

Additional examples:

$ join_ws '' a b c
abc
$ join_ws ':' {1,7}{A..C}
1A:1B:1C:7A:7B:7C
$ join_ws -e -e
-e
$ join_ws $'\033[F' $'\n\n\n'  1.  2.  3.  $'\n\n\n\n'
3.
2.
1.
$ join_ws $ 
$
guest
  • 71
  • 1
  • 1
4
$ set a 'b c' d

$ history -p "$@" | paste -sd,
a,b c,d
Zombo
  • 1
  • 62
  • 391
  • 407
4

Combine best of all worlds so far with following idea.

# join with separator
join_ws()  { local IFS=; local s="${*/#/$1}"; echo "${s#"$1$1$1"}"; }

This little masterpiece is

  • 100% pure bash ( parameter expansion with IFS temporarily unset, no external calls, no printf ... )
  • compact, complete and flawless ( works with single- and multi-character limiters, works with limiters containing white space, line breaks and other shell special characters, works with empty delimiter )
  • efficient ( no subshell, no array copy )
  • simple and stupid and, to a certain degree, beautiful and instructive as well

Examples:

$ join_ws , a b c
a,b,c
$ join_ws '' a b c
abc
$ join_ws $'\n' a b c
a
b
c
$ join_ws ' \/ ' A B C
A \/ B \/ C
guest
  • 41
  • 1
  • 5
    Not that nice: at least 2 problems: 1. `join_ws ,` (with no arguments) wrongly outputs `,,`. 2. `join_ws , -e` wrongly outputs nothing (that's because you're wrongly using `echo` instead of `printf`). I actually don't know why you advertised the use of `echo` instead of `printf`: `echo` is notoriously broken, and `printf` is a robust builtin. – gniourf_gniourf Mar 02 '18 at 21:02
3

Here's a single liner that is a bit weird but works well for multi-character delimiters and supports any value (including containing spaces or anything):

ar=(abc "foo bar" 456)
delim=" | "
printf "%s\n$delim\n" "${ar[@]}" | head -n-1 | paste -sd ''

This would show in the console as

abc | foo bar | 456

Note: Notice how some solutions use printf with ${ar[*]} and some with ${ar[@]}?

The ones with @ use the printf feature that supports multiple arguments by repeating the format template.

The ones with * should not be used. They do not actually need printfand rely on manipulating the field separator and bash's word expansion. These would work just as well with echo, cat, etc. - these solutions likely use printf because the author doesn't really understand what they are doing...

Potherca
  • 13,207
  • 5
  • 76
  • 94
Guss
  • 30,470
  • 17
  • 104
  • 128
  • I believe the solutions use `printf` because [printf is better than echo](https://unix.stackexchange.com/a/65819/17945). Also, `head -n-1` is not portable (does not work on macOS, for example). – Old Pro Oct 26 '22 at 06:44
  • @OldPro: I'm not complaining about the use of `printf` - I use it myself. I'm complaining about the use of `${ar[*]}` that doesn't make any sense. I would also contend that `echo`, while not better, is often sufficient for simple output, especially if you don't want to bother with specifying the new line character every output. – Guss Oct 26 '22 at 10:20
  • @OldPro: regarding `head -n-1`, yes - it isn't portable to Mac. It is also isn't portable to SunOS or AIX or any of the non-modern UN*X that don't support GNU. Unlike those though, on Mac you can `brew install coreutils` to get a compatible `ghead`, or do any of the other things mentioned here: https://superuser.com/q/543950/10942 – Guss Oct 26 '22 at 10:36
3

I believe this is the shortest solution, as Benamin W. already mentioned:

(IFS=,; printf %s "${a[*]}")

Wanted to add that if you use zsh, you can drop the subshell:

IFS=, printf %s "${a[*]}"

Test:

a=(1 'a b' 3)
IFS=, printf %s "${a[*]}"
1,a b,3
mattalxndr
  • 9,143
  • 8
  • 56
  • 87
2

My attempt.

$ array=(one two "three four" five)
$ echo "${array[0]}$(printf " SEP %s" "${array[@]:1}")"
one SEP two SEP three four SEP five
Ben Davis
  • 13,112
  • 10
  • 50
  • 65
1

Right now I'm using:

TO_IGNORE=(
    E201 # Whitespace after '('
    E301 # Expected N blank lines, found M
    E303 # Too many blank lines (pep8 gets confused by comments)
)
ARGS="--ignore `echo ${TO_IGNORE[@]} | tr ' ' ','`"

Which works, but (in the general case) will break horribly if array elements have a space in them.

(For those interested, this is a wrapper script around pep8.py)

David Wolever
  • 148,955
  • 89
  • 346
  • 502
  • from where do you get those array values? if you are hardcoding it like that, why not just foo="a,b,c".? – ghostdog74 Oct 06 '09 at 23:55
  • In this case I actually *am* hard-coding the values, but I want to put them in an array so I can comment on each individually. I've updated the answer to show you what I mean. – David Wolever Oct 07 '09 at 05:11
  • Assuming you are actually using bash, this might work better: `ARGS="--ignore $(echo "${TO_IGNORE[@]}" | tr ' ' ',')"`. Operator `$()` is more powerful than backtics (allows nesting of `$()` and `""`). Wrapping `${TO_IGNORE[@]}` with double quotes should also help. – kevinarpe Nov 08 '13 at 16:58
1

In case the elements you want to join is not an array just a space separated string, you can do something like this:

foo="aa bb cc dd"
bar=`for i in $foo; do printf ",'%s'" $i; done`
bar=${bar:1}
echo $bar
    'aa','bb','cc','dd'

for example, my use case is that some strings are passed in my shell script and I need to use this to run on a SQL query:

./my_script "aa bb cc dd"

In my_script, I need to do "SELECT * FROM table WHERE name IN ('aa','bb','cc','dd'). Then above command will be useful.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
Dexin Wang
  • 477
  • 4
  • 6
1

Use perl for multicharacter separators:

function join {
   perl -e '$s = shift @ARGV; print join($s, @ARGV);' "$@"; 
}

join ', ' a b c # a, b, c

Or in one line:

perl -le 'print join(shift, @ARGV);' ', ' 1 2 3
1, 2, 3
Daniel Patru
  • 1,968
  • 18
  • 15
  • works for me, although the `join` name conflicts with some crap on `OS X`.. i'd call it `conjoined`, or maybe [`jackie_joyner_kersee`](http://1.bp.blogspot.com/-p4VycNmiWvo/UBQejj-_pII/AAAAAAAAFWg/aVgjsUEKu5c/s1600/3.jpg)? – Alex Gray Jul 10 '15 at 14:51
1

If you build the array in a loop, here is a simple way:

arr=()
for x in $(some_cmd); do
   arr+=($x,)
done
arr[-1]=${arr[-1]%,}
echo ${arr[*]}
Ian Kelling
  • 9,643
  • 9
  • 35
  • 39
1

Using variable indirection to refer directly to an array also works. Named references can also be used, but they only became available in 4.3.

The advantage of using this form of a function is that you can have the separator optional (defaults to the first character of default IFS, which is a space; perhaps make it an empty string if you like), and it avoids expanding values twice (first when passed as parameters, and second as "$@" inside the function).

This solution also doesn't require the user to call the function inside a command substitution - which summons a subshell, to get a joined version of a string assigned to another variable.

function join_by_ref {
    __=
    local __r=$1[@] __s=${2-' '}
    printf -v __ "${__s//\%/%%}%s" "${!__r}"
    __=${__:${#__s}}
}

array=(1 2 3 4)

join_by_ref array
echo "$__" # Prints '1 2 3 4'.

join_by_ref array '%s'
echo "$__" # Prints '1%s2%s3%s4'.

join_by_ref 'invalid*' '%s' # Bash 4.4 shows "invalid*[@]: bad substitution".
echo "$__" # Prints nothing but newline.

Feel free to use a more comfortable name for the function.

This works from 3.1 to 5.0-alpha. As observed, variable indirection doesn't only work with variables but with other parameters as well.

A parameter is an entity that stores values. It can be a name, a number, or one of the special characters listed below under Special Parameters. A variable is a parameter denoted by a name.

Arrays and array elements are also parameters (entities that store value), and references to arrays are technically references to parameters as well. And much like the special parameter @, array[@] also makes a valid reference.

Altered or selective forms of expansion (like substring expansion) that deviate reference from the parameter itself no longer work.

Update

In the release version of Bash 5.0, variable indirection is already called indirect expansion and its behavior is already explicitly documented in the manual:

If the first character of parameter is an exclamation point (!), and parameter is not a nameref, it introduces a level of indirection. Bash uses the value formed by expanding the rest of parameter as the new parameter; this is then expanded and that value is used in the rest of the expansion, rather than the expansion of the original parameter. This is known as indirect expansion.

Taking note that in the documentation of ${parameter}, parameter is referred to as "a shell parameter as described (in) PARAMETERS or an array reference". And in the documentation of arrays, it is mentioned that "Any element of an array may be referenced using ${name[subscript]}". This makes __r[@] an array reference.

Join by arguments version

See my comment in Riccardo Galli's answer.

konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • 2
    Is there a specific reason to use `__` as a variable name? Makes the code really unreadable. – PesaThe Jul 15 '18 at 17:34
  • @PesaThe It's just a preference. I prefer using generic names for a return variable. Other non-generic names attribute themselves to specific functions, and it requires memorization. Calling multiple functions that return values on different variables can make the code less easy to follow. Using a generic name would force the scripter to transfer the value from the return variable to the proper variable to avoid conflict, and it makes the code end up being more readable since it becomes explicit where the returned values go. I make few exceptions to that rule though. – konsolebox Jul 15 '18 at 23:34
  • I'm unable to get the code to work. I'm using `5.0.16(1)-release`, and when I try to invoke the function, I get no output. – Mark Pettit Jul 17 '20 at 21:07
  • @MarkPettit The function is not meant to produce an output. It stores the value to __. However, the first four commands do. Tested on 5.1.0(1)-alpha. – konsolebox Aug 04 '20 at 12:35
1

Perhaps late for the party, but this works for me:

function joinArray() {
  local delimiter="${1}"
  local output="${2}"
  for param in ${@:3}; do
    output="${output}${delimiter}${param}"
  done

  echo "${output}"
}
TacB0sS
  • 10,106
  • 12
  • 75
  • 118
1

Many, if not most, of these solutions rely on arcane syntax, brain-busting regex tricks, or calls to external executables. I would like to propose a simple, bash-only solution that is very easy to understand, and only slightly sub-optimal, performance-wise.

join_by () {
    # Argument #1 is the separator. It can be multi-character.
    # Argument #2, 3, and so on, are the elements to be joined.
    # Usage: join_by ", " "${array[@]}"
    local SEPARATOR="$1"
    shift

    local F=0
    for x in "$@"
    do
        if [[ F -eq 1 ]]
        then
            echo -n "$SEPARATOR"
        else
            F=1
        fi
        echo -n "$x"
    done
    echo
}

Example:

$ a=( 1 "2 2" 3 )
$ join_by ", " "${a[@]}"
1, 2 2, 3
$ 

I'd like to point out that any solution that uses /usr/bin/[ or /usr/bin/printf is inherently slower than my solution, since I use 100% pure bash. As an example of its performance, Here's a demo where I create an array with 1,000,000 random integers, then join them all with a comma, and time it.

$ eval $(echo -n "a=("; x=0 ; while [[ x -lt 1000000 ]]; do echo -n " $RANDOM" ; x=$((x+1)); done; echo " )")
$ time join_by , ${a[@]} >/dev/null
real    0m8.590s
user    0m8.591s
sys     0m0.000s
$ 
Mark Pettit
  • 141
  • 6
1

This one particularly works with busybox's sh and $@:

$ FOO=(a b c)
$ printf '%s\n' "${FOO[@]}" | paste -sd,
a,b,c

Or:

join_by() {
    local d=$1
    shift
    printf '%s\n' "$@" | paste -sd "$d"
}
join_by , "${FOO[@]}"  # a,b,c
x-yuri
  • 16,722
  • 15
  • 114
  • 161
0

This approach takes care of spaces within the values, but requires a loop:

#!/bin/bash

FOO=( a b c )
BAR=""

for index in ${!FOO[*]}
do
    BAR="$BAR,${FOO[$index]}"
done
echo ${BAR:1}
dengel
  • 305
  • 5
  • 8
0

Perhaps I'm missing something obvious, since I'm a newb to the whole bash/zsh thing, but it looks to me like you don't need to use printf at all. Nor does it get really ugly to do without.

join() {
  separator=$1
  arr=$*
  arr=${arr:2} # throw away separator and following space
  arr=${arr// /$separator}
}

At least, it has worked for me thus far without issue.

For instance, join \| *.sh, which, let's say I'm in my ~ directory, outputs utilities.sh|play.sh|foobar.sh. Good enough for me.

EDIT: This is basically Nil Geisweiller's answer, but generalized into a function.

Community
  • 1
  • 1
Jordan
  • 166
  • 7
0

Here's one that most POSIX compatible shells support:

join_by() {
    # Usage:  join_by "||" a b c d
    local arg arr=() sep="$1"
    shift
    for arg in "$@"; do
        if [ 0 -lt "${#arr[@]}" ]; then
            arr+=("${sep}")
        fi
        arr+=("${arg}") || break
    done
    printf "%s" "${arr[@]}"
}
user541686
  • 205,094
  • 128
  • 528
  • 886
  • 1
    It’s fine Bash code, but POSIX [doesn’t have](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html) arrays (or `local`) at all. – Anders Kaseorg Jan 16 '19 at 03:59
  • @Anders: Yeah I learned this the hard way very recently :( I'll leave it up for now though since most POSIX compatible shells do seem to support arrays. – user541686 Jan 16 '19 at 07:37
0

My personal favorite, utilize scoping rule of bash.
No forking, no word splitting or other gotchas. By return value to caller by modify agreed variable, eg, x,

jointox() {
x="$2"
for (( i=3; i <= $#; i++ )); do
    x="$x$1${!i}"
done
}

How to use, the caller preferably

  • set local variable x
  • call jointox with delimiter and arguments
# demonstration of caller
x() {
sep=,
local x
jointox "$sep" "$@"
}
$ x a "b c" d
a,b c,d

Another more practical use case

some_complex_stuff() {
# ...
# build arr from input or dependents, then join with \|
local x
jointox '\|' "${arr[@]}"
# ...
}

How it works. In bash, by default variable are global, but global in bash is not true global in common programming language sense like in python/js/etc.

Global variable (the default) only upto nearest callers that set it local, or true global if non of them set is as local.

For example,

- f -> g -> h -> j -> k, 
- only g mark x as local, 
- if any h/j/k modify x, 
- g see it modified, but not f
qeatzy
  • 1,363
  • 14
  • 21
-2
awk -v sep=. 'BEGIN{ORS=OFS="";for(i=1;i<ARGC;i++){print ARGV[i],ARGC-i-1?sep:""}}' "${arr[@]}"

or

$ a=(1 "a b" 3)
$ b=$(IFS=, ; echo "${a[*]}")
$ echo $b
1,a b,3
Meow
  • 4,341
  • 1
  • 18
  • 17
-2
liststr=""
for item in list
do
    liststr=$item,$liststr
done
LEN=`expr length $liststr`
LEN=`expr $LEN - 1`
liststr=${liststr:0:$LEN}

This takes care of the extra comma at the end also. I am no bash expert. Just my 2c, since this is more elementary and understandable

byte_array
  • 2,767
  • 1
  • 16
  • 10