我将此字符串存储在变量中:

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

现在,我想将字符串拆分为:;分隔符,以便我可以:

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

我不一定需要ADDR1和ADDR2变量。如果它们是数组的元素,那就更好了。


根据以下答案的建议,我最终得出了以下结论,这就是我想要的:

#!/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)设置为;。我不确定这个答案发生了什么,如何将IFS重置为默认值?

回复:IFS解决方案,我尝试了这个,它奏效了,我保留了旧的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)

在循环中打印时,我只得到了第一个字符串,$in周围没有括号,它可以工作。


当前回答

兼容答案

在bash中有很多不同的方法可以做到这一点。

然而,首先需要注意的是,bash有许多特殊功能(所谓的bashms),这些功能在任何其他shell中都不起作用。

特别是,本篇文章中的解决方案以及线程中的其他解决方案中使用的数组、关联数组和模式替换都是抨击,可能无法在许多人使用的其他外壳下工作。

例如:在我的Debian GNU/Linux上,有一个名为dash的标准shell;我认识很多人,他们喜欢使用另一种叫做ksh的shell;还有一个叫做busybox的特殊工具,带有自己的shell解释器(ash)。

对于posix shell兼容的答案,请转到此答案的最后一部分!

请求的字符串

上述问题中要拆分的字符串是:

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

我将使用此字符串的修改版本,以确保我的解决方案对包含空格的字符串具有鲁棒性,这可能会破坏其他解决方案:

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

基于bash中的分隔符拆分字符串(版本>=4.2)

在纯bash中,我们可以创建一个数组,其中元素由IFS的临时值(输入字段分隔符)分割。除其他外,IFS还告诉bash在定义数组时应将哪些字符视为元素之间的分隔符:

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

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

在较新版本的bash中,用IFS定义前缀命令只会更改该命令的IFS,并在之后立即将其重置为先前的值。这意味着我们可以在一行中完成上述操作:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

我们可以看到,字符串IN已存储在一个名为fields的数组中,以分号分隔:

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

(我们还可以使用declare-p显示这些变量的内容:)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

请注意,读取是执行拆分的最快方式,因为没有调用fork或外部资源。

一旦定义了数组,就可以使用一个简单的循环来处理每个字段(或者说,现在定义的数组中的每个元素):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

或者,您可以在使用移位方法处理后从阵列中删除每个字段,我喜欢:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    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>]

更新:最近的bash>=4.4

在较新版本的bash中,还可以使用命令mapfile:

mapfile -td \; fields < <(printf "%s\0" "$IN")

此语法保留特殊字符、换行符和空字段!

如果不想包含空字段,可以执行以下操作:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

使用mapfile,您还可以跳过声明数组,并隐式“循环”分隔元素,对每个元素调用一个函数:

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.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(注意:如果您不关心字符串末尾的空字段或它们不存在,则格式字符串末尾的\0无效。)

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

或者可以使用<<<,在函数体中包含一些处理来删除它添加的换行符:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

基于shell中的分隔符拆分字符串

如果你不能使用bash,或者你想写一些可以在许多不同的shell中使用的东西,你通常不能使用bashms——这包括我们在上面的解决方案中使用的数组。

然而,我们不需要使用数组来循环字符串的“元素”。在许多shell中,有一种语法用于从模式的第一次或最后一次出现中删除字符串的子字符串。请注意,*是一个通配符,表示零个或多个字符:

