如何在Bash shell中将一个字符串分割成多个字符串,每个字符串之间至少用一个空格分隔?

344

我有一个包含多个单词的字符串,每两个单词之间至少有一个空格。如何将该字符串拆分为单独的单词,以便可以循环遍历它们?

这个字符串作为参数传递。例如:${2} == "cat cat file"。如何循环遍历它?

此外,如何检查字符串是否包含空格?


1
什么样的 shell?Bash、cmd.exe、powershell...? - Alexey Sviridov
你只需要循环(例如为每个单词执行一个命令)吗?还是需要存储单词列表以供以后使用? - DVK
11个回答

448

我喜欢把它转换为数组,以便能够访问单个元素:

sentence="this is a story"
stringarray=($sentence)

现在您可以直接访问单个元素(从0开始):

echo ${stringarray[0]}

或者将其转换为字符串以便循环:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

当然,直接遍历该字符串的解答之前已经回答过了,但该答案有一个缺点是不能跟踪后续使用的单个元素:

for i in $sentence
do
  :
  # do whatever on $i
done

另请参阅Bash数组参考


37
很遗憾,由于shell-globbing的原因,它并不完美:touch NOPE; var='* a *'; arr=($var); set | grep ^arr=输出了arr=([0]="NOPE" [1]="a" [2]="NOPE")而不是预期的arr=([0]="*" [1]="a" [2]="*") - Tino
3
@Tino:如果你不想让通配符干扰,那就把它关闭。这样解决方案也可以很好地与通配符一起使用。在我看来,这是最好的方法。 - Alexandros
6
我的方法是只使用默认安全并在任何情况下完美工作的模式。要求改变shell-globbing以获得安全解决方案,这不仅是一条非常危险的道路,而且已经是黑暗面了。因此,我的建议是永远不要习惯于在此处使用这样的模式,因为 sooner or later 你会忘记一些细节,然后有人会利用你的漏洞。你可以在新闻中找到这种攻击的证据。每天都有。 - Tino
此外,它似乎不尊重转义空格。"abc\ def" 不是一个单独的元素。 - ktb
@ktb 显然,用于引用的字符如果是扩展结果,就没有任何特殊含义。也就是说,只有原始行中存在的字符才会生效。例如,a='" "'; echo $a会打印出" " - undefined

349

你是否尝试过直接将字符串变量传递给 for 循环?Bash 会自动按空格进行分割。

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.

1
@MobRule - 唯一的缺点是你不能轻松地捕获(至少我不记得有什么方法)输出以进行进一步处理。请参见下面我的“tr”解决方案,它可以将内容发送到标准输出。 - DVK
4
你可以将它附加到一个变量上:A=${A}${word} - Lucas Jones
1
设置$text [这将把单词放入$1,$2,$3等] - Rajeshkumar
48
实际上,这个技巧不仅是错误的解决方法,而且由于shell通配符扩展而极为危险touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; done输出的结果是[NOPE] [a] [NOPE],而不是预期的[*] [a] [*](为了方便阅读,LF被替换为SPC)。 - Tino
@mob如果我想根据特定的字符串拆分字符串,我该怎么办?例如**".xlsx"**分隔符。 - user9518134
很高兴知道Bash可以使用变量来实现这个功能。一开始我有些困惑,因为我认为这种方法也可以奏效。但是我意识到我试图将硬编码的字符串作为“for”命令的参数使用。显然,Bash只在操作一个变量时才会执行这种分割行为。 - sherrellbc

156

在BASH 3及以上版本中,最简单且最安全的方法可能是:

var="string    to  split"
read -ra arr <<<"$var"

(其中arr是包含字符串分割部分的数组),或者,如果输入中可能有换行符,并且您想获得不止第一行:

var="string    to  split"
read -ra arr -d '' <<<"$var"
请注意-d ''中的空格;不可省略,但这可能会导致您从<<<"$var"获得意外换行符(因为它会在结尾隐式添加LF)。
示例:
touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

输出预期结果

[*]
[a]
[*]

与所有先前的解决方案不同,此解决方案不容易受到意外和常常无法控制的shell扩展的影响。

而且这也为你提供了完整的IFS功能,正如你所期望的:

示例:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

输出类似于:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

正如您所见,空格也可以用这种方式保留:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

输出

[ split  ]
[   this    ]
请注意,BASH中的IFS处理是一个独立的主题,因此请进行测试;一些有趣的主题包括:
  • unset IFS:忽略SPC、TAB、NL和行首尾
  • IFS='':没有字段分隔符,只是读取所有内容
  • IFS=' ':SPC(仅限SPC)运行

最后几个例子:

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

输出

1 [this is]
2 [a test]

当......时

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

输出

1 [this]
2 [is]
3 [a]
4 [test]

顺便说一句:

  • 如果你还不习惯$'ANSI-ESCAPED-STRING',那就要适应它吧;这样可以省很多时间。

  • 如果你不加-r(比如在read -a arr <<<"$var"中),那么读取时会出现反斜杠转义。这个问题留给读者自己思考。


至于第二个问题:

