关于shell:如何在bash中分割分隔符上的字符串?

How do I split a string on a delimiter in Bash?

我有这个字符串变量存储在a

现在我想分裂的字符串是由一个分隔符:;

我不不需要的ADDR1ADDR2变量。如果他们是元素的数组这是甚至更好。

建议从下面的答案后,我来了,下面的是什么:是我。

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]

有一个解决方案,包括设置内部_场_ ;分离器(IFS)。我不知道要发生什么事了吗,你怎么IFS重置回默认?

解决方案:一个IFS",这和它的作品,我把旧IFS然后恢复它:

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)

我只把它当打印字符串中的第一环,它在$IN没有括号。


您可以设置内部字段分隔符(IFS)变量,然后让它解析为一个数组。当在命令中发生这种情况时,对IFS的分配只发生在该单个命令的环境(对read的环境)中。然后,它根据IFS变量值将输入解析为一个数组,然后我们可以迭代该数组。

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

它将解析由;分隔的一行项目,并将其推送到一个数组中。用于处理整个$IN的资料,每次由;分隔的一行输入:

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
IN="[email protected];[email protected]"
arrIN=(${IN//;/ })

说明:

这个结构用' '替换字符串IN中所有出现的';'(初始//表示全局替换),然后将空格分隔的字符串解释为一个数组(这就是周围括号所做的)。

大括号内使用的语法将每个';'字符替换为' '字符,称为参数扩展。

有一些常见的问题:

  • 如果原始字符串有空格,则需要使用ifs:
    • IFS=':'; arrIN=($IN); unset IFS;
  • 如果原始字符串有空格并且分隔符是新行,则可以使用以下命令设置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)。

    请求的字符串

    有问题的字符串示例是:

    因为这对空白很有用,而且空白可以修改例程的结果,所以我更喜欢使用这个示例字符串:

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

    在纯bash下,我们可以使用数组和ifs:

    1
    2
    3
    4
    5
    oIFS="$IFS"
    IFS=";"
    declare -a fields=($var)
    IFS="$oIFS"
    unset oIFS

    1
    IFS=\; read -a fields <<<"$IN"

    在最近的bash中使用此语法不会更改当前会话的$IFS,但只会更改当前命令:

    1
    2
    3
    set | grep ^IFS=
    IFS=$' \t
    '

    现在,字符串var被拆分并存储到一个数组(名为fields)中:

    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]>'

    我们可以通过declare -p请求可变内容:

    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]>")

    read是最快速的分割方法,因为没有分叉,也没有调用外部资源。

    从这里,您可以使用已经知道的语法来处理每个字段:

    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

    你可以玩mapfile

    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

    (注:格式字符串末尾的\0是无用的,而您不关心字符串末尾的空字段)

    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.

    或删除函数中由<<<bash语法添加的换行符:

    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>]

    玩得高兴!


    我看到过一些引用cut命令的答案,但它们都被删除了。有点奇怪,没有人详细阐述过这一点,因为我认为这是执行此类操作的更有用的命令之一,尤其是解析定界日志文件。

    在将这个特定的示例拆分为bash脚本数组的情况下,tr可能更有效,但可以使用cut,如果您想从中间提取特定的字段,则更有效。

    例子:

    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

    cut非常方便,可以使用cat这个文件并选择一个特定的字段进行进一步处理。


    这对我很有用:

    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
    echo"[email protected];[email protected]" | sed -e 's/;/
    /g'

    bla@some.com
    john@home.com


    这也适用于:

    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
    echo"[email protected];[email protected]" | awk -F';' '{print $1,$2}'

    将给予

    1
    bla@some.com john@home.com

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


    达伦的回答不同,我是这样做的:

    1
    2
    IN="[email protected];[email protected]"
    read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)


    在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(分隔符)的-d选项,以便read被强制读取其输入的所有内容。我们给read加上变量in的内容,由于printf没有后继换行。注意,我们还在printf中放置分隔符,以确保传递给read的字符串有一个尾随的分隔符。没有它,read将减少潜在的尾随空字段:

    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以来,内置的mapfile(aka readarray支持-d选项来指定分隔符。因此,另一个典型的方法是:

    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

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

    如果在这之后还有代码,还需要恢复$IFS,例如unset IFS


    不设国际单项体育联合会

    如果你只有一个结肠,你可以这样做:

    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
    echo"[email protected];[email protected]"|awk -F';' '{printf"%s
    %s
    ", $1, $2}'

    你也可以用这个

    1
    2
    echo"[email protected];[email protected]"|awk -F';' '{print $1,$2}' OFS="
    "


    这里有一些很酷的答案(特别是勘误表),但是对于类似于用其他语言拆分的内容——这就是我认为最初的问题的意思——我已经解决了这个问题:

    1
    2
    IN="[email protected];[email protected]"
    declare -a a="(${IN/;/ })";

    现在,${a[0]}${a[1]}等,如您所料。用${#a[*]}表示术语数。或者迭代,当然:

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

    重要注意事项:

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


    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"指出这一点。


    除了已经提供的奇妙答案外,如果只是打印出数据的问题,您可以考虑使用awk

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

    这将字段分隔符设置为;,以便它可以使用for循环遍历字段并相应地打印。

    试验

    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

    为什么这种方法对我来说是"最好的"?

    因为两个原因:

  • 您不需要转义分隔符;
  • 您不会对空格有问题。该值将在数组中正确分隔!
  • []


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

    1
    2
    3
    IN="[email protected];[email protected]"
    IFS=';'; set $IN; IFS=$' \t
    '

    然后,让聚会开始:

    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,因此您不必担心保存和恢复其值。


    也许不是最优雅的解决方案,但适用于*和spaces:

    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
    IN=";bla@so me.com;*;[email protected];"
    > []
    > [bla@so me.com]
    > [*]
    > [john@home.com]
    > []

    基本上,它删除除;之外的所有字符,使delims例如;;;。然后按${#delims}的计数,从1number-of-delimiters进行for循环。最后一步是使用cut安全地获得$i第部分。


    它为我工作:

    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'