如何在Bash中的分隔符上分割string?

我有这个string存储在一个variables:

IN="bla@some.com;john@home.com" 

现在我想分割string; 定界符,使我有:

 ADDR1="bla@some.com" ADDR2="john@home.com" 

我不一定需要ADDR1ADDR2variables。 如果它们是一个更好的数组的元素。


经过下面答案的build议后,我得到了以下的结果:

 #!/usr/bin/env bash IN="bla@some.com;john@home.com" mails=$(echo $IN | tr ";" "\n") for addr in $mails do echo "> [$addr]" done 

输出:

 > [bla@some.com] > [john@home.com] 

有一个涉及设置Internal_field_separator (IFS)的解决scheme; 。 我不确定这个答案是怎么回事,你如何将IFS重置为默认?

RE: IFS解决scheme,我试过这个,它工作,我保留旧的IFS ,然后恢复它:

 IN="bla@some.com;john@home.com" OIFS=$IFS IFS=';' mails2=$IN for x in $mails2 do echo "> [$x]" done IFS=$OIFS 

顺便说一句,当我尝试

 mails2=($IN) 

在循环打印时,只有第一个string,没有$IN左右括号。

您可以设置内部字段分隔符 (IFS)variables,然后让它parsing成一个数组。 当这种情况发生在一个命令中,那么对IFS的赋值只发生在单个命令的环境( read )上。 然后它根据IFSvariables值将inputparsing成一个数组,然后我们可以迭代。

 IFS=';' read -ra ADDR <<< "$IN" for i in "${ADDR[@]}"; do # process "$i" done 

它将parsing由一行隔开的项目; ,把它推到一个数组中。 用于处理整个$IN ,每次用一行input隔开;

  while IFS=';' read -ra ADDR; do for i in "${ADDR[@]}"; do # process "$i" done done <<< "$IN" 

采取从Bash shell脚本拆分数组

 IN="bla@some.com;john@home.com" arrIN=(${IN//;/ }) 

说明:

这种构造取代了所有的';' (最初的//意思是全局replace),然后将空格分隔的string解释为一个数组(这是括号内的括号)。

在花括号里面使用的语法来replace每个';' 带有' '字符的字符称为参数扩展 。

有一些常见的问题:

  1. 如果原始string有空格,则需要使用IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. 如果原始string有空格分隔符是新行,则可以使用以下命令设置IFS :
    • IFS=$'\n'; arrIN=($IN); unset IFS;

如果你不介意处理,我喜欢这样做:

 for i in $(echo $IN | tr ";" "\n") do # process done 

你可以使用这种循环来初始化一个数组,但可能有一个更简单的方法来做到这一点。 希望这有助于,虽然。

兼容的答案

对于这个问题,在bash中已经有很多不同的方法来做到这一点。 但bash有许多特殊的function,所谓的bashism运行良好,但是在其他shell中不起作用。 特别是, 数组关联数组模式replace都是纯粹的双方,并且可能无法在其他shell下工作。

在我的Debian GNU / Linux上 ,有一个叫做dash的标准 shell,但是我知道很多喜欢使用ksh的人 。

最后,在非常小的情况下,有一个叫做busybox的特殊工具,带有自己的shell解释器( ash )。

请求的string

SO问题中的string示例是:

 IN="bla@some.com;john@home.com" 

由于这可能对空格有用,并且由于空格可以修改例程的结果,所以我更喜欢使用这个示例string:

  IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>" 

根据bash中的分隔符分割string(version> = 4.2)

纯粹的 bash下,我们可以使用数组IFS

 var="bla@some.com;john@home.com;Full Name <fulnam@other.org>" 
 oIFS="$IFS" IFS=";" declare -a fields=($var) IFS="$oIFS" unset oIFS 
 IFS=\; read -a fields <<<"$var" 

在最近的bash下使用这个语法不会改变当前会话的$IFS ,但只能用于当前的命令:

 set | grep ^IFS= IFS=$' \t\n' 

现在stringvar被分割并存储到一个数组(名为fields )中:

 set | grep ^fields=\\\|^var= fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>") var='bla@some.com;john@home.com;Full Name <fulnam@other.org>' 

这是做这件事最快捷的方法,因为没有叉子 ,也没有外部的资源。

从那里,你可以使用你已经知道的语法来处理每个字段;

 for x in "${fields[@]}";do echo "> [$x]" done > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] 

或者在处理之后丢弃每个字段(我喜欢这种转换方法):

 while [ "$fields" ] ;do echo "> [$fields]" fields=("${fields[@]:1}") done > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] 

甚至是简单的打印输出(短语法):

 printf "> [%s]\n" "${fields[@]}" > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] 

