如何在Bash中加入数组元素?


416

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

FOO=( a b c )

如何用逗号将元素加入?例如,产生a,b,c

Answers:


571

Pascal Pilz将解决方案重写为100%纯Bash中的函数(无外部命令):

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

另外,我们可以使用printf来支持多字符定界符,使用@gniourf_gniourf的想法

function join_by { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; }

例如,

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

9
将其用于多字符分隔符:function join {perl -e'$ s = shift @ARGV; 打印连接($ s,@ARGV);' “ $ @”;} join','abc#a,b,c
Daniel Patru 2014年

4
@dpatru反正使纯扑?
CMCDragonkai 2014年

4
@puchu多字符分隔符无效。说“空间无效”,听起来好像与空间连接无效。是的
埃里克

6
如果将输出存储到变量,则这会提升生成子外壳。使用konsolebox样式:) function join { local IFS=$1; __="${*:2}"; }function join { IFS=$1 eval '__="${*:2}"'; }。然后使用__。是的,我是一个提倡使用__as结果变量;)(以及常见的迭代变量或临时变量)的人。如果这个概念进入了流行的Bash Wiki网站,他们就会抄袭我:)
konsolebox

6
不要将扩展名$d放在的格式说明符中printf。您认为自己是安全的,因为您已经“转义了”,%但是还有其他警告:当分隔符包含反斜杠(例如\n)时,或者当分隔符以连字符开头时(可能还有我现在想不到的其他字符)。您当然可以解决这些问题(将反斜杠替换为双反斜杠并使用printf -- "$d%s"),但是在某些时候,您会感觉到您正在与外壳进行斗争而不是使用它。这就是为什么在下面的回答中,我将分隔符放在要连接的术语之前。
gniourf_gniourf's

206

另一个解决方案:

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

7
+1。那呢printf -v bar ",%s" "${foo[@]}"?它fork实际上少了一个clone。甚至还需要读取文件:printf -v bar ",%s" $(<infile)
TrueY

14
相反祈祷的$separator不含有%s或这样的,你可以让你printf健壮:printf "%s%s" "$separator" "${foo[@]}"
musiphil

5
@musiphil错误。从bash的人:“格式重复使用所必需的消耗所有的参数使用两个格式的占位符像。printf "%s%s"在一审中唯一一组输出,将使用分离器,然后简单地拼接参数的其余部分。
AnyDev

3
@AndrDevEK:感谢您发现错误。相反,我会建议类似的东西printf "%s" "${foo[@]/#/$separator}"
musiphil

2
@musiphil,谢谢。是! 然后printf变得多余,并且该行可以减少为IFS=; regex="${foo[*]/#/$separator}"。在这一点上,这基本上成为gniourf_gniourf的答案,而IMO从一开始就是比较干净的,也就是说,使用函数来限制IFS更改和临时变量的范围。
AnyDev 2014年

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

3
不需要外部双引号和冒号周围的双引号。仅需使用内部双引号:bar=$( IFS=, ; echo "${foo[*]}" )
2012年

8
+1是最紧凑的解决方案,不需要循环,不需要外部命令,并且不对参数的字符集施加其他限制。
ceving 2012年

22
我喜欢该解决方案,但只有在IFS是一个字符的情况下,它才有效
Jayen

8
任何想法,为什么如果使用这个不工作@,而不是*,如$(IFS=, ; echo "${foo[@]}")?我可以看到,*已经在元素中保留了空格,再次不确定如何保留,因为@为此通常需要这样做。
haridsv 2014年

10
我在上面找到了自己的问题的答案。答案是IFS仅针对*。在bash手册页中,搜索“特殊参数”,然后在以下位置查找说明*
haridsv

66

也许,例如

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

echo "$FOOJOIN"

3
如果这样做,它将认为IFS-是变量。您必须这样做echo "-${IFS}-"(花括号将破折号与变量名分开)。
暂停,直到另行通知。

1
仍然得到了同样的结果(我只是把在破折号说明了这一点...... echo $IFS做同样的事。
大卫Wolever

41
也就是说,这似乎仍然有效……因此,就像Bash的大多数事情一样,我会假装自己理解并继续生活。
David Wolever 09年

2
“-”不是变量名的有效字符,因此当您使用$ IFS-时,shell会做正确的事情,您不需要$ {IFS}-(Linux和solaris中的bash,ksh,sh和zsh)也同意)。
Idelic