我通常使用case来测试字符串中是否包含某些内容,因为它可以同时检查多种情况(注意:case只会执行第一个匹配的情况,如果需要继续执行,请使用多个case语句),而且这种需求经常会遇到(题外话:pun intended):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

因此,您可以将返回值设置为以下内容以检查SPC:

case "$var" in (*' '*) true;; (*) false;; esac

为什么选择case?因为它通常比正则表达式更易读,并且由于Shell元字符的存在,它可以很好地处理99%的需求。


9
这个回答值得更多的赞,因为它强调了globbing问题并且内容详尽。 - Brian Agnew
@brian 谢谢。请注意,您可以使用 set -fset -o noglob 来关闭 globbing,以便在此上下文中 shell 元字符不再造成影响。但我并不是真正的支持者,因为这会削弱 shell 的很多功能 / 在切换设置时非常容易出错。 - Tino
3
确实是一篇精彩的答案,值得更多的赞。关于 case 的 fall through,可以使用 ;& 来实现。不太确定这个功能在哪个版本的 bash 中出现。我是一个4.3的用户。 - Sergiy Kolodyazhnyy
4
谢谢你的指出,我之前不知道这个!所以我查了一下,这是在Bash4中出现的。;&类似于C语言中强制执行无需模式检查的 fallthrough。还有;;&,它只会继续进行其他模式检查。因此,;;就像是if ..; then ..; else if ..,而;;&就像是if ..; then ..; fi; if ..,其中;&就像是m=false; if ..; then ..; m=:; fi; if $m || ..; then .. - 一个人永远都在学习(从别人那里);) - Tino
2
对于不太熟悉使用bash数组变量的人来说,如果你echo数组变量并期望看到数组的内容,你只会看到第一个元素,因此这可能看起来不正常。请使用echo "${ARRAY[*]}"来查看内容。 - Kvass
显示剩余4条评论

101

只需使用shell内置的“set”命令。例如:

set $text

之后,$text中的单词将分别为$1、$2、$3等。为了提高鲁棒性,通常需要进行如下操作:

set -- junk $text
shift

处理$text为空或以破折号开头的情况。例如:

text="This is          a              test"
set -- junk $text
shift
for word; do
  echo "[$word]"
done

这将会被打印出来

[This]
[is]
[a]
[test]

5
这是一种很好的方法来拆分变量,以便可以直接访问各个部分。+1;解决了我的问题。 - Cheekysoft
我本来想建议使用 awk,但是 set 更容易。现在我成了 set 的粉丝。谢谢 @Idelic! - Yzmir Ramirez
27
如果您执行此类操作,请注意Shell通配符:touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; done输出的是[NOPE] [a] [NOPE]而不是预期的[*] [a] [*]仅在101%确定分割后的字符串中没有SHELL元字符时才使用它!请注意Shell通配符。 - Tino
4
@Tino: 这个问题不仅适用于这里,而是普遍存在。但在这种情况下,你可以在执行 set -- $var 前使用 set -f 来禁用通配符展开,在执行完后再使用 set +f 来恢复通配符展开。 - Idelic
3
@Idelic: 好发现。使用 set -f ,您的解决方案也很安全。但是 set +f 是每个 shell 的默认设置,因此这是一个必要的细节,必须注意到,因为其他人可能没有意识到它(就像我一样)。 - Tino

50
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

要检查空格,请使用grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1

1
在BASH中,echo "X" |通常可以被<<<"X"替换,例如:grep -s " " <<<"This contains SPC"。如果你做类似于echo X | read varread var <<< X的操作,你会发现它们之间的区别。只有后者将变量var导入到当前shell中,而要在第一种情况下访问它,你必须像这样分组:echo X | { read var; handle "$var"; } - Tino

24
echo $WORDS | xargs -n1 echo

这会输出每个单词,您随后可以根据需要处理该列表。


1
优雅的解决方案,我在CI中使用它来拆分带有空格的环境变量。例如:npm install $(echo $NPM_PACKAGES | xargs -n1 echo) --save-dev - Steve Moretz

21

(A) 将句子按单词(以空格分隔)拆分,您可以直接使用默认的IFS,如下所示:

array=( $string )


示例:运行以下代码段

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

将会输出

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

正如你所看到的,你可以使用单引号或双引号,而不会有任何问题。

注:
-- 这基本上与mob的答案相同,但通过这种方式,您可以为任何进一步需要存储数组。 如果您只需要单个循环,则可以使用他的答案,它比这个短一行 :)
-- 请参考此问题以获取基于定界符拆分字符串的其他方法。


(B) 您还可以使用正则表达式匹配来检查字符串中的某个字符。
例如,要检查空格字符是否存在,您可以使用以下内容:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi

对于正则表达式提示(B),加1分,但对于错误的解决方案(A)扣1分,因为这容易出现shell通配符问题。 ;) - Tino

10

$ echo foo bar baz | sed 's/ /\n/g'

foo
bar
baz

6

使用bash检查空格:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"

2
我的使用情况下,最好的选择是:

grep -oP '\w+' file

基本上这是一个正则表达式,用于匹配连续的非空格字符。这意味着任何类型和任何数量的空格都不会匹配。 -o参数将每个单词匹配输出到不同的行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接