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

stefanB 1885 bash shell scripting split

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

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它工作.

Johannes Sch.. 1153

您可以设置内部字段分隔符(IFS)变量,然后将其解析为数组.当在命令中发生这种情况时,分配IFS仅发生在该单个命令的环境(to 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"

  • @LucaBorrione将`IFS`设置在与`read`相同的行上,没有分号或其他分隔符,而不是在单独的命令中,将其范围限定为该命令 - 因此它总是"恢复"; 你不需要手动做任何事情. (44认同)
  • 这可能是最好的方法.IFS会持续多久它的当前值,它可以通过设置它不应该被设置来搞乱我的代码,以及如何在我完成它时重置它? (18认同)
  • 你可以在不使用while循环的情况下一次阅读所有内容:read -r -d''-a addr <<<"$ in"#-d''在这里是关键,它告诉read不要停在第一个换行符(这是默认的-d)但要继续直到EOF或NULL字节(仅出现在二进制数据中). (14认同)
  • 现在应用修复后,仅在读取命令的持续时间内:) (7认同)
  • @imagineerThis有一个错误涉及到IFS的herestrings和本地更改,需要引用`$ IN`.该错误在`bash` 4.3中得到修复. (5认同)
  • 不处理包含的换行符.还要添加尾随换行符. (3认同)

palindrom.. 901

取自Bash shell脚本拆分数组:

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

说明:

