How do I split a string on a delimiter in Bash?
我有这个字符串变量存储在a
现在我想分裂的字符串是由一个分隔符:
1 2 |
我不不需要的
建议从下面的答案后,我来了,下面的是什么:是我。
1 2 3 4 5 6 7 8 9 10 11 | #!/usr/bin/env bash IN="[email protected];[email protected]" mails=$(echo $IN | tr";"" ") for addr in $mails do echo"> [$addr]" done |
输出:
1 2 | > [bla@some.com] > [john@home.com] |
有一个解决方案,包括设置内部_场_
解决方案:一个
1 2 3 4 5 6 7 8 9 10 11 | IN="[email protected];[email protected]" OIFS=$IFS IFS=';' mails2=$IN for x in $mails2 do echo"> [$x]" done IFS=$OIFS |
顺便说一句,当我尝试
1 | mails2=($IN) |
我只把它当打印字符串中的第一环,它在
您可以设置内部字段分隔符(IFS)变量,然后让它解析为一个数组。当在命令中发生这种情况时,对
1 2 3 4 | IFS=';' read -ra ADDR <<<"$IN" for i in"${ADDR[@]}"; do # process"$i" done |
它将解析由
1 2 3 4 5 | while IFS=';' read -ra ADDR; do for i in"${ADDR[@]}"; do # process"$i" done done <<<"$IN" |
取自bash shell脚本拆分数组:
1 2 |
说明:
这个结构用
大括号内使用的语法将每个
有一些常见的问题:
IFS=':'; arrIN=($IN); unset IFS;
IFS=$'
'; arrIN=($IN); unset IFS;
如果您不介意立即处理它们,我喜欢这样做:
1 2 3 4 5 | for i in $(echo $IN | tr";"" ") do # process done |
可以使用这种循环来初始化数组,但可能有一种更简单的方法。不过,希望这能有所帮助。
兼容答案
对于这个问题,在bash中已经有了很多不同的方法来实现这一点。但是bash有许多特殊的特性,即所谓的bashism,可以很好地工作,但是在任何其他shell中都不能工作。
特别是,数组、关联数组和模式替换是纯粹的bashims,在其他shell下可能不起作用。
在我的debian gnu/linux上,有一个叫做dash的标准shell,但我认识许多喜欢使用ksh的人。
最后,在非常小的情况下,有一个特殊的工具叫做busybox,它有自己的shell解释器(ash)。
请求的字符串有问题的字符串示例是:
因为这对空白很有用,而且空白可以修改例程的结果,所以我更喜欢使用这个示例字符串:
1 | IN="[email protected];[email protected];Full Name <[email protected]>" |
基于bash中的分隔符拆分字符串(版本>=4.2)
在纯bash下,我们可以使用数组和ifs:
1 | var="[email protected];[email protected];Full Name <[email protected]>" |
1 2 3 4 5 | oIFS="$IFS" IFS=";" declare -a fields=($var) IFS="$oIFS" unset oIFS |
1 | IFS=\; read -a fields <<<"$IN" |
在最近的bash中使用此语法不会更改当前会话的
1 2 3 | set | grep ^IFS= IFS=$' \t ' |
现在,字符串
1 2 3 | set | grep ^fields=\\\|^var= fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>") var='[email protected];[email protected];Full Name <[email protected]>' |
我们可以通过
1 2 3 | declare -p IN fields declare -- IN="[email protected];[email protected];Full Name <[email protected]>" declare -a fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>") |
从这里,您可以使用已经知道的语法来处理每个字段:
1 2 3 4 5 6 | for x in"${fields[@]}";do echo"> [$x]" done > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] |
或者在处理后删除每个字段(我喜欢这种移位方法):
1 2 3 4 5 6 7 | while ["$fields" ] ;do echo"> [$fields]" fields=("${fields[@]:1}") done > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] |
甚至对于简单的打印输出(较短的语法):
1 2 3 4 5 | printf"> [%s] ""${fields[@]}" > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] |
更新:最近的bash>=4.4
你可以玩
1 | mapfile -td \; fields < <(printf"%s\0""$IN") |
此语法保留特殊字符、换行符和空字段!
如果您不关心空字段,可以:
1 2 3 4 | mapfile -td \; fields <<<"$IN" fields=("${fields[@]%$' '}") # drop ' ' added by '<<<' |
但您可以通过函数使用字段:
1 2 3 4 5 6 7 8 | myPubliMail() { printf"Seq: %6d: Sending mail to '%s'..." $1"$2" # mail -s"This is not a spam...""$2" </path/to/body printf"\e[3D, done. " } mapfile < <(printf"%s\0""$IN") -td \; -c 1 -C myPubliMail |
(注:格式字符串末尾的
1 | mapfile < <(echo -n"$IN") -td \; -c 1 -C myPubliMail |
将呈现如下内容:
1 2 3 | Seq: 0: Sending mail to '[email protected]', done. Seq: 1: Sending mail to '[email protected]', done. Seq: 2: Sending mail to 'Full Name <[email protected]>', done. |
或删除函数中由
1 2 3 4 5 6 7 8 9 10 | myPubliMail() { local seq=$1 dest="${2%$' '}" printf"Seq: %6d: Sending mail to '%s'..." $seq"$dest" # mail -s"This is not a spam...""$dest" </path/to/body printf"\e[3D, done. " } mapfile <<<"$IN" -td \; -c 1 -C myPubliMail |
将呈现相同的输出:
1 2 3 | Seq: 0: Sending mail to '[email protected]', done. Seq: 1: Sending mail to '[email protected]', done. Seq: 2: Sending mail to 'Full Name <[email protected]>', done. |
基于shell中的分隔符拆分字符串
但是如果你想在许多shell下写一些有用的东西,你就不能使用bashims。
在许多shell中有一种语法,用于在子字符串的第一次或最后一次出现处拆分字符串:
1 2 3 4 | ${var#*SubStr} # will drop begin of string up to first occur of `SubStr` ${var##*SubStr} # will drop begin of string up to last occur of `SubStr` ${var%SubStr*} # will drop part of string from last occur of `SubStr` to the end ${var%%SubStr*} # will drop part of string from first occur of `SubStr` to the end |
(这是我发表答卷的主要原因,我没有找到答案;)
正如得分所指出的:
# and% delete the shortest possible matching string, and
## and%% delete the longest possible.where
# and## mean from left (begin) of string, and
% and%% meand from right (end) of string.
这个小示例脚本在bash、dash、ksh、busybox下运行良好,在mac os的bash下也进行了测试:
1 2 3 4 5 6 7 8 9 10 11 | var="[email protected];[email protected];Full Name <[email protected]>" while ["$var" ] ;do iter=${var%%;*} echo"> [$iter]" ["$var" ="$iter" ] && \ var='' || \ var="${var#*;}" done > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] |
玩得高兴!
我看到过一些引用
在将这个特定的示例拆分为bash脚本数组的情况下,
例子:
1 2 3 4 | $ echo"[email protected];[email protected]" | cut -d";" -f 1 bla@some.com $ echo"[email protected];[email protected]" | cut -d";" -f 2 john@home.com |
显然,您可以将其放入一个循环中,并迭代-f参数以独立地拉动每个字段。
当您有一个带如下行的分隔日志文件时,这会更有用:
1 | 2015-04-27|12345|some action|an attribute|meta data |
这对我很有用:
1 2 3 | string="1;2" echo $string | cut -d';' -f1 # output is 1 echo $string | cut -d';' -f2 # output is 2 |
这种方法怎么样:
1 2 3 4 5 6 | IN="[email protected];[email protected]" set --"$IN" IFS=";"; declare -a Array=($*) echo"${Array[@]}" echo"${Array[0]}" echo"${Array[1]}" |
来源
1 2 3 4 |
这也适用于:
1 2 3 | IN="[email protected];[email protected]" echo ADD1=`echo $IN | cut -d \; -f 1` echo ADD2=`echo $IN | cut -d \; -f 2` |
小心,这个解决方案并不总是正确的。如果您只传递"[email protected]",它将把它同时分配给add1和add2。
我认为awk是解决你问题的最好和有效的命令。在几乎每个Linux发行版中,默认情况下,awk都包含在bash中。
1 |
将给予
1 | bla@some.com john@home.com |
当然,您可以通过重新定义awk打印字段来存储每个电子邮件地址。
达伦的回答不同,我是这样做的:
1 2 |
在bash中,这是一种防弹方法,即使变量包含换行符,也可以使用:
1 | IFS=';' read -d '' -ra array < <(printf '%s;\0'"$in") |
看:
1 2 3 4 5 6 7 8 | $ in=$'one;two three;*;there is a newline in this field' $ IFS=';' read -d '' -ra array < <(printf '%s;\0'"$in") $ declare -p array declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is a newline in this field")' |
这项工作的诀窍是使用带有空分隔符的EDOCX1(分隔符)的
1 2 3 4 | $ in='one;two;three;' # there's an empty field $ IFS=';' read -d '' -ra array < <(printf '%s;\0'"$in") $ declare -p array declare -a array='([0]="one" [1]="two" [2]="three" [3]="")' |
保留尾随的空字段。
bash更新≥4.4自bash 4.4以来,内置的
1 | mapfile -d ';' -t array < <(printf '%s;'"$in") |
如果不使用数组,那么这个单行程序怎么样:
1 | IFS=';' read ADDR1 ADDR2 <<<$IN |
这是一个干净的三衬板:
1 2 3 | in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof" IFS=';' list=($in) for item in"${list[@]}"; do echo $item; done |
其中,
如果在这之后还有代码,还需要恢复
不设国际单项体育联合会
如果你只有一个结肠,你可以这样做:
1 2 3 | a="foo:bar" b=${a%:*} c=${a##*:} |
你会得到:
1 2 | b = foo c = bar |
下面的bash/zsh函数将其第一个参数拆分为第二个参数给定的分隔符:
1 2 3 4 5 6 7 8 9 10 11 | split() { local string="$1" local delimiter="$2" if [ -n"$string" ]; then local part while read -d"$delimiter" part; do echo $part done <<<"$string" echo $part fi } |
例如,命令
1 | $ split 'a;b;c' ';' |
产量
1 2 3 | a b c |
例如,该输出可以通过管道传输到其他命令。例子:
1 2 3 4 | $ split 'a;b;c' ';' | cat -n 1 a 2 b 3 c |
与给出的其他解决方案相比,本方案具有以下优点:
IFS 不被重写:由于偶数个局部变量的动态范围,在循环上重写IFS 会导致新值泄漏到从循环内执行的函数调用中。不使用数组:使用
read 将字符串读取到数组中需要bash中的标志-a ,zsh中的标志-a 。
如果需要,可以将函数放入脚本中,如下所示:
1 2 3 4 5 6 7 | #!/usr/bin/env bash split() { # ... } split"$@" |
有一种简单而聪明的方法:
1 | echo"add:sfff" | xargs -d: -i echo {} |
但您必须使用gnu-xargs,bsd-xargs不能支持-d delim。如果你像我一样使用苹果Mac。可以安装GNU xargs:
1 | brew install findutils |
然后
1 | echo"add:sfff" | gxargs -d: -i echo {} |
这是最简单的方法。
1 2 3 4 5 6 | spo='one;two;three' OIFS=$IFS IFS=';' spo_array=($spo) IFS=$OIFS echo ${spo_array[*]} |
你可以在很多情况下使用awk
1 2 3 |
你也可以用这个
1 2 |
这里有一些很酷的答案(特别是勘误表),但是对于类似于用其他语言拆分的内容——这就是我认为最初的问题的意思——我已经解决了这个问题:
1 2 |
现在,
1 | for i in ${a[*]}; do echo $i; done |
重要注意事项:
这在没有空间担心的情况下是有效的,解决了我的问题,但可能无法解决你的问题。在这种情况下,使用
1 2 3 4 5 6 7 | IN="[email protected];[email protected]" IFS=';' read -a IN_arr <<<"${IN}" for entry in"${IN_arr[@]}" do echo $entry done |
产量
1 2 | bla@some.com john@home.com |
系统:Ubuntu 12.04.1
如果没有空间,为什么不呢?
1 2 3 4 5 | IN="[email protected];[email protected]" arr=(`echo $IN | tr ';' ' '`) echo ${arr[0]} echo ${arr[1]} |
两种不需要bash数组的Bourne-ish替代方案:
案例1:保持简单明了:使用换行符作为记录分隔符…如。
1 2 3 4 5 6 7 | IN="[email protected] [email protected]" while read i; do # process"$i" ... eg. echo"[email:$i]" done <<<"$IN" |
注意:在第一种情况下,没有分支子流程来协助列表操作。
想法:也许在内部广泛使用nl是值得的,并且在外部生成最终结果时,只需转换为不同的rs。
案例2:使用";"作为记录分隔符…如。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | NL=" " IRS=";" ORS=";" conv_IRS() { exec tr"$1""$NL" } conv_ORS() { exec tr"$NL""$1" } IN="[email protected];[email protected]" IN="$(conv_IRS";" <<<"$IN")" while read i; do # process"$i" ... eg. echo -n"[email:$i]$ORS" done <<<"$IN" |
在这两种情况下,子列表都可以在循环完成后在循环内持久化。这在处理内存中的列表时非常有用,而不是将列表存储在文件中。P.S.保持冷静,继续b-)
1 2 3 4 5 6 7 8 9 | IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? are no problem;simple is beautiful :-)' set -f oldifs="$IFS" IFS=';'; arrayIN=($IN) IFS="$oldifs" for i in"${arrayIN[@]}"; do echo"$i" done set +f |
输出:
1 2 3 4 5 | bla@some.com john@home.com Charlie Brown <cbrown@acme.com !"#$%&/()[]{}*? are no problem simple is beautiful :-) |
解释:使用括号()的简单赋值将分号分隔的列表转换为数组,前提是在执行此操作时具有正确的IFS。循环的标准像往常一样处理该数组中的单个项。请注意,为in变量提供的列表必须是"硬"引用的,也就是说,只有一个勾号。
必须保存和恢复ifs,因为bash不将赋值视为命令。另一种解决方法是将赋值包装在函数中,并使用修改后的ifs调用该函数。在这种情况下,不需要单独保存/恢复IFS。感谢"bize"指出这一点。
除了已经提供的奇妙答案外,如果只是打印出数据的问题,您可以考虑使用
1 2 | awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s] ", $i)}' <<<"$IN" |
这将字段分隔符设置为
1 2 3 4 5 | $ IN="[email protected];[email protected]" $ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s] ", $i)}' <<<"$IN" > [bla@some.com] > [john@home.com] |
使用其他输入:
1 2 3 4 5 6 7 | $ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s] ", $i)}' <<<"a;b;c d;e_;f" > [a] > [b] > [c d] > [e_] > [f] |
在Android Shell中,大多数建议的方法都不起作用:
1 2 | $ IFS=':' read -ra ADDR <<<"$PATH" /system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory |
工作内容是:
1 2 3 4 5 6 | $ for i in ${PATH//:/ }; do echo $i; done /sbin /vendor/bin /system/sbin /system/bin /system/xbin |
其中,
好吧,伙计们!
这是我的答案!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | DELIMITER_VAL='=' read -d '' F_ABOUT_DISTRO_R <<"EOF" DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=trusty DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS" NAME="Ubuntu" VERSION="14.04.4 LTS, Trusty Tahr" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 14.04.4 LTS" VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" EOF SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf"%s ", $i}}' <<<"${F_ABOUT_DISTRO_R}") while read -r line; do SPLIT+=("$line") done <<<"$SPLIT_NOW" for i in"${SPLIT[@]}"; do echo"$i" done |
为什么这种方法对我来说是"最好的"?
因为两个原因:
[]
使用
1 2 3 |
然后,让聚会开始:
1 2 3 | echo $# for a; do echo $a; done ADDR1=$1 ADDR2=$2 |
这甚至可以处理空白:
1 | IFS=';' read ADDR1 ADDR2 <<< $(echo ${IN}) |
将由";"分隔的字符串拆分为数组的一行程序是:
1 2 3 4 | IN="[email protected];[email protected]" ADDRS=( $(IFS=";" echo"$IN") ) echo ${ADDRS[0]} echo ${ADDRS[1]} |
这只在子shell中设置ifs,因此您不必担心保存和恢复其值。
也许不是最优雅的解决方案,但适用于
1 2 3 4 5 | IN="bla@so me.com;*;[email protected]" for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))` do echo"> [`echo $IN | cut -d';' -f$i`]" done |
输出
1 2 3 | > [bla@so me.com] > [*] > [john@home.com] |
其他示例(开始和结束处的分隔符):
1 2 3 4 5 6 |
基本上,它删除除
它为我工作:
echo $PATH | ruby -ne 'puts $_.split(":")'
又一个迟来的答案…如果你是Java意识的,这里是BasHJ(HTTPS://SooSurfGe.NET/PrimeSt/BasHJ/)解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #!/usr/bin/bashj #!java private static String[] cuts; private static int cnt=0; public static void split(String words,String regexp) {cuts=words.split(regexp);} public static String next() {return(cnt<cuts.length ? cuts[cnt++] :"null");} #!bash IN="[email protected];[email protected]" : j.split($IN,";") # java method call while true do NAME=j.next() # java method call if [ $NAME != null ] ; then echo $NAME ; else exit ; fi done |
有两种简单的方法:
1 2 | cat"text1;text2;text3" | tr""" " |
和
1 2 | cat"text1;text2;text3" | sed -e 's/ / /g' |