如何将Bash数组的元素合并成一个带分隔符的字符串?

579

如果我在Bash中有这样一个数组:

FOO=( a b c )

如何用逗号连接元素?例如,生成a,b,c

34个回答

766

一个支持多个字符分隔符的100%纯Bash函数是:

function join_by {
  local d=${1-} f=${2-}
  if shift 2; then
    printf %s "$f" "${@/#/$d}"
  fi
}
例如,
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

上面的代码基于@gniourf_gniourf、@AdamKatz、@MattCowell和@x-yuri的思路。它使用选项 errexitset -e)和nounsetset -u)。

或者,一个更简单的函数只支持单个字符分隔符:

function join_by { local IFS="$1"; shift; echo "$*"; }
例如,
join_by , a "b c" d #a,b c,d
join_by / var local tmp #var/local/tmp
join_by , "${FOO[@]}" #a,b,c

这个解决方案基于Pascal Pilz的原始建议。

在此之前提出的解决方案的详细说明可以在"如何在bash脚本中连接()数组元素",由meleu撰写的文章,在dev.to上找到。.


11
用这个函数来处理多字符分隔符:function join { perl -e '$s = shift @ARGV; print join($s, @ARGV);' "$@"; } join ', ' a b c # a, b, c - Daniel Patru
5
有没有办法将这个纯粹使用Bash完成? - CMCDragonkai
5
@puchu 不起作用的是多字符分隔符。说“空格不起作用”会让人觉得使用空格连接不起作用。但实际上是可以的。 - Eric
8
如果将输出存储到变量中,这会促使生成子shell。使用“konsolebox”风格 :) function join { local IFS=$1; __="${*:2}"; }function join { IFS=$1 eval '__="${*:2}"'; }。然后在之后使用__。是的,我正在推广使用__作为结果变量;) (以及常见的迭代变量或临时变量)。如果这个概念传播到一个流行的Bash wiki网站,他们就是抄袭我 :) - konsolebox
7
不要在printf 的格式说明符中放置扩展$d。你可能认为你是安全的,因为你“转义”了%,但是还有其他注意事项:例如分隔符包含反斜杠(例如\n)或者分隔符以连字符开头(也许还有其他我现在想不到的情况)。当然,你可以修复它们(将反斜杠替换为双反斜杠并使用printf -- "$d%s"),但是在某些时候你会感觉你正在与shell作斗争而不是与其合作。这就是为什么在我的下面的答案中,我将分隔符预置到要连接的术语之前的原因。 - gniourf_gniourf
显示剩余27条评论

283

还有另一个解决方案:

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

echo $bar

编辑:同样的问题,但是针对多字符变量长度分隔符:

#!/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

277
$ foo=(a "b c" d)
$ bar=$(IFS=, ; echo "${foo[*]}")
$ echo "$bar"
a,b c,d

22
+1 对于不需要循环、不需要外部命令并且不会对参数字符集施加额外限制的最简解决方案。 - ceving
35
我喜欢这个解决方案,但它仅在IFS只有一个字符时才有效。 - Jayen
14
为什么如果使用@而非*,比如$(IFS=, ; echo "${foo[@]}"),就会导致这个命令不起作用呢?我知道*已经保留了元素中的空格,但不确定为什么,因为通常需要使用@来实现这一点。 - haridsv
17
我找到了上面问题的答案。答案是IFS仅被识别为*。在bash手册页面中,搜索“特殊参数”,并查找与*相邻的解释。 - haridsv
6
关于${foo[@]}${foo[*]}的区别,请参考Shellcheck"Error code SC2145" - David Tonhofer
显示剩余8条评论

78
也许,例如,
SAVE_IFS="$IFS"
IFS=","
FOOJOIN="${FOO[*]}"
IFS="$SAVE_IFS"

echo "$FOOJOIN"

3
如果你这样做,它会认为IFS-是一个变量。你需要执行echo "-${IFS}-"命令(花括号将短横线与变量名称分开)。 - Dennis Williamson
53
话虽如此,这个还是能够运行……所以,就像大多数Bash相关的事情一样,我会假装理解它并继续我的生活。 - David Wolever
6
“-” 不是一个有效的变量名称字符,因此当您使用 $IFS- 时,shell 做出了正确的处理,您不需要使用 ${IFS}-(在 Linux 和 Solaris 上的 bash、ksh、sh 和 zsh 也都一致)。 - Idelic
2
@David 你的echo和Dennis的不同之处在于他使用了双引号。IFS的内容被用作“输入时”声明单词分隔符字符,因此如果没有引号,你总会得到一个空行。 - martin clayton
3
无论你是否使用方括号,Bash 都不会将“-”视为变量名的一部分。 - raphink
显示剩余9条评论

48

不使用任何外部命令:

$ 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

警告:此假设假定元素不包含空格。


