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

时间:2009-05-28 02:03:44

标签: bash shell split scripting

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

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
当我尝试

时,BTW
mails2=($IN)

我在循环中打印时只获得了第一个字符串,$IN周围没有括号。

34 个答案:

答案 0 :(得分:1075)

您可以设置internal field separator(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"

答案 1 :(得分:850)

取自 Bash shell script split array

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

说明:

此构造用';'(单个空格)替换字符串//中所有出现的IN(初始' '表示全局替换),然后解释空格 - 作为数组的定界字符串(这是周围的括号所做的)。

花括号内部使用';'字符替换每个' '字符的语法称为Parameter Expansion

有一些常见问题:

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

答案 2 :(得分:221)

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

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

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

答案 3 :(得分:160)

兼容的答案

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

特别是,数组关联数组模式替换是纯 bashisms ,可能无法正常工作其他 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>"

基于中的分隔符拆分字符串(版本&gt; = 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>]

更新:最近&gt; = 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下编写可用的东西,你必须使用 bashisms

在许多shell中使用了一种语法,用于在子串的第一个 last 出现时拆分字符串:

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

玩得开心!

答案 4 :(得分:126)

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

答案 5 :(得分:93)

这对我有用:

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

答案 6 :(得分:83)

这种方法怎么样:

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

Source

答案 7 :(得分:62)

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

答案 8 :(得分:62)

这也有效:

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。

答案 9 :(得分:38)

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

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

将给出

bla@some.com john@home.com

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

答案 10 :(得分:30)

Darron's answer的不同看法,我就是这样做的:

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

答案 11 :(得分:26)

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

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

答案 12 :(得分:21)

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

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

答案 13 :(得分:19)

这是一个干净的3线:

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

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

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

答案 14 :(得分:16)

不设置IFS

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

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

你会得到:

b = foo
c = bar

答案 15 :(得分:8)

以下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 "$@"

答案 16 :(得分:7)

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

app = Flask(__name__)
app.config.from_envvar('FLASKAPP_SETTINGS', silent=True)
db = SQLAlchemy(app)

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

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

然后

brew install findutils

答案 17 :(得分:6)

你可以将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"

答案 18 :(得分:5)

这是最简单的方法。

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

答案 19 :(得分:4)

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

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解决方案。

答案 20 :(得分:3)

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

答案 21 :(得分:2)

除了已经提供的精彩答案之外,如果只是打印出数据,您可以考虑使用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]

答案 22 :(得分:2)

两个不需要bash数组的bourne-ish替代方案:

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

答案 23 :(得分:2)

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

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

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

答案 24 :(得分:1)

在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

其中//表示全局替换。

答案 25 :(得分:1)

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变量提供的列表必须是&#34; hard&#34;引用,即单蜱。

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

答案 26 :(得分:1)

好的伙计们!

这是我的答案!

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. 您不会遇到空格问题。该值将在数组中正确分隔!
  3. []的

答案 27 :(得分:1)

使用内置的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

答案 28 :(得分:0)

这甚至可以处理空格:

struct MySum : Sum<std::vector<int>>
{
  MySum(int t0, int t1, int t2, int t3) : Sum<std::vector<int>>{{t0, t1, t2, t3}} { }
};

struct MySum4 : Sum<std::array<int, 4>>
{
  MySum4(int t0, int t1, int t2, int t3) : Sum<std::array<int, 4>>{{t0, t1, t2, t3}} { }
};

struct MySumSet : Sum<std::set<int>>
{
    MySumSet(int t0, int t1, int t2, int t3) : Sum<std::set<int>>{{t0, t1, t2, t3}} { }
};

struct MySumList : Sum<std::list<int>>
{
    MySumList(int t0, int t1, int t2, int t3) : Sum<std::list<int>>{{t0, t1, t2, t3}} { }
};

int main()
{  
    MySum s(1, 2, 3, 4);
    s.print();

    MySum4 s4(1, 2, 3, 4);
    s4.print();
    static_assert(s4.m_terms.size() == 4);

    MySumSet ss(1, 2, 3, 4);
    ss.print();

    MySumList sl(1, 2, 3, 4);
    sl.print();
}

答案 29 :(得分:0)

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

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}计算。最后一步是使用$i安全地获取cut部分。

答案 30 :(得分:0)

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

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

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

答案 31 :(得分:-2)

对我有用:

  

echo $ PATH | ruby -ne'放入$ _。split(“:”)'

答案 32 :(得分:-5)

又一个迟到的答案......如果你是一个有头脑的人,这里是 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

答案 33 :(得分:-6)

有两种简单的方法:

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

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