(到目前为止发布的任何解决方案中都没有这种方法,这是我写这个答案的主要原因;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

如Score_Bow所述:

#和%分别从字符串的开头和结尾删除可能最短的匹配子字符串,以及##和%%删除可能最长的匹配子字符串。

使用上述语法,我们可以创建一种方法,通过删除分隔符之前或之后的子字符串,从字符串中提取子字符串“元素”。

下面的代码块在bash(包括Mac OS的bash)、dash、ksh、lksh、yash、zsh和busybox的ash中运行良好:

(感谢Adam Katz的评论,使这个循环更加简单!)

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" != "$iter" ] ;do
    # extract the substring from start of string up to delimiter.
    iter=${IN%%;*}
    # delete this first "element" AND next separator, from $IN.
    IN="${IN#$iter;}"
    # Print (or doing anything with) the first "element".
    printf '> [%s]\n' "$iter"
done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

为什么不剪?

cut用于提取大文件中的列,但重复执行fork(var=$(echo…|cut…))会很快变得过火!

这是一个正确的语法,在许多posix shell下使用cut进行测试,如DougW的另一个答案所建议的:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
i=1
while iter=$(echo "$IN"|cut -d\; -f$i) ; [ -n "$iter" ] ;do
    printf '> [%s]\n' "$iter"
    i=$((i+1))
done

我写这个是为了比较执行时间。

在我的树莓皮上,这看起来像:

$ export TIMEFORMAT=$'(%U + %S) / \e[1m%R\e[0m : %P  '
$ time sh splitDemo.sh >/dev/null
(0.000 + 0.019) / 0.019 : 99.63  
$ time sh splitDemo_cut.sh >/dev/null
(0.051 + 0.041) / 0.188 : 48.98  

这里的总执行时间大约是10倍长,使用1个叉进行切割,按场计算!

其他回答

又一个迟来的答案。。。如果你对java有兴趣,这里是bashj(https://sourceforge.net/projects/bashj/)解决方案:

#!/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="bla@some.com;john@home.com"

: 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

有两种简单的方法:

cat "text1;text2;text3" | tr " " "\n"

and

cat "text1;text2;text3" | sed -e 's/ /\n/g'

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

IN="bla@some.com;john@home.com"
arr=(`echo $IN | tr ';' ' '`)

echo ${arr[0]}
echo ${arr[1]}

两种都不需要bash数组的伯尔尼式替代方案:

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

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

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

注意:在第一种情况下,没有分支子进程来帮助列表操作。

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

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

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"

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

编辑:对不起,我在SO上读到POSIX需要perl,所以我认为使用它是合法的。但在unix.stackexchange.com上,一些用户表示perl不是POSIX规范的一部分。

我的解决方案:一个使用perl的拆分来完成工作的函数。

详细评论如下:

#!/bin/bash

# This function is a wrapper for Perl's split.\
# \
# Since we cannot return an array like in Perl,
# it takes the name of the resulting array as last
# argument.\
# \
# See https://perldoc.perl.org/functions/split for usage info
# and examples.\
# \
# If you provide a Perl regexp that contains e. g. an escaped token like \b,
# space(s) and/or capture group(s), it must be quoted, and e. g. /\b/ must
# be single-quoted.\
# Thus, it's best to generally single-quote a Perl regexp.
function split # Args: <Element separator regexp> <string> <array name>
{
    (($# != 3)) && echo "${FUNCNAME[0]}: Wrong number of arguments, returning." && return 1

    local elementSepRE=$1
    local string=$2
    local -n array=$3

    local element i=0

    # Attention! read does Word Splitting on each line!
    # I must admit I didn't know that so far.
    # This removes leading and trailing spaces, exactly
    # what we don't want.
    # Thus, we set IFS locally to newline only.
    local IFS=$'\n'

    while read element; do
        # As opposed to array+=($element),
        # this preserves leading and trailing spaces.
        array[i++]=$element
    done <<<$(_perl_split)
}

# This function calls Perl's split function and prints the elements of the
# resulting array on separate lines.\
# It uses the caller's $elementSepRE and $string.
function _perl_split
{
    # A heredoc is a great way of embedding a Perl script.
    # N.B.: - Shell variables get expanded.
    #         - Thus:
    #           - They must be quoted.
    #           - Perl scalar variables must be escaped.
    #       - The backslash of \n must be escaped to protect it.
    #       - Instead of redirecting a single heredoc to perl, we may
    #         use multiple heredocs with cat within a command group and
    #         pipe the result to perl.
    #         This enables us to conditionally add certain lines of code.

    {
        cat <<-END
            my \$elementSepRE=q($elementSepRE);
        END

        # If $elementSepRE is a literal Perl regexp, qr must be applied
        # to it in order to use it.
        # N.B.: We cannot write this condition in Perl because when perl
        # compiles the script, all statements are checked for validity,
        # no matter if they will actually be executed or not.
        # And if $elementSepRE was e. g. == ', the line below – although
        # not to be executed – would give an error because of an unterminated
        # single-quoted string.
        [[ $elementSepRE =~ ^m?/ && $elementSepRE =~ /[msixpodualn]*$ ]] && cat <<-END
            \$elementSepRE=qr$elementSepRE;
        END

        cat <<-END
            my @array=split(\$elementSepRE, q($string));

            print(\$_ . "\\n") for (@array);
        END
    } | perl
}

对于那些一眼就知道发生了什么的人来说,这一点没有任何评论;)

#!/bin/bash

# This function is a wrapper for Perl's split.\
# \
# Since we cannot return an array like in Perl,
# it takes the name of the resulting array as last
# argument.\
# \
# See https://perldoc.perl.org/functions/split for usage info
# and examples.\
# \
# If you provide a Perl regexp that contains e. g. an escaped token like \b,
# space(s) and/or capture group(s), it must be quoted, and e. g. /\b/ must
# be single-quoted.\
# Thus, it's best to generally single-quote a Perl regexp.
function split # Args: <Element separator regexp> <string> <array name>
{
    (($# != 3)) && echo "${FUNCNAME[0]}: Wrong number of arguments, returning." && return 1

    local elementSepRE=$1
    local string=$2
    local -n array=$3

    local element i=0

    local IFS=$'\n'

    while read element; do
        array[i++]=$element
    done <<<$(_perl_split)
}

function _perl_split
{
    {
        cat <<-END
            my \$elementSepRE=q($elementSepRE);
        END

        [[ $elementSepRE =~ ^m?/ && $elementSepRE =~ /[msixpodualn]*$ ]] && cat <<-END
            \$elementSepRE=qr$elementSepRE;
        END

        cat <<-END
            my @array=split(\$elementSepRE, q($string));

            print(\$_ . "\\n") for (@array);
        END
    } | perl
}