IT-Swarm.Net

如何在Bash中的分隔符上拆分字符串?

我把这个字符串存储在一个变量中:

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

现在我想通过;分隔符拆分字符串,以便我有:

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

我不一定需要ADDR1ADDR2变量。如果它们是阵列的元素甚至更好。


根据以下答案的建议,我最终得到了以下内容,这就是我所追求的:

#!/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重置为默认值?

RE: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周围没有括号。

1733
stefanB

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

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

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

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

取自Bash Shell脚本拆分数组

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

说明:

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

花括号内部用于将每个';'字符替换为' '字符的语法称为 参数扩展

有一些常见的问题:

  1. 如果原始字符串有空格,则需要使用 _ ifs _ :[。_____。]
    • IFS=':'; arrIN=($IN); unset IFS;
  2. 如果原始字符串有空格分隔符是一个新行,你可以设置 _ ifs _ with:[。_____。]
    • IFS=$'\n'; arrIN=($IN); unset IFS;
843
palindrom

如果您不介意立即处理它们,我喜欢这样做:

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

您可以使用这种循环来初始化数组,但可能有一种更简单的方法。但希望这会有所帮助。

220
Chris Lutz

兼容的答案

对于这个SO问题,在 bash 中已经有很多不同的方法可以做到这一点。但是bash有很多特殊特性,所谓的bashism运行良好,但是在任何其他 Shell 中都不行。

特别是,数组关联数组模式替换是纯bashisms,可能无法在其他shell下工作_。

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

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

请求的字符串

SO问题中的字符串示例是:

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

因为这可能对whitespaceswhitespaces有用,可以修改例程的结果,我更喜欢使用这个示例字符串:

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

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

pure 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 <<<"$IN"

在最近的bash下使用此语法不会为当前会话更改$IFS,但仅针对当前命令:

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

现在字符串var被拆分并存储到一个数组中(名为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>'

我们可以使用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>")

read是进行拆分的最快方式,因为没有forks并且没有调用外部资源。

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

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

更新:最近 bash > = 4.4

你可以玩mapfile

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

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

如果你不关心空字段,你可以:

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

但你可以通过函数使用字段:

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

(Nota:格式字符串末尾的\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.

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

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

将呈现相同的输出:

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中的分隔符拆分字符串

但是如果你想在许多shell下编写可用的东西,你必须 not use bashisms

在许多shell中使用了一种语法,用于在firstlast substring of substring中分割字符串:

${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

(缺少这是我的答案发布的主要原因;)

正如 Score_Under 指出:

#%删除最短的匹配字符串,并且

##%%删除最长的可能。

其中###表示 从左边 (开始)字符串,并且

%%% meand 从右边 (结束)字符串。

这个小样本脚本在 bashdashkshbusybox 下运行良好,并且在Mac-OS的bash下进行了测试:

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

玩得开心!

159
F. Hauri

我已经看到了几个引用cut命令的答案,但它们都被删除了。没有人详细说明这一点有点奇怪,因为我认为它是执行此类事情的更有用的命令之一,尤其是用于解析分隔的日志文件。

在将此特定示例拆分为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此文件并选择特定字段进行进一步处理。

124
DougW

这对我有用:

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

这种方法怎么样:

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

资源

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

这也有效:

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

小心,这个解决方案并不总是正确的。如果您仅传递“bla@some.com”,它会将其分配给ADD1和ADD2。

61
Ashok

我认为 _ awk _ 是解决问题的最佳和最有效的命令。几乎每个Linux发行版中都默认将AWK包含在Bash中。

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

会给

bla@some.com john@home.com

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

38
Tony

一个不同的看法 达隆的答案 ,这就是我这样做的:

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

在Bash中,一种防弹方式,即使您的变量包含换行符也可以使用:

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(delimiter)的-d选项,以便强制read读取它所提供的所有内容。我们将read与变量in的内容完全一致,并且由于printf而没有尾随换行符。请注意,我们还将分隔符放在printf中,以确保传递给read的字符串具有尾随分隔符。没有它,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开始,内置mapfile(又名readarray)支持-d选项来指定分隔符。因此另一种规范方式是:

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

如果您没有使用数组,这个衬垫怎么样:

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

这是一个干净的3班轮:

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

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

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

19
kenorb

没有设置IFS

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

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

你会得到:

b = foo
c = bar
16
Emilien Brigand

以下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

与给出的其他解决方案相比,这个解决方案具有以下优点:

  • IFS未被覆盖:由于偶数局部变量的动态范围,在循环上覆盖IFS会导致新值泄漏到从循环内执行的函数调用。

  • 不使用数组:使用read将字符串读入数组需要Bash中的标志-a和zsh中的-A

如果需要,可以将该函数放入脚本中,如下所示:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"
8
Halle Knast

有一个简单而聪明的方式:

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

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

brew install findutils

然后

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

你可以在许多情况下应用awk

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

你也可以用它

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"
6
shuaihanhungry

这是最简单的方法。

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

这里有一些很酷的答案(errator esp。),但是对于类似于在其他语言中拆分的东西 - 这就是我原来的问题意思 - 我已经解决了这个问题:

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

现在${a[0]}${a[1]}等正如您所期望的那样。使用${#a[*]}表示术语数量。或者迭代,当然:

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

重要的提示:

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

4
eukras
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

3
rashok

两个bourne-ish替代品,其中既不需要bash数组:

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

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

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

注意:在第一种情况下,没有子进程分叉以协助列表操作。

想法:也许值得广泛使用NL internal ,并且只在生成最终结果时转换为不同的RS external

案例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-)}

2
NevilleDNZ

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

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

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

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

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

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

测试

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

另有输入:

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

好的伙计们!

这是我的答案!

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

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

由于两个原因:

  1. 你做 不需要逃避 分隔符;
  2. 你不会有 空格问题 。该值将在数组中正确分隔!

[]的

1
Eduardo Lucio
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循环像往常一样处理该数组中的各个项目。请注意,为IN变量指定的列表必须是“硬”引用,即使用单个刻度。

必须保存和恢复IFS,因为Bash不会以与命令相同的方式处理赋值。另一种解决方法是将赋值包装在函数内,并使用修改后的IFS调用该函数。在这种情况下,不需要单独保存/恢复IFS。感谢“Bize”指出了这一点。

1
ajaaskel

在Android Shell中,大多数提议的方法都不起作用:

$ 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

其中//表示全局替换。

1
18446744073709551615

使用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
1
jeberle

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

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]

其他示例(开头和结尾的分隔符):

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

基本上它会删除;以外的所有字符,例如delims;;;。然后它for1循环到number-of-delimiters,由${#delims}计算。最后一步是使用cut安全地获取$ith部分。

0
Petr Újezdský

用于分隔由';'分隔的字符串的单行进入一个数组是:

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

这仅在子shell中设置IFS,因此您不必担心保存和恢复其值。

0
Michael Hale

这甚至可以处理空白:

IFS=';' read ADDR1 ADDR2 <<< $(echo ${IN})
0
Mat Bess