根据shell中的分隔符分割string

但是,如果你要在许多shell下写一些可用的东西,你不得不使用bashisms

在许多shell中有一个语法用于分割stringaccros 首次最后一次发生的子string:

 ${var#*SubStr} # will drop begin of string upto first occur of `SubStr` ${var##*SubStr} # will drop begin of string upto 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 

(这是我的答案出版物的主要原因;)

这个小示例脚本在bash , dash , ksh , busybox下工作得很好,并在Mac-OS的bash下也进行了testing:

 var="bla@some.com;john@home.com;Full Name <fulnam@other.org>" while [ "$var" ] ;do iter=${var%%;*} echo "> [$iter]" [ "$var" = "$iter" ] && \ var='' || \ var="${var#*;}" done > [bla@some.com] > [john@home.com] > [Full Name <fulnam@other.org>] 

玩的开心!

这种方法如何:

 IN="bla@some.com;john@home.com" set -- "$IN" IFS=";"; declare -a Array=($*) echo "${Array[@]}" echo "${Array[0]}" echo "${Array[1]}" 

资源

这也适用:

 IN="bla@some.com;john@home.com" echo ADD1=`echo $IN | cut -d \; -f 1` echo ADD2=`echo $IN | cut -d \; -f 2` 

小心,这个解决scheme并不总是正确的。 如果您仅通过“bla@some.com”,则将其分配给ADD1和ADD2。

 echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g' bla@some.com john@home.com 

我已经看到了几个参考cut命令的答案,但是他们都被删除了。 有一点奇怪,没有人详细说明这一点,因为我认为这是做这种事情的更有用的命令之一,特别是parsing分隔的日志文件。

在将这个特定的例子分割成bash脚本数组的情况下, tr可能更有效,但是可以使用cut ,并且如果要从中间拉特定的字段,则更有效。

例:

 $ echo "bla@some.com;john@home.com" | cut -d ";" -f 1 bla@some.com $ echo "bla@some.com;john@home.com" | cut -d ";" -f 2 john@home.com 

你显然可以把它放到一个循环中,并迭代-f参数来独立地拉取每个字段。

当你有一个像这样的行的分隔日志文件时,这会变得更有用:

 2015-04-27|12345|some action|an attribute|meta data 

cut是非常方便的,可以cat这个文件,并select一个特定的领域进一步处理。

这对我工作:

 string="1;2" echo $string | cut -d';' -f1 # output is 1 echo $string | cut -d';' -f2 # output is 2 

我认为AWK是解决您的问题的最好,最有效率的命令。 在几乎所有Linux发行版中,AWK都默认包含在Bash中。

 echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}' 

会给

 bla@some.com john@home.com 

当然,您可以通过重新定义awk打印字段来存储每个电子邮件地址。

对Darron的回答有一个不同的看法,这就是我的做法:

 IN="bla@some.com;john@home.com" read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN) 

在Bash中,一个防弹的方法,即使你的variables包含换行符也可以工作:

 IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in") 

看:

 $ in=$'one;two three;*;there is\na newline\nin 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")' 

