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

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

FOO=( a b c )

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

323387 次浏览

也许,例如,

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


echo "$FOOJOIN"

现在我用的是:

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的包装器脚本)

这种方法处理值中的空格,但需要一个循环:

#!/bin/bash


FOO=( a b c )
BAR=""


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

还有另一个解决方案:

#!/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
liststr=""
for item in list
do
liststr=$item,$liststr
done
LEN=`expr length $liststr`
LEN=`expr $LEN - 1`
liststr=${liststr:0:$LEN}

这也可以处理结尾多余的逗号。我不是bash专家。只是我的2c,因为这更基本,更容易理解

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

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

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

printf有'格式字符串被重复使用,以满足参数。,这样字符串的连接就被记录下来了。然后诀窍是使用LIST长度来切割最后一个sperator,因为cut将只保留LIST的长度作为字段计数。

$ set a 'b c' d


$ history -p "$@" | paste -sd,
a,b c,d

不使用外部命令:

$ 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

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

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

如果你想要连接的元素不是一个数组,而是一个空格分隔的字符串,你可以这样做:

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表WHERE name In ('aa','bb','cc','dd')。那么上面的命令将是有用的。

Printf解决方案,接受任何长度的分隔符(基于@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

支持多字符分隔符的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的想法。它的工作选项errexit (set -e)和nounset (set -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 at dev.to中找到。

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

我的尝试。

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

下面是一个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}"
}

使用perl实现多字符分隔符:

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


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

或者在一行中:

perl -le 'print join(shift, @ARGV);' ', ' 1 2 3
1, 2, 3

也许我遗漏了一些明显的东西,因为我是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。对我来说足够好了。

编辑:这基本上是尼尔·盖斯韦勒的回答,但概括为一个函数。

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

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

注意:观察到该解决方案在非posix模式下工作良好。在POSIX模式中,元素仍然正确地连接,但IFS=,变成永久的。

顶部答案的简短版本:

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

用法:

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

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

# EYZ0

结果:

# EYZ0

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

如果你在循环中构建数组,这里有一个简单的方法:

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

结合所有最好的世界到目前为止与以下的想法。

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

这个小杰作是

  • 100%纯bash(参数扩展,暂时不设置IFS,没有外部调用,没有printf…)
  • 紧凑、完整和完美(适用于单字符和多字符限制符,适用于包含空格、换行符和其他shell特殊字符的限制符,适用于空分隔符)
  • 高效(无子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

感谢@gniourf_gniourf对我迄今为止的最佳世界组合的详细评论。很抱歉发布的代码没有完全设计和测试。这是一个更好的尝试。

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

这种概念上的美是

  • (仍然)100%纯bash(感谢您明确指出printf也是内置的。我之前没有意识到这一点…)
  • 使用多字符分隔符
  • 更加紧凑和完整,这一次经过仔细考虑和长期压力测试,使用来自shell脚本的随机子字符串,包括在分隔符和/或参数中使用shell特殊字符或控制字符或不使用字符,以及边缘情况,角落情况和其他完全没有参数的问题。这并不能保证不再有漏洞,但找到一个漏洞将是一个更大的挑战。顺便说一句,即使是目前投票最多的答案和相关的问题也有类似的-e错误……

附加的例子:

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

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

使用这种形式的函数的好处是,分隔符可以是可选的(默认为IFS的第一个字符,它是一个空格;如果你喜欢,也可以将其设置为空字符串),并且它避免了两次展开值(第一次作为参数传递,第二次作为函数中的"$@")。

这个解决方案也不需要用户在命令替换中调用函数(调用子shell)来获得赋值给另一个变量的字符串的连接版本。

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不是一个nameref,它引入了一个间接级别。 Bash使用通过展开参数的其余部分所形成的值作为 新的参数;然后展开该值,并在 其余的扩充,而不是原来的扩充 参数。这就是所谓的间接展开

注意,在${parameter}的文档中,parameter被称为参数中描述的shell参数或数组引用”。在数组的文档中,提到数组的任何元素都可以使用${name[subscript]}引用。这使得__r[@]成为一个数组引用。

通过参数加入

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

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

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

导致

a,b,c

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


可以化简为

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

在这一点上,它基本上与帕斯卡的回答相同,但使用printf而不是echo,并将结果打印到标准输出,而不是将其分配给变量。

下面是大多数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[@]}"
}

# EYZ0

这是做这件事最短的方法。

的例子,

# 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

也许迟到了,但这对我来说是可行的:

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


echo "${output}"
}

这些解决方案中的许多(如果不是大多数的话)依赖于晦涩难懂的语法、耗费脑力的正则表达式技巧或对外部可执行文件的调用。我想提出一个简单的、只使用bash的解决方案,它非常容易理解,在性能方面只是稍微次优。

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
}

例子:

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

我想指出,任何使用/usr/bin/[/usr/bin/printf的解决方案本质上都比我的解决方案慢,因为我使用100%纯bash。作为性能的一个例子,这里有一个演示,我创建了一个包含1,000,000个随机整数的数组,然后用逗号将它们全部连接起来,并计时。

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

这里有一个单行,有点奇怪,但适用于多字符分隔符,并支持任何值(包括包含空格或任何东西):

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

这将在控制台显示为

abc | foo bar | 456

注意到一些解决方案是如何使用printf${ar[*]},而一些解决方案是如何使用${ar[@]}的?

带有@的使用printf特性,该特性通过重复格式模板来支持多个参数。

* 不应该使用。它们实际上不需要# eyz1,而是依赖于操作字段分隔符和bash的单词展开。这些也可以用echocat等——这些解决方案可能使用printf,因为作者并不真正理解他们在做什么……

我相信这是最短的解决方案,正如Benamin W。已经提到的:

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

想要添加,如果你使用zsh,你可以删除子shell:

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

测试:

a=(1 'a b' 3)
IFS=, printf %s "${a[*]}"
1,a b,3