2
@David您的回声和Dennis的区别在于他使用了双引号。IFS的内容“在输入时”用作单词分隔符的声明-因此,您始终会得到一个空行且不带引号。
马丁克莱顿2009年

30

令人惊讶的是我的解决方案还没有给出:)这对我来说是最简单的方法。它不需要功能:

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

注意:观察到该解决方案在非POSIX模式下可以正常工作。在POSIX模式下,元素仍然可以正确连接,但IFS=,变为永久性。


不幸的是,仅适用于单字符定界符
maoizm

24

这是完成此任务的100%纯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}"
}

1
您的最后一个解决方案非常好,但是可以通过创建join_ret局部变量,然后在末尾回显该变量,使其变得更整洁。这允许join()以常规的shell脚本方式使用,例如$(join ":" one two three),并且不需要全局变量。
James Sneeringer,2015年

1
@JamesSneeringer我故意使用此设计以避免出现子外壳。在shell脚本中,与许多其他语言不同,使用这种方式的全局变量不一定是一件坏事;特别是如果它们在这里是为了避免使用子壳。此外,$(...)修剪尾随换行符;因此,如果数组的最后一个字段包含尾随换行符,则将对它们进行修剪(请参见演示,其中我的设计未对其进行修剪)。
gniourf_gniourf

这适用于多字符分隔符,这让我很高兴^ _ ^
spiffytech

要解决“您为什么不喜欢printf -v?”:在Bash中,局部变量并不是真正的函数局部变量,因此您可以执行以下操作。(使用局部变量x调用函数f1,后者又调用修改f的函数f2-在f1的范围内声明为局部变量),但这并不是局部变量应该如何工作的。如果局部变量确实是局部变量(例如,假设在必须同时在bash和ksh上运行的脚本中是局部变量),则这会导致整个“通过将值存储在具有此名称的变量中来返回值”方案中出现问题。
tetsujin

15

这与现有解决方案并没有太大不同,但是它避免了使用单独的函数,不在IFS父外壳中进行修改并且全部在一行中:

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

导致

a,b,c

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


13

不使用外部命令:

$ 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

警告,它假定元素没有空格。


4
如果您不想使用中间变量,则可以使用更短的时间:echo ${FOO[@]} | tr ' ' ','
jesjimher

2
我不理解反对票。与此处发布的其他解决方案相比,它是一种紧凑且易于阅读的解决方案,并且明显警告说,有空间时,该解决方案将不起作用。
jesjimher

12

我会将数组作为字符串回显,然后将空格转换为换行符,然后使用paste将所有内容连接到一行中,如下所示:

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

结果:

a,b,c

这对我来说似乎是最快,最干净的!


$FOO不过,它只是数组的第一个元素。同样,这对于包含空格的数组元素也无效。
本杰明·

9

重复使用@无关紧要的解决方案,但是通过避免$ {:1}的替换和避免中间变量的使用来声明。

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

printf具有“格式字符串根据需要满足参数重复使用的频率。” 在其手册页中,以便记录字符串的串联。然后,诀窍是使用LIST长度来切碎最后一个spaster,因为cut只会保留LIST的长度作为字段数。


7
s=$(IFS=, eval 'echo "${FOO[*]}"')

8
你应该充实自己的答案。
13年

最好的一个。谢谢!!
彼得·潘gz

4
我希望我可以否决这个答案,因为它会打开一个安全漏洞,并且会破坏元素中的空间。
eel ghEEz

1
实际上,@ bxm似乎保留了空格,并且不允许从echo参数上下文中转义。我认为加法@Qfoo=("a ," "b ' ' c" "' 'd e" "f " ";" "ls -latr"); s=$(IFS=, eval 'echo "${foo[*]@Q}"'); echo "${s}"'a ,','b '\'' '\'' c',''\'' '\''d e','f ',';','ls -latr '
可以避免

1
除非必要,否则避免使用子壳的解决方案。
konsolebox

5

可以接受任何长度分隔符的printf解决方案(基于@无关紧要的答案)

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

这会产生带有逗号的输出。
Mark Renouf 2013年