这种结构替换所有出现';'(初始//意味着全球替换)字符串中IN' '(一个空格),然后解释空格分隔字符串数组(这是周围的括号做).

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

有一些常见的问题:

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

  • 我只想补充一点:这是最简单的,您可以使用$ {arrIN [1]}访问数组元素(当然从0开始) (78认同)
  • 出于其他原因,这是一种糟糕的方法:例如,如果您的字符串包含`;*;`,那么`*`将扩展为当前目录中的文件名列表.-1 (47认同)
  • 找到它:修改$ {}内变量的技术称为"参数扩展". (24认同)
  • 当原始字符串包含空格时它是否有效? (24认同)
  • 不,我不认为当存在空间时它会起作用......它将','转换为''然后构建一个空格分隔的数组. (21认同)
  • 非常简洁,但有一般用途的*警告*:shell将*word splitting*和*expansions*应用于字符串,这可能是不受欢迎的; 试试吧.`IN ="bla@some.com; john@home.com;*;分开"`.简而言之:如果您的令牌包含嵌入的空格和/或字符,则此方法将会中断.例如`*`恰好使当前文件夹中的令牌匹配文件名. (11认同)
  • 如果你想分割特殊字符,如波浪号(〜),请确保将其转义:arrIN =($ {IN // \〜/}) (8认同)
  • @John_West,是的,通过修改全局状态来禁用globbing(并以'IFS`的形式对进一步的全局状态进行严密控制)可以使这种方法变得可用,但是......好吧,为什么你会这样做? -a`没有任何风险? (5认同)
  • @KyleStrand设置`IFS`,然后设置`arrIN`,就像它们在不同的行上执行或用`;`分隔一样.也就是说,仅当分配出现在*non*-assignment命令之前时,分配才是临时的.因此,在`IFS ='之后:'arrIN =($ IN)`,`echo'$ IFS"`给出`:`,并且```上的字被拆分为后续命令,这通常是不需要的.(这很容易被忽略,因为`echo $ var`足以检查`$ var`是否为`:`,当`:`不在`$ IFS`中时.)因此,除了可能在最后一个脚本,`IFS =':'arrIN =($ IN)IFS = $'\ t \n'`或`IFS =':'arrIN =($ IN); 未设置IFS`是可取的. (3认同)
  • 你可以通过使用`IFS`而不是参数扩展/替换来解决空间问题:`IFS =':'arrIN =($ IN)`在我看来,这也更具可读性. (2认同)
  • 不确定为什么`IFS =';' 声明-a arr =($ IN)`在这里没有得到更多的信任.不需要设置任何中间变量,IFS更改仅适用于`declare`命令,我们扩展IFS而不必将其更改为其他内容. (2认同)

Chris Lutz.. 230

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

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

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

  • -1,你显然不知道wordplitting,因为它在你的代码中引入了两个bug.一个是当你不引用$ IN而另一个是你假装换行是wordplitting中使用的唯一分隔符.你正在迭代IN中的每个WORD,而不是每一行,并且确定不是每个由分号分隔的元素,尽管它看起来可能具有看起来像它有效的副作用. (33认同)
  • 总结评论中的争论:*一般用途的注意事项*:shell将*word splitting*和*expansions*应用于字符串,这可能是不受欢迎的; 试试吧.`IN ="bla@some.com; john@home.com;*;分开"`.简而言之:如果您的令牌包含嵌入的空格和/或字符,则此方法将会中断.例如`*`恰好使当前文件夹中的令牌匹配文件名. (8认同)
  • 您可以将其更改为回显"$ IN"| tr';' '\n'| 读取-r ADDY; 做#process"$ ADDY"; 为了让他幸运,我想:)请注意,这将分叉,你不能从循环内改变外部变量(这就是为什么我使用<<<"$ IN"语法)然后 (3认同)

F. Hauri.. 177

兼容的答案

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

特别是,数组,关联数组模式替换是纯粹的基础,并且可能在其他shell下不起作用.

在我的Debian GNU/Linux上,有一个名为标准 shell ,但我知道很多人喜欢使用.

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

请求的字符串

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

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

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

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

基于分隔符拆分字符串(版本> = 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 <<<"$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是最快速的分割方式,因为没有分叉,也没有外部资源.

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

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

更新:最近 > = 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中使用了一种语法,用于在子字符串的第一次最后一次出现时拆分字符串:

${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 字符串的右边(结束).

这个小样本脚本在,,,下运行良好,并且在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>]

玩得开心!

  • `#`,`##`,`%`和`%%`替换使IMO更容易解释(删除多少):`#`和`%`删除最短的匹配字符串和`##`和`%%`删除最长的. (13认同)

DougW.. 151

我已经看到了几个引用该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此文件并选择特定字段进行进一步处理非常方便.

  • 感谢使用`cut`,它是适合这项工作的合适工具!比任何贝壳黑客都清除得多. (5认同)
  • 只有事先知道元素的数量,这种方法才有效; 你需要围绕它编写一些更多的逻辑.它还为每个元素运行外部工具. (4认同)

Steven Lizar.. 105

这对我有用:

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

  • cut仅将单个char作为分隔符。 (2认同)

小智.. 85

这种方法怎么样:

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

资源

  • +1 ......但"set"和声明-a是不必要的.你也可以只用'IFS';" && Array =($ IN)` (14认同)
  • +1 ......但我不会将变量命名为"数组"......我想是宠物.好的解决方案 (6认同)
  • -1:首先,@ is是正确的,因为这里的大多数命令都不起作用.其次,它使用分词来形成数组,并且在执行此操作时不执行任何操作来禁止glob-expansion(因此,如果在任何数组元素中都有glob字符,则这些元素将替换为匹配的文件名). (6认同)

小智.. 64

这也有效:

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.


lothar.. 63

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

  • -1**如果字符串包含空格怎么办?**例如`IN ="这是第一行;这是第二行"arrIN =($(echo"$ IN"| sed -e's /;/\n/g'))`在这种情况下将生成一个包含8个元素的数组(每个单词空间分隔一个元素),而不是2(每个行的一个元素,半冒号分隔) (3认同)
  • @Luca没有sed脚本正好创建两行.什么为你创建多个条目是当你把它放入一个bash数组(默认情况下在白色空间分割) (3认同)

Tony.. 57

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

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

会给

bla@some.com john@home.com

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

  • 甚至更简单:回声"bla@some.com; john@home.com"| awk'BEGIN {RS =";"} {print}' (3认同)

nickjb.. 32

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

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

  • 诊断:`IFS =";"`赋值仅存在于`$(...; echo $ IN)`子shell中; 这就是为什么一些读者(包括我)最初认为它不起作用的原因.我假设所有$ IN都被ADDR1哄骗了.但是nickjb是正确的; 它确实有效.原因是`echo $ IN`命令使用$ IFS的当前值解析其参数,但随后使用空格分隔符将它们回显到stdout,而不管$ IFS的设置如何.所以净效果好像有人叫`读ADDR1 ADDR2 <<<"bla@some.com john@home.com"`(注意输入是空格分隔的;不是分开的). (5认同)

gniourf_gnio.. 29

在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")'

这项工作的诀窍是使用带有空分隔符-dread(分隔符)选项,以便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")

  • 我发现它是该列表中的罕见解决方案,它可以同时与`\n`,空格和`*`一起正常工作.而且,没有循环; 执行后,shell中可以访问数组变量(与最高的upvoted答案相反).注意,`in = $'...'`,它不适用于双引号.我认为,它需要更多的赞成. (5认同)

Darron.. 23

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

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


kenorb.. 19

这是一个干净的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.

  • 使用`$ in` unquoted可以扩展通配符. (5认同)

Emilien Brig.. 17

没有设置IFS

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

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

你会得到:

b = foo
c = bar


Halle Knast.. 10

以下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循环会导致新值泄漏到循环内执行的函数调用中.

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

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

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"


shuaihanhung.. 8

您可以在许多情况下使用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"


Victor Choy.. 7

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

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

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

brew install findutils

然后

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


MageProspero.. 5

这是最简单的方法.

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


归档时间:

查看次数:

2036745 次

最近记录:

6 月,1 周 前