如何在Bash中的分隔符上分割string?
我有这个string存储在一个variables:
IN="bla@some.com;john@home.com"
现在我想分割string;
定界符,使我有:
ADDR1="bla@some.com" ADDR2="john@home.com"
我不一定需要ADDR1
和ADDR2
variables。 如果它们是一个更好的数组的元素。
经过下面答案的build议后,我得到了以下的结果:
#!/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)的解决scheme;
。 我不确定这个答案是怎么回事,你如何将IFS
重置为默认?
RE: IFS
解决scheme,我试过这个,它工作,我保留旧的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)
在循环打印时,只有第一个string,没有$IN
左右括号。
您可以设置内部字段分隔符 (IFS)variables,然后让它parsing成一个数组。 当这种情况发生在一个命令中,那么对IFS
的赋值只发生在单个命令的环境( read
)上。 然后它根据IFS
variables值将inputparsing成一个数组,然后我们可以迭代。
IFS=';' read -ra ADDR <<< "$IN" for i in "${ADDR[@]}"; do # process "$i" done
它将parsing由一行隔开的项目;
,把它推到一个数组中。 用于处理整个$IN
,每次用一行input隔开;
:
while IFS=';' read -ra ADDR; do for i in "${ADDR[@]}"; do # process "$i" done done <<< "$IN"
采取从Bash shell脚本拆分数组 :
IN="bla@some.com;john@home.com" arrIN=(${IN//;/ })
说明:
这种构造取代了所有的';'
(最初的//
意思是全局replace),然后将空格分隔的string解释为一个数组(这是括号内的括号)。
在花括号里面使用的语法来replace每个';'
带有' '
字符的字符称为参数扩展 。
有一些常见的问题:
- 如果原始string有空格,则需要使用IFS :
-
IFS=':'; arrIN=($IN); unset IFS;
-
- 如果原始string有空格,分隔符是新行,则可以使用以下命令设置IFS :
-
IFS=$'\n'; arrIN=($IN); unset IFS;
-
如果你不介意处理,我喜欢这样做:
for i in $(echo $IN | tr ";" "\n") do # process done
你可以使用这种循环来初始化一个数组,但可能有一个更简单的方法来做到这一点。 希望这有助于,虽然。
兼容的答案
对于这个问题,在bash中已经有很多不同的方法来做到这一点。 但bash有许多特殊的function,所谓的bashism运行良好,但是在其他shell中不起作用。 特别是, 数组 , 关联数组和模式replace都是纯粹的双方,并且可能无法在其他shell下工作。
在我的Debian GNU / Linux上 ,有一个叫做dash的标准 shell,但是我知道很多喜欢使用ksh的人 。
最后,在非常小的情况下,有一个叫做busybox的特殊工具,带有自己的shell解释器( ash )。
请求的string
SO问题中的string示例是:
IN="bla@some.com;john@home.com"
由于这可能对空格有用,并且由于空格可以修改例程的结果,所以我更喜欢使用这个示例string:
IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
根据bash中的分隔符分割string(version> = 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 <<<"$var"
在最近的bash下使用这个语法不会改变当前会话的$IFS
,但只能用于当前的命令:
set | grep ^IFS= IFS=$' \t\n'
现在stringvar
被分割并存储到一个数组(名为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>'
这是做这件事最快捷的方法,因为没有叉子 ,也没有外部的资源。
从那里,你可以使用你已经知道的语法来处理每个字段;
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>]
根据shell中的分隔符分割string
但是,如果你要在许多shell下写一些可用的东西,你不得不使用bashisms 。
在许多shell中有一个语法用于分割stringaccros 首次或最后一次发生的子string:
${var#*SubStr} # will drop begin of string upto first occur of `SubStr` ${var##*SubStr} # will drop begin of string upto 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
(这是我的答案出版物的主要原因;)
这个小示例脚本在bash , dash , ksh , busybox下工作得很好,并在Mac-OS的bash下也进行了testing:
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>]
玩的开心!
这种方法如何:
IN="bla@some.com;john@home.com" set -- "$IN" IFS=";"; declare -a Array=($*) echo "${Array[@]}" echo "${Array[0]}" echo "${Array[1]}"
资源
这也适用:
IN="bla@some.com;john@home.com" echo ADD1=`echo $IN | cut -d \; -f 1` echo ADD2=`echo $IN | cut -d \; -f 2`
小心,这个解决scheme并不总是正确的。 如果您仅通过“bla@some.com”,则将其分配给ADD1和ADD2。
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g' bla@some.com john@home.com
我已经看到了几个参考cut
命令的答案,但是他们都被删除了。 有一点奇怪,没有人详细说明这一点,因为我认为这是做这种事情的更有用的命令之一,特别是parsing分隔的日志文件。
在将这个特定的例子分割成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
这个文件,并select一个特定的领域进一步处理。
这对我工作:
string="1;2" echo $string | cut -d';' -f1 # output is 1 echo $string | cut -d';' -f2 # output is 2
我认为AWK是解决您的问题的最好,最有效率的命令。 在几乎所有Linux发行版中,AWK都默认包含在Bash中。
echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}'
会给
bla@some.com john@home.com
当然,您可以通过重新定义awk打印字段来存储每个电子邮件地址。
对Darron的回答有一个不同的看法,这就是我的做法:
IN="bla@some.com;john@home.com" read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)
在Bash中,一个防弹的方法,即使你的variables包含换行符也可以工作:
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
(分隔符)的-d
选项,以便read
被强制读取所有提供的内容。 而且我们提供的read
完全是variables的内容,没有拖尾换行感谢printf
。 请注意,我们也将分隔符放在printf
以确保传递给read
的string具有尾随分隔符。 没有它, 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以来,内buildmapfile
(又名readarray
)支持-d
选项来指定分隔符。 因此,另一个规范的方法是:
mapfile -d ';' -t array < <(printf '%s;' "$in")
这里是一个干净的3class轮:
in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof" IFS=';' list=($in) for item in "${list[@]}"; do echo $item; done
IFS
根据分隔符分隔单词, ()
用于创build一个数组 。 然后[@]
用来作为单独的单词返回每个项目。
如果您之后有任何代码,您还需要恢复$IFS
,例如未unset IFS
。
如果你不使用数组,那这个class轮呢?
IFS=';' read ADDR1 ADDR2 <<<$IN
没有设置IFS
如果你只有一个冒号,你可以这样做:
a="foo:bar" b=${a%:*} c=${a##*:}
你会得到:
b = foo c = bar
有这样一个简单而巧妙的方法:
echo "add:sfff" | xargs -d: -i echo {}
但是你必须使用gnu xargs,BSD xargs不能支持-d delim。 如果你像我一样使用苹果mac。 你可以安装gnu xargs:
brew install findutils
然后
echo "add:sfff" | gxargs -d: -i echo {}
这是最简单的方法。
spo='one;two;three' OIFS=$IFS IFS=';' spo_array=($spo) IFS=$OIFS echo ${spo_array[*]}
下面的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
与其他解决scheme相比,这个解决scheme具有以下优点:
-
IFS
未被覆盖:由于即使是局部variables的dynamic作用域,覆盖循环中的IFS
也会导致新值泄漏到循环中执行的函数调用中。 -
不使用数组:使用
read
将string读入数组需要在Bash中使用-a
标志,在zsh使用-A
。
如果需要的话,可以按如下方式将函数放入脚本中:
#!/usr/bin/env bash split() { # ... } split "$@"
如果没有空间,为什么不呢?
IN="bla@some.com;john@home.com" arr=(`echo $IN | tr ';' ' '`) echo ${arr[0]} echo ${arr[1]}
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
这里有一些很酷的答案(尤其是errator),但是对于类似于其他语言的分裂的东西 – 这就是我原来的问题的意思 – 我在这个问题上解决了:
IN="bla@some.com;john@home.com" declare -aa="(${IN/;/ })";
现在${a[0]}
, ${a[1]}
等等,就像你期望的那样。 使用${#a[*]}
作为条款数量。 或者当然重复:
for i in ${a[*]}; do echo $i; done
重要的提示:
这在没有空间担心的情况下工作,这解决了我的问题,但可能无法解决你的问题。 在这种情况下,使用$IFS
解决scheme。
使用内置的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
两个都不需要bash数组的bourne-ish选项:
案例1 :保持简洁:使用NewLine作为logging分隔符。
IN="bla@some.com john@home.com" while read i; do # process "$i" ... eg. echo "[email:$i]" done <<< "$IN"
注意:在第一种情况下,没有任何subprocess分叉来协助列表操作。
想法:也许值得在内部广泛使用NL,并且在外部产生最终结果时仅转换为不同的RS。
案例2 :使用“;” 作为logging分隔符…例如。
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"
在这两种情况下,可以在循环内组成一个子列表,在循环完成后持久化。 这在处理内存中的列表时非常有用,而不是将列表存储在文件中。 {ps保持冷静,继续B-)}
除了已经提供的奇妙答案之外,如果仅仅是打印数据,你可能会考虑使用awk
:
awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
这将字段分隔符设置为;
,所以它可以通过for
循环遍历字段并for
相应的打印。
testing
$ 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]
另外input:
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;cd;e_;f" > [a] > [b] > [cd] > [e_] > [f]
在Android shell中,大部分build议的方法都不起作用:
$ 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
//
表示全局replace。
单行划分由';'分隔的string 进入一个数组是:
IN="bla@some.com;john@home.com" ADDRS=( $(IFS=";" echo "$IN") ) echo ${ADDRS[0]} echo ${ADDRS[1]}
这只能在一个子shell中设置IFS,所以你不必担心保存和恢复它的价值。
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循环像往常一样处理该数组中的单个项目。 注意给INvariables的列表必须是“硬”的,也就是说,只有一个刻度。
IFS必须保存和恢复,因为Bash不会像命令一样处理赋值。 另一种解决方法是将分配包装在一个函数中,并用一个修改后的IFS调用该函数。 在这种情况下,不需要单独保存/恢复IFS。 感谢“Bize”的指出。
也许不是最优雅的解决scheme,但与*
和空格:
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]
Other example (delimiters at beginning and end):
IN=";bla@so me.com;*;john@home.com;" > [] > [bla@so me.com] > [*] > [john@home.com] > []
Basically it removes every character other than ;
making delims
eg. ;;;
。 Then it does for
loop from 1
to number-of-delimiters
as counted by ${#delims}
. The final step is to safely get the $i
th part using cut
.
Okay guys!
Here's my answer!
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
Why this approach is "the best" for me?
Because of two reasons:
- You do not need to escape the delimiter;
- You will not have problem with blank spaces . The value will be properly separated in the array!
[]'s
Python version:
python -c 'from __future__ import print_function ; f = open("your-file"); [print(a) for a in f.read().split()]'