这个工作的技巧是使用带有空分隔符的read (分隔符)的-d选项,以便read被强制读取所有提供的内容。 而且我们提供的read完全是variables的内容,没有拖尾换行感谢printf 。 请注意,我们也将分隔符放在printf以确保传递给read的string具有尾随分隔符。 没有它, read将修剪潜在的空白字段:

 $ 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以来,内buildmapfile (又名readarray )支持-d选项来指定分隔符。 因此,另一个规范的方法是:

 mapfile -d ';' -t array < <(printf '%s;' "$in") 

这里是一个干净的3class轮:

 in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof" IFS=';' list=($in) for item in "${list[@]}"; do echo $item; done 

IFS根据分隔符分隔单词, ()用于创build一个数组 。 然后[@]用来作为单独的单词返回每个项目。

如果您之后有任何代码,您还需要恢复$IFS ,例如未unset IFS

如果你不使用数组,那这个class轮呢?

 IFS=';' read ADDR1 ADDR2 <<<$IN 

没有设置IFS

如果你只有一个冒号,你可以这样做:

 a="foo:bar" b=${a%:*} c=${a##*:} 

你会得到:

 b = foo c = bar 

有这样一个简单而巧妙的方法:

 echo "add:sfff" | xargs -d: -i echo {} 

但是你必须使用gnu xargs,BSD xargs不能支持-d delim。 如果你像我一样使用苹果mac。 你可以安装gnu xargs:

 brew install findutils 

然后

 echo "add:sfff" | gxargs -d: -i echo {} 

这是最简单的方法。

 spo='one;two;three' OIFS=$IFS IFS=';' spo_array=($spo) IFS=$OIFS echo ${spo_array[*]} 

下面的Bash / zsh函数将第一个参数分割为第二个参数给定的分隔符:

 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 } 

例如,命令

 $ split 'a;b;c' ';' 

产量

 a b c 

例如,这个输出可以被传送给其他命令。 例:

 $ split 'a;b;c' ';' | cat -n 1 a 2 b 3 c 

与其他解决scheme相比,这个解决scheme具有以下优点:

  • IFS未被覆盖:由于即使是局部variables的dynamic作用域,覆盖循环中的IFS也会导致新值泄漏到循环中执行的函数调用中。

  • 不使用数组:使用read将string读入数组需要在Bash中使用-a标志,在zsh使用-A

如果需要的话,可以按如下方式将函数放入脚本中:

 #!/usr/bin/env bash split() { # ... } split "$@" 

如果没有空间,为什么不呢?

 IN="bla@some.com;john@home.com" arr=(`echo $IN | tr ';' ' '`) echo ${arr[0]} echo ${arr[1]} 
 IN="bla@some.com;john@home.com" IFS=';' read -a IN_arr <<< "${IN}" for entry in "${IN_arr[@]}" do echo $entry done 

产量

 bla@some.com john@home.com 

系统:Ubuntu 12.04.1

这里有一些很酷的答案(尤其是errator),但是对于类似于其他语言的分裂的东西 – 这就是我原来的问题的意思 – 我在这个问题上解决了:

 IN="bla@some.com;john@home.com" declare -aa="(${IN/;/ })"; 

