如何将一个数组转换为逗号分隔的字符串?

43
我有一个数组,我想像这样打印它:

I have an array and I am printing it like this:

echo "${data[*]}"

输出:

/QE-CI-RUN-71/workspace/QE-AU/57/testng-results_1.xml 
/QE-CI-RUN-71/workspace/QE-AU/57/testng-results_2.xml

我想将上述输出作为逗号分隔值存储。如何在Bash中实现这一点?

数据数组是动态的,它可能有任意数量的值。


3
echo "${data[*]}" 不应该生成换行符,除非它们存在于值本身内。你能否[编辑]以包括 declare -p data 的输出,这样我们就可以看到数组的实际形式是什么?(如果它不是一个数组,而实际上只是一个带有字面换行符的大字符串,那么这将解释你当前的输出)。 - Charles Duffy
我添加了“bash”标签,因为POSIX shell不允许数组。如果您指的是其他shell,请更正。 - Adam Katz
zsh 的一行解决方案:echo "${${data[@]}//${IFS:0:1}/,}" - Adam Katz
这个回答解决了你的问题吗?如何在Bash中连接数组元素? - Adam Katz
7个回答

63

有几种方法可以做到这一点:

1. 直接使用printf进行连接(通过Charles Duffy的评论

printf -v joined '%s,' "${data[@]}"
echo "${joined%,}"

printf内置函数会隐式地合并数组。您可以像下面的3a一样交互式地打印,使用一行命令printf '%s,' "${data[@]}" ,但是最后会留下一个尾随逗号。(即使在POSIX shell中,这种方法也有效,尽管您必须使用$@作为您的数组,因为POSIX无法处理其他类型的数组)。

2. 更改$IFS字段分隔符(通过chepner's答案)。

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

join_arr , "${data[@]}"

这会在函数的范围内重新定义字段分隔符,因此当自动展开$data数组时,它将使用期望的定界符而不是全局$IFS的第一个值(如果它为空或未定义,则如此)。

虽然可以在没有函数的情况下完成此操作,但保留$IFS有些麻烦: Charles Duffy指出,在临时重新分配后恢复IFS="$OLD_IFS"可能会被计算为IFS="",但如果之前$IFS未定义,那就不同于unset IFS,虽然有可能区分这两种情况,但这种函数式方法由于使用local限制了$IFS的作用域,因此更加清晰。

该解决方案仅支持单字符定界符。请参见下面的#5,其中有一个类似的函数支持任何长度的定界符。

3a. 循环遍历其内容(并逐步打印)

delim=""
for item in "${data[@]}"; do
  printf "%s" "$delim$item"
  delim=","
done
echo # add a newline

如果循环中的其他代码涉及外部调用(甚至是 sleep 0.1),你实际上可以一步一步地观察这个构建过程,这在交互式环境中可能有所帮助。

3b. 循环遍历其内容(并构建一个变量)

delim=""
joined=""
for item in "${data[@]}"; do
  joined="$joined$delim$item"
  delim=","
done
echo "$joined"

4. 将数组保存为字符串并对其进行替换操作(注意,数组中不能包含空格*)

data_string="${data[*]}"
echo "${data_string//${IFS:0:1}/,}"

* 只有当$IFS的第一个字符(默认为空格)不存在于数组的任何项目中时,此方法才有效。

这使用bash模式替换:${parameter//pattern/string}将用string替换$parameter中的每个pattern实例。在这种情况下,string${IFS:0:1},即从开头开始并在一个字符后结束的$IFS子字符串。

Z Shell (zsh)可以在一个嵌套的参数扩展中完成此操作:

echo "${${data[@]}//${IFS:0:1}/,}"

(尽管 Z Shell 也可以更优雅地使用其专用的join标志,如 echo "${(j:,:)data}" 所示,正如@DavidBaynard在本答案下面的评论中所提到的。)

5. 在隐式循环中进行替换后连接 (来自Nicholas Sushkin 对一个重复问题的回答)

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

join_by , "${data[@]}"
这与上面的#2非常相似(通过chepner),但它使用模式替换而不是$IFS,因此支持多个字符的分隔符。 $d保存定界符,$f保存数组中的第一项(稍后会说明原因)。真正的魔法是${@/#/$d},它用定界符($d)替换每个数组元素的开头(#)。由于你不想以定界符开始,这里使用shift来越过定界符参数,同时也越过了第一个数组元素(保存为$f),然后将其打印在替换之前。
当像我们这样给printf提供额外的参数时,它有一种奇怪的行为。模板(%s)仅指定将有一个参数,因此其余的参数就好像是循环,都连接到彼此上。考虑将关键行更改为printf "%s\n" "$f" "${@/#/$d}"。每个元素后会得到一个换行符。如果你想要在打印连接的数组后有一个尾随换行符,请使用printf %s "$f" "${@/#/$d}" $'\n'(我们需要使用$'…'符号告诉bash要解释转义符;另一种方法是插入字面换行符,但那样代码看起来很奇怪)。

3
“字符串替换”方法也会改变其他空格。考虑data=( "first item" "second item" "third item" ); 你想要输出first item,second item,third item,而不是first,item,second,item,third,item - Charles Duffy
3
实际上,我通常会使用printf -v var '%s,' "${data[@]}"; echo "${var%,}" -- 这不会改变 IFS,也不会对数据的格式做出任何假设。 - Charles Duffy
3
花括号本身并不限制更改的范围。这是命令组和子Shell之间的主要区别;命令组仍在当前Shell中执行。 - chepner
1
@AdamKatz 对于zsh,您可以使用"${(j:,:)data}"来实现相同的功能。关于单词拆分有一个微妙之处:
请注意,在s:string:标志或SH_WORD_SPLIT选项进行字段拆分之前会发生这种情况。
(来源:https://zsh.sourceforge.io/Doc/Release/Expansion.html#Parameter-Expansion)
- David Baynard
1
@DavidBaynard - 谢谢,我已经将它添加到答案中。由于这是一个Bash问题,它不会有自己的部分,但我认为它值得一提。 - Adam Katz
显示剩余6条评论

12
为了更容易地本地化更改 IFS,请使用一个函数:
join () {
  local IFS="$1"
  shift
  echo "$*"
}

join , "${data[@]}"

我喜欢这个。它解决了 $IFS 被覆盖的问题,并使用其他语言用户非常熟悉的语法。 - Adam Katz
1
有一个缩短本地化值的方法,(IFS=,; echo "${data[*]}"),但代价是(几乎肯定)要为子Shell 分叉一个新进程。 - chepner
1
花括号也不能本地化 IFS 的值。你必须使用子shell来完成。函数的好处是可以使用 local 命令避免覆盖全局值。 - chepner

6
对于ksh,请尝试这样做!
foo=`echo $(echo ${data[@]}) | tr ' ' ','`

通过将空格(默认)转换为逗号,您可以控制分隔符!(或者任何其他您能想到的分隔符):)


1
不错!为什么要多余的echo?这可以简化为:foo=`echo ${data[@]} | tr ' ' ','` - neowulf33

6

如果你想用逗号分隔,请将逗号作为IFS的第一个字符:

data=( first second third )
IFS=,
echo "${data[*]}"

...发出:

first,second,third

为了避免修改IFS状态,您可以将此代码嵌入到函数中,并将IFS声明为局部变量。如果您使用的是bash 4.3或更高版本,则可以使用namevar支持来参数化保存输出的变量,而无需使用子shell带来的开销:
comma_sep() {
  local -n comma_sep__dest=$1; shift || return
  local IFS=,
  comma_sep__dest=$*
}

comma_sep result "${data[@]}"
echo "$result" # prints first,second,third

2
在这种方式下,小心不要让$IFS保持定义状态;否则后面的脚本命令可能会出现问题。 - Adam Katz
1
问题在于 oIFS=$IFS; ...; IFS=$oIFS 也不是一个空操作 -- 它会将 unset IFS 改变为 IFS='',这两种状态具有不同的行为(前者的行为类似于 IFS=$' \t\n')。如果想要安全,那么使用 local 方法或在子shell中进行作用域限定是合适的 -- chepner 的回答已经涵盖了它们。我不愿意编写假装具有不存在的安全功能的代码,并且认为任何执行未引用扩展而没有显式设置 IFS 值的代码(或在运行 read 时未明确设置 IFS)都是有缺陷的。 - Charles Duffy

4

在子命令中更改IFS的单行代码(安全):

echo "$(IFS=,; echo "${data[*]}")"

注意:IFS 只会取第一个字符,所以结果将是没有空格的逗号分隔的值列表。
$ data=(file1 file2 file3)
$ (IFS=,; echo "${data[*]}")
file1,file2,file3

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

1
printComma(){
    printf "%s," "${@:1:${#}-1}"
    printf "%s" "${@:${#}}"
}
printNewline(){
    printf "%s\n" "${@:1:${#}-1}"
    echo "${@:${#}}"
}
join () {
  local IFS="$1"
  shift
  echo "$*"
}
declare -a comma=(
    a
    b
    c
)
declare -a newline=(
    xyz
    abc
    def
    ghi
)
echo "\
Comma separated list $(printComma "${comma[@]}")
Newline list: $(printNewline "${newline[@]}")

Comma separated list $(join , "${comma[@]}")
Newline list: $(join \n "${newline[@]}")"

Comma separated list a,b,c
Newline list: xyz
abc
def
ghi

Comma separated list a,b,c
Newline list: xyznabcndefnghi

0

这种方法不会赢得任何奖项,但它适合我的更复杂需求,即将带有(foo bar baz)的数组更改为用空格分隔并用转义双引号括起来的逗号分隔字符串\"foo\", \"bar\", \"baz\"。这是IFS不太适合的。

data=(foo bar baz)
data_str="${data[*]}"
separated_data_str=\"${data_str// /\", \"}\"

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