在bash中将字符串拆分为数组

9
我正在寻找一种在bash中按照分隔符字符串拆分字符串并将其放入数组的方法。
简单情况:
#!/bin/bash
b="aaaaa/bbbbb/ddd/ffffff"
echo "simple string: $b"

IFS='/' b_split=($b)
echo ;
echo "split"
for i in ${b_split[@]}
do
    echo "------ new part ------"
    echo "$i"
done

给出输出。
simple string: aaaaa/bbbbb/ddd/ffffff

split
------ new part ------
aaaaa
------ new part ------
bbbbb
------ new part ------
ddd
------ new part ------
ffffff

更复杂的情况:
#!/bin/bash
c=$(echo "AA=A"; echo "B=BB"; echo "======="; echo "C==CC"; echo "DD=D"; echo "======="; echo "EEE"; echo "FF";)
echo "more complex string"
echo "$c";
echo ;
echo "split";

IFS='=======' c_split=($c) ;#    <----    LINE TO BE CHANGED 

for i in ${c_split[@]}
do
    echo "------ new part ------"
    echo "$i"
done

给出输出:

more complex string
AA=A
B=BB
=======
C==CC
DD=D
=======
EEE
FF

split
------ new part ------
AA
------ new part ------
A
B
------ new part ------
BB

------ new part ------

------ new part ------

------ new part ------

------ new part ------

------ new part ------

------ new part ------

------ new part ------

C
------ new part ------

------ new part ------
CC
DD
------ new part ------
D

------ new part ------

------ new part ------

------ new part ------

------ new part ------

------ new part ------

------ new part ------

------ new part ------

EEE
FF

我希望第二个输出像这样:

------ new part ------
AA=A
B=BB
------ new part ------
C==CC
DD=D
------ new part ------
EEE
FF

我想要将字符串按照一个字符序列而不是单个字符进行分割。该怎么做?
我正在寻找的答案只需要修改第二个脚本中的这一行:
IFS='=======' c_split=($c) ;#    <----    LINE TO BE CHANGED 

1
如果你想调整IFS以使用多个字符作为分隔符,那是不可能的。请查看此线程以获取更多信息:链接 - Saman Barghi
5个回答

19

介绍

在这个底部,你会发现一个将字符串转换为数组的函数,并且具有以下语法:

ssplit "<string>" "<array name>" "<delimiter string>"

对于这个:
ssplit "$c" c_split $'\n=======\n'
declare -p c_split 
declare -a c_split=([0]=$'AA=A\nB=BB' [1]=$'C==CC\nDD=D' [2]=$'EEE\nFF')

IFS 歧义

IFS 的意思是 输入字段分隔符,即可以用作分隔符的字符列表

默认情况下,它设置为 \t\n,这意味着任何数量(大于零)的空格制表符和/或换行符都可以是一个分隔符

因此,对于字符串:$' blah foo=bar \nbaz '

 read -a c_split <<<"    blah  foo=bar 
 baz  "
 declare -p c_split 
 declare -a c_split=([0]="blah" [1]="foo=bar")

领先和尾随的分隔符将被忽略,这个字符串只包含3个部分:blahfoo=barbaz。但除了空格,IFS会将每个分隔符视为自己。
IFS=Z read a b c d e f <<<ZaZZbZcZZdZeZf
declare -p a b c d e f
declare -- a=""
declare -- b="a"
declare -- c=""
declare -- d="b"
declare -- e="c"
declare -- f="ZdZeZf"