现在${a[0]}${a[1]}等等,就像你期望的那样。 使用${#a[*]}作为条款数量。 或者当然重复:

 for i in ${a[*]}; do echo $i; done 

重要的提示:

这在没有空间担心的情况下工作,这解决了我的问题,但可能无法解决你的问题。 在这种情况下,使用$IFS解决scheme。

使用内置的set来加载$@数组:

 IN="bla@some.com;john@home.com" IFS=';'; set $IN; IFS=$' \t\n' 

然后,让派对开始:

 echo $# for a; do echo $a; done ADDR1=$1 ADDR2=$2 

两个都不需要bash数组的bourne-ish选项:

案例1 :保持简洁:使用NewLine作为logging分隔符。

 IN="bla@some.com john@home.com" while read i; do # process "$i" ... eg. echo "[email:$i]" done <<< "$IN" 

注意:在第一种情况下,没有任何subprocess分叉来协助列表操作。

想法:也许值得在内部广泛使用NL,并且在外部产生最终结果时仅转换为不同的RS。

案例2 :使用“;” 作为logging分隔符…例如。

 NL=" " IRS=";" ORS=";" conv_IRS() { exec tr "$1" "$NL" } conv_ORS() { exec tr "$NL" "$1" } IN="bla@some.com;john@home.com" IN="$(conv_IRS ";" <<< "$IN")" while read i; do # process "$i" ... eg. echo -n "[email:$i]$ORS" done <<< "$IN" 

在这两种情况下,可以在循环内组成一个子列表,在循环完成后持久化。 这在处理内存中的列表时非常有用,而不是将列表存储在文件中。 {ps保持冷静,继续B-)}

除了已经提供的奇妙答案之外,如果仅仅是打印数据,你可能会考虑使用awk

 awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN" 

这将字段分隔符设置为; ,所以它可以通过for循环遍历字段并for相应的打印。

testing

 $ IN="bla@some.com;john@home.com" $ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN" > [bla@some.com] > [john@home.com] 

另外input:

 $ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;cd;e_;f" > [a] > [b] > [cd] > [e_] > [f] 

在Android shell中,大部分build议的方法都不起作用:

 $ IFS=':' read -ra ADDR <<<"$PATH" /system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory 

什么工作是:

 $ for i in ${PATH//:/ }; do echo $i; done /sbin /vendor/bin /system/sbin /system/bin /system/xbin 

//表示全局replace。

单行划分由';'分隔的string 进入一个数组是:

 IN="bla@some.com;john@home.com" ADDRS=( $(IFS=";" echo "$IN") ) echo ${ADDRS[0]} echo ${ADDRS[1]} 

这只能在一个子shell中设置IFS,所以你不必担心保存和恢复它的价值。

 IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? 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 

输出:

 bla@some.com john@home.com Charlie Brown <cbrown@acme.com !"#$%&/()[]{}*? are no problem simple is beautiful :-) 

说明:使用括号()的简单赋值可以将分号分隔列表转换为数组,只要您有正确的IFS。 标准FOR循环像往常一样处理该数组中的单个项目。 注意给INvariables的列表必须是“硬”的,也就是说,只有一个刻度。

IFS必须保存和恢复,因为Bash不会像命令一样处理赋值。 另一种解决方法是将分配包装在一个函数中,并用一个修改后的IFS调用该函数。 在这种情况下,不需要单独保存/恢复IFS。 感谢“Bize”的指出。

也许不是最优雅的解决scheme,但与*和空格:

 IN="bla@so me.com;*;john@home.com" for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))` do echo "> [`echo $IN | cut -d';' -f$i`]" done 

输出

 > [bla@so me.com] > [*] > [john@home.com] 

Other example (delimiters at beginning and end):

 IN=";bla@so me.com;*;john@home.com;" > [] > [bla@so me.com] > [*] > [john@home.com] > [] 

Basically it removes every character other than ; making delims eg. ;;; 。 Then it does for loop from 1 to number-of-delimiters as counted by ${#delims} . The final step is to safely get the $i th part using cut .

Okay guys!

Here's my answer!

 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\n", $i}}' <<<"${F_ABOUT_DISTRO_R}") while read -r line; do SPLIT+=("$line") done <<< "$SPLIT_NOW" for i in "${SPLIT[@]}"; do echo "$i" done 

Why this approach is "the best" for me?

Because of two reasons:

  1. You do not need to escape the delimiter;
  2. You will not have problem with blank spaces . The value will be properly separated in the array!

[]'s

Python version:

python -c 'from __future__ import print_function ; f = open("your-file"); [print(a) for a in f.read().split()]'