将Bash数组转换为分隔字符串

62

我想知道以下问题:

  1. 为什么给出的不可用示例无法工作。
  2. 是否存在比工作示例中给出的更干净的方法。

不可用示例

> ids=(1 2 3 4);echo ${ids[*]// /|}
1 2 3 4
> ids=(1 2 3 4);echo ${${ids[*]}// /|}
-bash: ${${ids[*]}// /|}: bad substitution
> ids=(1 2 3 4);echo ${"${ids[*]}"// /|}
-bash: ${"${ids[*]}"// /|}: bad substitution

演示例子

> ids=(1 2 3 4);id="${ids[@]}";echo ${id// /|}
1|2|3|4
> ids=(1 2 3 4); lst=$( IFS='|'; echo "${ids[*]}" ); echo $lst
1|2|3|4

上下文中,要在sed命令中使用的定界字符串,以进行进一步解析。


2
${${ids[*]}// /|} 是语法错误,仅此而已。不知道你在这里尝试实现什么。 - Gilles Quénot
1
试图在一次跳转中实现变量替换,这是不可能的... - koola
1
相关:如何在Bash中连接数组元素? - codeforester
你可以使用paste - x-yuri
4个回答

66

在bash中将数组转换为字符串

1. 使用$IFS将数组转换为字符串

因为括号用于界定一个数组,而不是一个字符串:

ids="1 2 3 4";echo ${ids// /|}
1|2|3|4

一些示例:使用两个字符串填充$idsa bc d
ids=("a b" "c d" e\ f)

echo ${ids[*]// /|}
a|b c|d e|f

IFS='|';echo "${ids[*]}";IFS=$' \t\n'
a b|c d|e f

...最后:
IFS='|';echo "${ids[*]// /|}";IFS=$' \t\n'
a|b|c|d|e|f

将数组组装在一起,以$IFS的第一个字符分隔,但是在数组的每个元素中将空格替换为|

当你执行以下操作时:

id="${ids[@]}"

你将由合并数组ids的字符串构建的字符串通过空格转移到一个新的字符串类型的变量中。
注意:当"${ids[@]}"给出一个以空格分隔的字符串时,"${ids[*]}"(用星号*代替at符号@)将会生成一个由$IFS的第一个字符分隔的字符串。
"man bash"的解释如下:
man -Len -Pcol\ -b bash | sed -ne '/^ *IFS /{N;N;p;q}'
   IFS    The  Internal  Field  Separator  that  is used for word splitting
          after expansion and to split  lines  into  words  with  the  read
          builtin command.  The default value is ``<space><tab><newline>''.

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

字面上是一个空格、一个制表符和一个换行符。所以,虽然第一个字符是一个空格,但使用 * 与 @ 的效果是相同的。
但是:
{
    IFS=: read -a array < <(echo root:x:0:0:root:/root:/bin/bash)
    
    echo 1 "${array[@]}"
    echo 2 "${array[*]}"
    OIFS="$IFS" IFS=:
    echo 3 "${array[@]}"
    echo 4 "${array[*]}"
    IFS="$OIFS"
}
1 root x 0 0 root /root /bin/bash
2 root x 0 0 root /root /bin/bash
3 root x 0 0 root /root /bin/bash
4 root:x:0:0:root:/root:/bin/bash

注意:行IFS=: read -a array < <(...)将使用:作为分隔符,而不会永久设置$IFS。这是因为输出行#2以空格作为分隔符。
1.1 使用函数localize $IFS 只需打印数组
printArry() {
    local IFS="$1"
    shift
    echo "$*"
}        

printArry @ "${ids[@]}"
a b@c d@e f

或者在原地合并数组。
mergeArry() {
    local IFS="$1"
    local -n _array_to_merge=$2
    _array_to_merge=("${_array_to_merge[*]}")
}

declare -p ids
declare -a ids=([0]="a b" [1]="c d" [2]="e f")

mergeArry '#' ids
declare -p ids
declare -a ids=([0]="a b#c d#e f")

2. 字符串数组转字符串数组([@] vs [*]

这里有一个明显的区别:

  • "$@""${var[@]}" 会生成一个字符串数组
  • "$*""${var[*]}" 会生成一个唯一的字符串

请仔细阅读:man '-Pless +/Special\ Parameters' bash

为了实现这个目的,我将引用每个参数,以防止在命令行扩展时被$IFS分割,使用双引号来允许变量扩展

ids=('a b c' 'd e f' 'g h i')

printf '<< %s >>\n' "${ids[@]// /|}"

<< a|b|c >>
<< d|e|f >>
<< g|h|i >>

printf '<< %s >>\n' "${ids[*]// /|}"

<< a|b|c d|e|f g|h|i >>

在哪里:

  • 所有空格都被管道符替换,每个字符串中都是如此
  • 所有字符串都合并为一个字符串,由第一个$IFS字符分隔。
( IFS='@'; printf '<< %s >>\n' "${ids[*]// /|}" )
<< a|b|c@d|e|f@g|h|i >>

注意:`${var// /something}`将会用`something`替换每一个空格,但是`${var[*]}`将会通过只使用第一个字符来合并数组。
( IFS='FOO'; printf '<< %s >>\n' "${ids[*]// / BAR }" )
<< a BAR b BAR cFd BAR e BAR fFg BAR h BAR i >>

是的,通过使用${var// / ... },你可以用任何你想要的东西,包括更多的空格,来替换1个空格

3. 使用printf将数组转换为字符串

正如我们所见,使用$IFS仅限于一个字符。如果你需要使用更多的字符来插入你的字段之间。你必须使用printf

ids=("a b" "c d" e\ f)
sep=" long separator "
printf -v string "%s$sep" "${ids[@]}"
echo "${string%$sep}"
a b long separator c d long separator e f

注意:这个语法是可行的,但有一些限制,请继续阅读!

3.1 使用printf将数组转换为字符串,并传入函数

为了支持特殊字符%*separator中的使用,函数必须进行以下处理:

  • 防止%printf解释(printf '%%'将显示一个%
  • 防止*参数扩展解释,因此$sep必须使用双引号引起来。
printArry() {
    local sep=$1 string
    shift
    printf -v string "%s${sep//%/%%}" "$@"
    echo "${string%"$sep"}"
}        

printArry ' long separator ' "${ids[@]}"
a b long separator c d long separator e f

printArry '*' "${ids[@]}"
a b*c d*e f

printArry '%' "${ids[@]}"
a b%c d%e f

或者在原地合并数组。
mergeArry() {
    local sep=$1 string
    local -n _array_to_merge=$2
    printf -v string "%s${sep//%/%%}" "${_array_to_merge[@]}"
    _array_to_merge=("${string%"$sep"}")
}

mergeArry ' another separator ' ids
declare -p ids
declare -a ids=([0]="a b another separator c d another separator e f")

ids=("a b" "c d" e\ f)
mergeArry '*' ids
declare -p ids
declare -a ids=([0]="a b*c d*e f")

4. 使用bash参数扩展将数组合并为字符串

又一种方法TMTOWTDI:但是在工作时,我们必须清空$IFS,我更喜欢在一个函数中使用它来本地化$IFS

printArry () { 
    local -n _array_to_print=$2
    local IFS=
    local _string_to_print="${_array_to_print[*]/#/"$1"}"
    echo "${_string_to_print/#"$1"}"
}

请注意,你可以将#替换为%
  • "${_array_to_merge[*]/#/$1}"将会将字符串的开头替换为$1,而
  • "${_array_to_merge[*]/%/$1}"将会将字符串的结尾替换为$1,然后
  • "${_array_to_merge/#"$1"}"将会将位于字符串开头的$1替换为,或者
  • "${_array_to_merge/%"$1"}"将会将位于字符串结尾的$1替换为
mergeArry () { 
    local -n _array_to_merge=$2
    local IFS=
    _array_to_merge=("${_array_to_merge[*]/#/"$1"}")
    _array_to_merge=("${_array_to_merge/#"$1"}")
}

4.1 根据分隔符的长度进行微小变化:
printArry () { 
    local -n _array_to_print=$2
    local IFS=
    local _string_to_print="${_array_to_print[*]/#/"$1"}"
    echo "${_string_to_print:${#1}}"
}

mergeArry () { 
    local -n _array_to_merge=$2
    local IFS=
    _array_to_merge=("${_array_to_merge[*]/#/"$1"}")
    _array_to_merge=("${_array_to_merge:${#1}}")
}

或者

printArry () { 
    local -n _array_to_print=$2
    local IFS=
    local _string_to_print="${_array_to_print[*]/%/"$1"}"
    echo "${_string_to_print::-${#1}}"
}

5. 比较

待完成...


所以这是一个 typeof 和变量替换错误,因为 ${ 期望的是字符串类型的变量,但实际上却没有接收到。感谢详细的解释。 - koola
一个人也可以跳过字符串赋值,仍然可以使用 printf '%smyDelim' "${array[@]}" 将数组转换为带有任意分隔符的分隔字符串,而不一定是单个字符。最后一个分隔符 myDelim 可以通过管道传递到 sed -e 's/myDelim$//' 中删除,但这很麻烦。还有更好的想法吗? - Jonathan Y.
1
@JonathanY。如果是这样,请使用printf -v myVar'%smyDelim' "${array[@]}"; myVar="${myVar%myDelim}"而不是分叉到sed - F. Hauri - Give Up GitHub
除了不跳过变量分配,例如使用 printf 为另一个可执行文件提供输入参数。我明白分配变量可能更快、更便宜;只是感觉不太优雅。 - Jonathan Y.
@F.Hauri,你需要在myDelim周围使用双引号。你还必须使用美元符号。像这样“%s$myDelim”。 - Swepter
在我的示例中,myDelim 不是一个变量,而是一个固定的字符串,以回答之前 @Jonathan 的评论。 - F. Hauri - Give Up GitHub

29

你也可以使用printf,而不需要任何外部命令或者改变IFS的需求:

ids=(1 2 3 4)                     # create array
printf -v ids_d '|%s' "${ids[@]}" # yields "|1|2|3|4"
ids_d=${ids_d:1}                  # remove the leading '|'

3
对我来说这很简单,因为我有一个由1个以上字符组成的字符串要放在数组元素之间,而且在我的情况下,我也不需要删除“多余”的部分。工作得非常好!printf -v strng "'%s',\n" ${thearray[*]} :) - Cometsong
2
同意,这是最简单易用的选项。此外,如果您正在使用带有语法高亮的IDE,则它也是更加用户友好的选项,因为它不会像gniourf_gniourf的答案中那样捕获eval的语法。 - JuroOravec

17

你的第一个问题已经在F. Hauri's answer中得到了解答。以下是连接数组元素的规范方法:

ids=( 1 2 3 4 )
IFS=\| eval 'lst="${ids[*]}"'

有些人会大声喊叫eval很邪恶,但是在这里它是完全安全的,这要归功于单引号。这样做只有优点:没有子shell,IFS没有全局修改,它不会修剪尾随换行符,而且非常简单。


5
一个将参数数组连接成分隔符字符串的实用函数:
#!/usr/bin/env bash

# Join arguments with delimiter
# @Params
# $1: The delimiter string
# ${@:2}: The arguments to join
# @Output
# >&1: The arguments separated by the delimiter string
array::join() {
  (($#)) || return 1 # At least delimiter required
  local -- delim="$1" str IFS=
  shift
  str="${*/#/$delim}" # Expands arguments with prefixed delimiter (Empty IFS)
  printf '%s\n' "${str:${#delim}}" # Echo without first delimiter
}

declare -a my_array=( 'Paris' 'Berlin' 'London' 'Brussel' 'Madrid' 'Oslo' )

array::join ', ' "${my_array[@]}"
array::join '*' {1..9} | bc # 1*2*3*4*5*6*7*8*9=362880 Factorial 9

declare -a null_array=()

array::join '== Ultimate separator of nothing ==' "${null_array[@]}"

输出:

Paris, Berlin, London, Brussel, Madrid, Oslo
362880


现在有了 Bash 4.2+ 的名称引用变量,就不再需要使用子 shell 的输出捕获。
#!/usr/bin/env bash

if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[0] < 2)))
then
  printf 'Bash version 4.2 or above required for nameref variables\n' >&2
  exit 1
fi

# Join arguments with delimiter
# @Params
# $1: The variable reference to receive the joined output
# $2: The delimiter string
# ${@:3}: The arguments to join
# @Output
array::join_to() {
  (($# > 1)) || return 1 # At least nameref and delimiter required
  local -n out="$1"
  local -- delim="$2" str IFS=
  shift 2
  str="${*/#/$delim}" # Expands arguments with prefixed delimiter (Empty IFS)
  # shellcheck disable=SC2034 # Nameref variable
  out="${str:${#delim}}" # Discards prefixed delimiter
}

declare -g result1 result2 result3
declare -a my_array=( 'Paris' 'Berlin' 'London' 'Brussel' 'Madrid' 'Oslo' )

array::join_to result1 ', ' "${my_array[@]}"
array::join_to result2 '*' {1..9}
result2=$((result2)) # Expands arythmetic expression

declare -a null_array=()

array::join_to result3 '== Ultimate separator of nothing ==' "${null_array[@]}"

printf '%s\n' "$result1" "$result2" "$result3"

1
非常方便,谢谢!不过,也许“join”会是一个更合适的名称,因为它与例如PerlPythonjoin函数的工作方式相同? - TheDudeAbides
@TheDudeAbides 无法直接命名为join,因为这个名称会与现有的合并文件行的命令名称产生冲突。 - Léa Gris
*扶额* - 啊,是的。我忘记了那个 - TheDudeAbides
不错的工具!允许使用变长分隔符!但请考虑使用nameref将结果作为变量传递,以防止forks... - F. Hauri - Give Up GitHub
1
@F.Hauri 现在有一个 nameref 版本。 - Léa Gris

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