如果您知道一个在字符串中没有使用的有效字段分隔符,那么使用 IFS 分割字符串是可能的,因此您可以使用 ${var//<pattern>/<separator>} 语法通过此字符替换您的模式

OIFS="$IFS"
IFS='§'
c=$'AA=A\nB=BB\n=======\nC==CC\nDD=D\n=======\nEEE\nFF'
c_split=(${c//=======/§})
IFS="$OIFS"
printf -- "------ new part ------\n%s\n" "${c_split[@]}"

------ new part ------
AA=A
B=BB

------ new part ------

C==CC
DD=D

------ new part ------

EEE
FF

这个方法只适用于字符串中不含有任何 § 的情况。
您可以使用另一个字符,比如 IFS=$'\026';c_split=(${c//=======/$'\026'}) 但是这可能会涉及到更多的错误。
您可以浏览字符映射来找到一个不在您的字符串中的字符:
myIfs=""
for i in {1..255};do
    printf -v char "$(printf "\\\%03o" $i)"
        [ "$c" == "${c#*$char}" ] && myIfs="$char" && break
  done
if ! [ "$myIFS" ] ;then
    echo no split char found, could not do the job, sorry.
    exit 1
  fi

但我觉得这个解决方案有点过头了。

按空格分割(或不修改IFS)

中,我们可以使用这个bashism:

b="aaaaa/bbbbb/ddd/ffffff"
b_split=(${b//// })

事实上,这个语法 ${varname// 将会启动一个翻译(由/限定),将所有的/替换成空格 赋值给数组b_split之前。

当然,这仍然使用IFS并且在空格上分割数组。

这不是最好的方法,但对于特定情况可能有效。

你甚至可以在分割之前去掉不需要的空格:

b='12 34 / 1 3 5 7 / ab'
b1=${b// }
b_split=(${b1//// })
printf "<%s>, " "${b_split[@]}" ;echo
<12>, <34>, <1>, <3>, <5>, <7>, <ab>, 

或交换它们...
b1=${b// /§}
b_split=(${b1//// })
printf "<%s>, " "${b_split[@]//§/ }" ;echo
<12 34 >, < 1 3 5 7 >, < ab>, 

分隔符字符串上拆分行:

因此,您需要不使用IFS来实现您的意图,但确实具有很好的功能:

#!/bin/bash

c=$'AA=A\nB=BB\n=======\nC==CC\nDD=D\n=======\nEEE\nFF'
echo "more complex string"
echo "$c";
echo ;
echo "split";

mySep='======='
while [ "$c" != "${c#*$mySep}" ];do
    echo "------ new part ------"
    echo "${c%%$mySep*}"
    c="${c#*$mySep}"
  done
echo "------ last part ------"
echo "$c"

让我们看一下:

more complex string
AA=A
B=BB
=======
C==CC
DD=D
=======
EEE
FF

split
------ new part ------
AA=A
B=BB

------ new part ------

C==CC
DD=D

------ last part ------

EEE
FF

关于Leading newline

在之前的示例中,前导和尾随的换行符并不会被删除。为此,您可以简单地执行以下操作:

mySep=$'\n=======\n'

代替=======

或者你可以重写分裂循环以明确地将其排除在外:

mySep=$'======='
while [ "$c" != "${c#*$mySep}" ];do
    echo "------ new part ------"
    part="${c%%$mySep*}"
    part="${part##$'\n'}"
    echo "${part%%$'\n'}"
    c="${c#*$mySep}"
  done
echo "------ last part ------"
c=${c##$'\n'}
echo "${c%%$'\n'}"

任何情况下,这符合SO问题所要求的(:以及他的示例:)

------ new part ------
AA=A
B=BB
------ new part ------
C==CC
DD=D
------ last part ------
EEE
FF

最终创建一个数组

#!/bin/bash
c=$'AA=A\nB=BB\n=======\nC==CC\nDD=D\n=======\nEEE\nFF'
echo "more complex string"
echo "$c";
echo ;
echo "split";

mySep=$'======='
export -a c_split
while [ "$c" != "${c#*$mySep}" ];do
    part="${c%%$mySep*}"
    part="${part##$'\n'}"
    c_split+=("${part%%$'\n'}")
    c="${c#*$mySep}"
  done
c=${c##$'\n'}
c_split+=("${c%%$'\n'}")

for i in "${c_split[@]}"
do
    echo "------ new part ------"
    echo "$i"
done

精细地完成这个任务:

more complex string
AA=A
B=BB
=======
C==CC
DD=D
=======
EEE
FF

split
------ new part ------
AA=A
B=BB
------ new part ------
C==CC
DD=D
------ new part ------
EEE
FF

一些解释:

  • export -a var 定义 var 为数组,并在子进程中共享它们。
  • ${variablename%string*}${variablename%%string*} 返回 variablename 左侧的部分,直到但不包括 string。一个 % 表示 字符串的最后出现,两个 %% 表示 所有出现。如果未找到 string,则返回完整的 variablename
  • ${variablename#*string},以相反的方式执行相同的操作:从 variablename 的末尾返回部分,但不包括 string。一个 # 表示 第一次出现,两个 ## 表示 所有出现

注意,在替换中,字符 * 是一个 通配符,表示任意数量的任何字符。

命令echo "${c%%$'\n'}"将会输出变量c,但不包括字符串末尾的任何数量的换行符。
因此,如果变量包含Hello WorldZorGluBHello youZorGluBI'm happy
variable="Hello WorldZorGluBHello youZorGluBI'm happy"

$ echo ${variable#*ZorGluB}
Hello youZorGlubI'm happy

$ echo ${variable##*ZorGluB}
I'm happy

$ echo ${variable%ZorGluB*}
Hello WorldZorGluBHello you

$ echo ${variable%%ZorGluB*}
Hello World

$ echo ${variable%%ZorGluB}
Hello WorldZorGluBHello youZorGluBI'm happy

$ echo ${variable%happy}
Hello WorldZorGluBHello youZorGluBI'm

$ echo ${variable##* }
happy

所有这些都在手册页中解释:

$ man -Len -Pless\ +/##word bash

$ man -Len -Pless\ +/%%word bash

$ man -Len -Pless\ +/^\\\ *export\\\ .*word bash

逐步分割循环:

分隔符:

mySep=$'======='

声明c_split为一个数组(可以与子元素共享)
export -a c_split

当变量c至少包含一个mySep

while [ "$c" != "${c#*$mySep}" ];do

从第一个mySep截取字符串并将其赋值给part
    part="${c%%$mySep*}"

删除前导换行符

    part="${part##$'\n'}"

从末尾删除换行符,并将结果作为新的数组元素添加到 c_split 中。
    c_split+=("${part%%$'\n'}")

当删除左边到 mySep 的部分时,重新分配 c 和字符串的其余部分。
    c="${c#*$mySep}"

完成了 ;-)

done

删除前导换行符

c=${c##$'\n'}

从末尾删除换行符并将结果作为新的数组元素添加到 c_split 中。
c_split+=("${c%%$'\n'}")

转换为函数:

ssplit() {
    local string="$1" array=${2:-ssplited_array} delim="${3:- }" pos=0
    while [ "$string" != "${string#*$delim}" ];do
        printf -v $array[pos++] "%s" "${string%%$delim*}"
        string="${string#*$delim}"
      done
    printf -v $array[pos] "%s" "$string"
}

使用方法:

ssplit "<quoted string>" [array name] [delimiter string]

其中数组名称默认为$splitted_array分隔符为一个空格。

您可以使用以下方法:

c=$'AA=A\nB=BB\n=======\nC==CC\nDD=D\n=======\nEEE\nFF'
ssplit "$c" c_split $'\n=======\n'
printf -- "--- part ----\n%s\n" "${c_split[@]}"
--- part ----
AA=A
B=BB
--- part ----
C==CC
DD=D
--- part ----
EEE
FF

1
感谢您提供的出色答案和详细的解释。干得好!您肯定应该得到更多赞,但我只能给一张…… :) - user000001
我正在尝试使用这个解决方案,但我的分隔符是一条带有星号(*)的长线。我已经尝试将分隔符更改为正则表达式,但那并不起作用。是否可以像这样做:mySep="/\*{50,}/" - Joudicek Jouda
同时我的字符串是从文件中加载的(多行):c=\cat /home/pi/test.txt``,如果这有所不同。 - Joudicek Jouda

3

使用awk命令来完成:

 awk -vRS='\n=*\n'  '{print "----- new part -----";print}' <<< $c

输出:

kent$  awk -vRS='\n=*\n'  '{print "----- new part -----";print}' <<< $c
----- new part -----
AA=A
B=BB
----- new part -----
C==CC
DD=D
----- new part -----
EEE
FF

感谢你的回答并点赞。但是我不能标记为已接受,因为(请纠正我如果我错了)你在awk中处理每个部分,但你无法将每个部分作为独立的元素返回到一个数组中。你能修改一下吗? - user000001
@user000001 是正确的,只有一个过滤器而没有分割方法:sed $'bb;:a;i----- 新部分 -----\n;1p;d;:b;1ba;/^====*$/ba;' 做同样的事情... - F. Hauri - Give Up GitHub

1

由于这个评论,我在示例文本中添加了一些内容:

如果你将AA=A替换为AA =A或AA=\nA,则会出现错误-另一个人

编辑:我添加了一个建议,该建议不会对文本中的某些分隔符敏感。但是,这并没有使用OP要求的“单行拆分”,而是我应该在bash中执行它并且想要结果存储在数组中的方法。

script.sh(新):

#!/bin/bash

text=$(
  echo "AA=A"; echo "AA =A"; echo "AA=\nA"; echo "B=BB"; echo "=======";
  echo "C==CC"; echo "DD=D"; echo "======="; echo "EEE"; echo "FF";
)
echo "more complex string"
echo "$text"
echo "split now"

c_split[0]=""
current=""
del=""
ind=0

# newline
newl=$'\n'

# Save IFS (not necessary when run as sub shell)
saveIFS="$IFS"
IFS="$newl"
for row in $text; do

  if [[ $row =~ ^=+$ ]]; then
    c_split[$ind]="$current"
    ((ind++))
    current=""
    # Avoid preceding newline
    del=""
    continue
  fi

  current+="$del$row"
  del="$newl"
done

# Restore IFS
IFS="$saveIFS"

# If there is a last poor part of the text
if [[ -n $current ]]; then
  c_split[$ind]="$current"
fi

# The result is an array
for i in "${c_split[@]}"
do
    echo "---- new part ----"
    echo "$i"
done

script.sh(旧版,“一行分割”):
(我从 @Kent 那里借鉴了使用 awk 的想法,并进行了一些调整)

#!/bin/bash

c=$(
  echo "AA=A"; echo "AA =A"; echo "AA=\nA"; echo "B=BB"; echo "=======";
  echo "C==CC"; echo "DD=D"; echo "======="; echo "EEE"; echo "FF";
)
echo "more complex string"
echo "$c"
echo "split now"

# Now, this will be almost absolute secure,
# perhaps except a direct hit by lightning.
del=""
for ch in $'\1' $'\2' $'\3' $'\4' $'\5' $'\6' $'\7'; do
  if [ -z "`echo "$c" | grep "$ch"`" ]; then
    del="$ch"
    break
  fi
done

if [ -z "$del" ]; then
  echo "Sorry, all this testing but no delmiter to use..."
  exit 1
fi

IFS="$del" c_split=($(echo "$c" | awk -vRS="\n=+\n" -vORS="$del" '1'))

for i in ${c_split[@]}
do
  echo "---- new part ----"
  echo "$i"
done

输出:

[244an]$ bash --version
GNU bash, version 4.2.24(1)-release (x86_64-pc-linux-gnu)

[244an]$ ./script.sh
more complex string
AA=A
AA =A
AA=\nA
B=BB
=======
C==CC
DD=D
=======
EEE
FF
split now
---- new part ----
AA=A
AA =A
AA=\nA
B=BB
---- new part ----
C==CC
DD=D
---- new part ----
EEE
FF

没有使用-e来控制echo命令,以使AA=\\nA不换行。


我觉得你说的很接近了。但是如果字符串里面有一个 | 符号就会出错:P 。你能想到一种转义它的方法吗(不影响其他内容)? - user000001
我认为选择哪个字符或序列并不重要,总会存在被放置在“错误”位置的风险。如果想保留这里提出的一些建议而不完全重写它,可以添加一个测试来确保100%正确性。一些代码从字符列表中选择分隔符(如上所示的|),找到第一个不在字符串中的字符。然后使用该字符代替始终使用“|”。例如要使用/测试的字符:|;,&% 等等。 - 244an
已更新答案,但迟早最好用其他语言(如Ruby)更好地编写它。我最终添加了一个更好的(?)替代方案,但这并没有使用split在一行中(正如OP所希望的那样?)。 - 244an
你可以使用\0作为分隔符,因为它在字符串数据中是不允许的(尝试var=$'foo\0bar'; echo "$var")。这就是我建议的答案所做的。 - that other guy
谢谢你的提示,但是\0不起作用,我认为IFS需要除了空字符之外的其他东西。但是我尝试使用\1代替,它似乎可以工作,并且很高兴学到更多关于$''语法的知识,谢谢。我将进行最后一次更新,但也许OP没有真正需要解决的问题,感觉更像是一个小测验。 - 244an

1

以下脚本在bash中测试:

kent@7pLaptop:/tmp/test$ bash --version
GNU bash, version 4.2.42(2)-release (i686-pc-linux-gnu)

脚本:(命名为t.sh

#!/bin/bash

c=$(echo "AA=A"; echo "B=BB"; echo "======="; echo "C==CC"; echo "DD=D"; echo "======="; echo "EEE"; echo "FF";)
echo "more complex string"
echo "$c"
echo "split now"

c_split=($(echo "$c"|awk -vRS="\n=*\n"  '{gsub(/\n/,"\\n");printf $0" "}'))

for i in ${c_split[@]}
do
    echo "---- new part ----"
    echo -e "$i" 
done

输出:

kent@7pLaptop:/tmp/test$ ./t.sh 
more complex string
AA=A
B=BB
=======
C==CC
DD=D
=======
EEE
FF
split now
---- new part ----
AA=A
B=BB
---- new part ----
C==CC
DD=D
---- new part ----
EEE
FF

请注意循环中的echo语句,如果您删除选项-e,则会看到:
---- new part ----
AA=A\nB=BB
---- new part ----
C==CC\nDD=D
---- new part ----
EEE\nFF\n

是否使用-e取决于您的需求。


如果将AA=A替换为AA =AAA=\\nA,那么这个会出错。 - that other guy
正如@thatotherguy所说,使用echo -e的hugly步骤不会很干净,可能会有很多边框效应 - F. Hauri - Give Up GitHub

1
这里有一种方法,当数据包含字面反斜杠序列、空格和其他字符时不会出错:
c=$(echo "AA=A"; echo "B=BB"; echo "======="; echo "C==CC"; echo "DD=D"; echo "======="; echo "EEE"; echo "FF";)
echo "more complex string"
echo "$c";
echo ;
echo "split";

c_split=()
while IFS= read -r -d '' part
do
  c_split+=( "$part" )
done < <(printf "%s" "$c" | sed -e 's/=======/\x00/g')
c_split+=( "$part" )

for i in "${c_split[@]}"
do
    echo "------ new part ------"
    echo "$i"
done

请注意,字符串实际上是按照请求的“=======”进行分割的,因此换行符成为数据的一部分(当“echo”添加自己的换行符时会导致额外的空白行)。

EEE,FF部分在这里丢失了。关于换行/换行符,OP已经给出了预期输出,很清楚。 - Kent

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