16
如果您不想使用中间变量,甚至可以更短地完成:echo ${FOO[@]} | tr ' ' ',' - jesjimher
8
我不理解为什么会有负面评价。与其他发布的方案相比,这个方案更加简洁易读,而且明确警告在存在空格时无法使用。 - jesjimher
这正是我需要的。我需要在逗号后面加上一个空格,所以在BAZ步骤中,通过这个解决方案,我可以做到 /, } - Neil Gaetano Lindberg
我在一个Shell脚本中使用了这个解决方法的变体,将一个基础网络接口与一组VLAN ID连接起来,以创建子接口名称。这是我想出来的代码:DHCP_VLAN="1 10 20 30"; DHCP_IF="eth1"; DHCP_IPFS=${DHCP_IF}.${DHCP_VLAN// / ${DHCP_IF}.} - SirNickity

39

这种简单的单字符分隔符解决方案需要非 POSIX 模式。在POSIX模式中,元素仍然正确地连接在一起,但IFS=, 赋值成为永久性的。

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

使用 #!bash 头部执行的脚本默认在非 POSIX 模式下执行,但为确保脚本在非 POSIX 模式下运行,建议在脚本开头添加 set +o posixshopt -uo posix


对于多字符定界符,建议使用转义和索引技术的 printf 解决方案。

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

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

或者

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

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

此内容基于Riccardo Galli的答案,并应用了我的建议


1
不幸的是,它只适用于单个字符分隔符。 - maoizm

32

这与现有解决方案并没有太大的不同,但它避免了使用单独的函数,不会修改父shell中的IFS,而且所有内容都在一行中:

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

限制:分隔符不能超过一个字符。


这可以简化为只使用

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

此时它基本上与Pascal的答案相同,但是使用printf而不是echo,并将结果打印到stdout而不是分配给变量。


我使用这个函数进行长定界符连接,像这样:printf '%s\n' "$((IFS="⁋"; printf '%s' "${arr[*]}") | sed "s,⁋,LONG DELIMITER,g"))" 被用作替换的占位符,并且可以是任何单个字符,不能出现在数组值中(因此使用不常见的 Unicode 符号)。 - Guss
你可以在子shell中直接使用echo,而不必在那里调用printf。 - Treviño
1
@Treviño,我其实不太记得为什么要使用嵌套的 printf,但我不会将内部的 printf 切换到 echo,以避免使用 echo 带来的歧义。但我可能可以简化为 (IFS=,; printf -- '%s\n' "${arr[*]}") - Benjamin W.
或者我只需添加简化并注明相似之处。 - Benjamin W.
在我看来,这是最好的方法。除非你不需要子shell。只需使用IFS=, printf '%s' "${arr[*]}"即可。 - mattalxndr
显示剩余2条评论

25

这是一个完全使用 Bash 编写的函数,可以完成此任务:

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}"
}

看:

$ 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


$

这样可以保留尾随的换行符,而且不需要使用子shell来获得函数的结果。如果你不喜欢 printf -v(为什么不喜欢呢?)和传递变量名,当然也可以使用全局变量来存储返回的字符串:

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}"
}

2
你的最后一个解决方案非常好,但是可以通过将 join_ret 变成局部变量并在最后输出它来使代码更加简洁。这样就可以像通常的 shell 脚本一样使用 join(),例如 $(join ":" one two three),而不需要全局变量。 - James Sneeringer
1
@JamesSneeringer 我特意使用了这种设计,以避免子shell。在shell脚本中,与许多其他语言不同,以这种方式使用全局变量并不一定是坏事;特别是如果它们在这里有助于避免子shell。此外,$(...)会修剪尾随的换行符;因此,如果数组的最后一个字段包含尾随的换行符,则会被修剪(请参见演示,在我的设计中未修剪)。 - gniourf_gniourf
这适用于多字符分隔符,这让我很开心 ^_^ - spiffytech
回答“为什么不喜欢printf -v?”: 在Bash中,局部变量并不是真正的函数局部变量,因此您可以执行以下操作。(使用局部变量x调用函数f1,该函数又调用修改x的函数f2-在f1的范围内声明为局部变量) 但这并不是局部变量应该工作的方式。如果局部变量确实是局部的(或者假定它们是局部的,例如在必须在bash和ksh上工作的脚本中),那么这将导致整个“通过将值存储在具有此名称的变量中返回值”的方案出现问题。 - tetsujin
这不是100%纯的Bash;你正在调用/usr/bin/printf - Mark Pettit
@MarkPettit 不是的,printf 是 Bash 内置命令。 - gniourf_gniourf

20

我会将数组转化为字符串,然后将空格转换为换行符,并使用paste将所有内容连接成一行,如下所示:

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

结果:

a,b,c

在我看来,这似乎是最快和最干净的方法!


$FOO 只是数组的第一个元素。而且,对于包含空格的数组元素,这种方法会出错。 - Benjamin W.
而不是打印每个元素之间用空字符分隔:printf '%s\0' "${FOO[@]}" | paste -zsd ","。通过这种方式,它支持包含空格和换行符的数组元素。 - mgutt

10

使用 @doesn't matters 的解决方案进行重用,但通过避免 ${:1} 替换和中间变量的需要来实现一条语句。

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

printf在其手册页面中有“格式字符串将根据需要重复使用以满足参数”的说明,因此字符串的连接已经被记录。然后的技巧是使用LIST长度来截取最后一个分隔符,因为cut只会保留LIST的长度作为字段计数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接