最后一个bar = $ {bar:$ {#sep}}删除分隔符。我只是复制并粘贴到bash外壳中,它确实起作用。您正在使用什么外壳?
里卡多·加里

2
任何printf 格式说明。(如%s无意中$sep会产生问题。
Peter.O

sep可以用消毒${sep//\%/%%}。我喜欢您的解决方案比${bar#${sep}}${bar%${sep}}(替代)好。如果将其转换为将结果存储到通用变量(例如__)而不是的函数,则很好echo
konsolebox

function join_by { printf -v __ "${1//\%/%%}%s" "${@:2}"; __=${__:${#1}}; }
konsolebox


4

最佳答案的简短版本:

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

用法:

joinStrings "$myDelimiter" "${myArray[@]}"

1
较长的版本,但无需将一部分参数复制到数组变量中:join_strings () { local d="$1"; echo -n "$2"; shift 2 && printf '%s' "${@/#/$d}"; }
Rockallite

另一个版本: join_strings () { local d="$1"; echo -n "$2"; shift 2 && printf '$d%s' "${@}"; } 适用于以下用途:join_strings 'delim' "${array[@]}"或未引用:join_strings 'delim' ${array[@]}
Cometsong

4

到目前为止,将世界上最好的与以下思想相结合。

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

这个小杰作是

  • 100%纯bash(暂时未设置IFS的参数扩展,无外部调用,无printf ...)
  • 紧凑,完整和完美(适用于单字符和多字符限制器,适用于包含空格,换行符和其他外壳特殊字符的限制器,适用于空的分隔符)
  • 高效(无子shell,无数组副本)
  • 简单而愚蠢,在一定程度上也很美丽和启发

例子:

$ 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

1
不太好:至少有两个问题:1. join_ws ,(不带参数)错误地输出,,。2. join_ws , -e错误地输出任何内容(那是因为您错误地使用echo而不是printf)。我实际上不知道您为什么宣传echo代替的使用printfecho臭名昭著的坏处,并且printf是一个强大的内置函数。
gniourf_gniourf

1

现在我正在使用:

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 ' ' ','`"

哪个可行,但是(在一般情况下)如果数组元素中有空格,则会严重中断。

(对于那些感兴趣的人,这是一个围绕pep8.py的包装脚本)


您从哪里获得这些数组值?如果您要像这样硬编码,为什么不只是foo =“ a,b,c”。
ghostdog74

在这种情况下,我实际上在对值进行硬编码,但是我想将它们放在数组中,以便可以分别对每个值进行注释。我已经更新了答案,向您展示了我的意思。
David Wolever,2009年

你实际上使用bash假设,这可能会更好地工作:ARGS="--ignore $(echo "${TO_IGNORE[@]}" | tr ' ' ',')"。运算符$()比背景符功能强大(允许嵌套$()"")。${TO_IGNORE[@]}用双引号包装也应有所帮助。
kevinarpe13年

1

我的尝试。

$ array=(one two "three four" five)
$ echo "${array[0]}$(printf " SEP %s" "${array[@]:1}")"
one SEP two SEP three four SEP five


1

感谢@gniourf_gniourf对到目前为止我最好的世界组合的详细评论。抱歉,发布代码未经全面设计和测试。这是一个更好的尝试。

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

从概念上讲,这种美是

  • (仍然)100%纯bash(感谢您明确指出printf也是内置函数。在此之前我没有意识到这一点)
  • 与多字符定界符一起使用
  • 更紧凑,更完整,这次我们仔细考虑了一下,并使用了来自shell脚本等的随机子字符串进行了长期的压力测试,其中包括使用shell特殊字符或控制字符,或者在分隔符和/或参数中均不使用字符,以及使用边沿情况,以及极端情况和其他怪癖,就像根本没有参数一样。那不能保证没有更多的bug,但是要找到一个bug会比较困难。顺便说一句,即使是目前票数最高的答案及相关问题,也受到诸如-e bug之类的困扰...

其他示例:

$ 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 $ 
$

1

如果您要连接的元素不是数组,而只是一个用空格分隔的字符串,则可以执行以下操作:

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

例如,我的用例是在我的shell脚本中传递了一些字符串,我需要使用它在SQL查询上运行:

./my_script "aa bb cc dd"

在my_script中,我需要执行“ SELECT * FROM table WHERE name IN('aa','bb','cc','dd')。然后上述命令将非常有用。


您可以使用printf -v bar ...而不是必须在子shell中运行printf循环并捕获输出。
codeforester

上面所有受赞誉的花哨的解决方案都行不通,但您的粗略解决方案对我
有用

1

这是大多数POSIX兼容shell支持的功能:

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[@]}"
}

这是不错的Bash代码,但POSIX 根本没有数组(或local)。
Anders Kaseorg '19

@Anders:是的,我最近才学到了这很难的方法:(尽管大多数兼容POSIX的shell似乎都支持数组
所以我暂时不做介绍。– user541686

1

使用变量间接指向直接引用数组也可以。也可以使用命名引用,但是它们仅在4.3中可用。

使用这种形式的函数的优点是您可以使分隔符为可选(默认为default的第一个字符IFS,它是一个空格;如果愿意,可以将其设置为空字符串),并且避免将值扩展两次(第一个作为参数传递,第二个作为"$@"函数传递)。

此解决方案也不需要用户在命令替换内调用该函数-召唤一个子外壳,以获得分配给另一个变量的字符串的合并版本。

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.

随意为该功能使用更舒适的名称。

这适用于3.1到5.0-alpha。正如所观察到的,变量间接寻址不仅适用于变量,还适用于其他参数。

参数是存储值的实体。它可以是名称,数字或特殊参数下面列出的特殊字符之一。变量是用名称表示的参数。

数组和数组元素也是参数(存储值的实体),并且对数组的引用在技术上也就是对参数的引用。就像特殊参数一样@array[@]也提供了有效的参考。

与参数本身不同的引用的扩展或选择性扩展形式(如子字符串扩展)不再起作用。

更新资料

在Bash 5.0的发行版本中,变量间接已被称为间接扩展,其行为已在手册中明确记录:

如果parameter的第一个字符是感叹号(!),并且parameter不是nameref,则它引入一个间接级别。Bash使用通过扩展其余参数形成的值作为新参数;然后对其进行扩展,并在其余扩展中使用该值,而不是原始参数的扩展。这称为间接扩展。

请注意,在的文档中${parameter}parameter将其称为“如在参数中描述的shell参数或数组引用 ”。并且在数组文档中,提到“可以使用来引用数组的任何元素${name[subscript]}”。 这将__r[@]成为数组引用。

按参数加入版本

看到我的评论里卡尔多·加利的回答


2
是否有特定的原因__用作变量名?使代码真正不可读。
佩萨

@PesaThe这只是一个偏好。我更喜欢使用通用名称作为返回变量。其他非通用名称将其自身归因于特定功能,因此需要记住。调用返回不同变量值的多个函数可以使代码不那么容易理解。使用通用名称将迫使脚本编写者将值从返回变量传递到适当的变量,以避免发生冲突,并且由于返回的值在何处变得明确,因此最终使代码更具可读性。我对该规则几乎没有例外。
konsolebox

0

这种方法照顾值内的空格,但需要循环:

#!/bin/bash

FOO=( a b c )
BAR=""

for index in ${!FOO[*]}
do
    BAR="$BAR,${FOO[$index]}"
done
echo ${BAR:1}

0

如果您以循环方式构建数组,请使用以下简单方法:

arr=()
for x in $(some_cmd); do
   arr+=($x,)
done
arr[-1]=${arr[-1]%,}
echo ${arr[*]}

0

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

这是最短的方法。

例,

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

1
这对于带空格的字符串不能正常工作:`t =(a“ b c” d); 回声$ {t [2]}(打印“ b c”);回声$ {“ $ {t [*]}”“ // /,}(打印a,b,c,d)
kounoupis

7
bash: ${"${arr[*]}"// /,}: bad substitution
卡梅隆·哈德森

0

也许聚会晚了,但这对我有用:

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

  echo "${output}"
}

-1

也许我缺少明显的东西,因为我是整个bash / zsh的新手,但是在我看来,您根本不需要使用它printf。没有它也不会变得非常丑陋。

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

至少到目前为止,它对我没有任何问题。

例如,join \| *.sh假设我在我的~目录中,则输出utilities.sh|play.sh|foobar.sh。对我来说足够好了。

编辑:这基本上是Nil Geisweiller的答案,但被概括为一个函数。


1
我不是拒绝投票的人,但是在函数中操作全局变量似乎很古怪。
Tripleee '18

-2
liststr=""
for item in list
do
    liststr=$item,$liststr
done
LEN=`expr length $liststr`
LEN=`expr $LEN - 1`
liststr=${liststr:0:$LEN}

最后还要注意多余的逗号。我不是bash专家。只是我的2c,因为这比较基础和易于理解


-2
awk -v sep=. 'BEGIN{ORS=OFS="";for(i=1;i<ARGC;i++){print ARGV[i],ARGC-i-1?sep:""}}' "${arr[@]}"

要么

$ a=(1 "a b" 3)
$ b=$(IFS=, ; echo "${a[*]}")
$ echo $b
1,a b,3
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.