如何在Bash中检查字符串是否包含子字符串

3490

我在Bash中有一个字符串:

string="My string"

如何测试字符串是否包含另一个字符串?

if [ $string ?? 'foo' ]; then
  echo "It's there!"
fi

其中??是我未知的运算符,我该使用echogrep吗?

if echo "$string" | grep 'foo'; then
  echo "It's there!"
fi

看起来有点笨拙。


4
嗨,如果空字符串是假的,为什么你认为它笨拙?尽管有提出的解决方案,但这是我唯一有效的方法。 - ericson.cepeda
1
你可以在这里使用 expr 命令。 - cifer
9
这是一个适用于posix shell的例子:https://dev59.com/yXE85IYBdhLWcg3wVR6g - sehe
2
请在您的示例中使用“$haystack中的*$needle*”习语。这样更容易阅读和理解。 - Piotr Henryk Dabrowski
30个回答

4804

如果您使用双方括号,您也可以在case语句之外使用Marcus的回答(*通配符):

string='My long string'
if [[ $string == *"My long"* ]]; then
  echo "It's there!"
fi

请注意,针对搜索字符串中的空格需要放在双引号之间,而 * 通配符应该位于外部。还要注意使用简单比较运算符(即 ==),而不是正则表达式运算符 =~


192
请注意,您可以通过在测试中切换到"!="来反转比较。感谢您的答案! - Quinn Taylor
5
使用这段代码时,我收到了“[[: not found”的错误提示。你认为是怎么回事?我的操作系统是Ubuntu,使用的是GNU Bash 4.1.5(1)版本。 - Jonik
81
您可能缺少shebang或将其设置为#!/bin/sh。请尝试改为使用#!/bin/bash - Dennis Williamson
@DennisWilliamson 不要使用 #!/bin/bash,为了可移植性,你应该使用 #!/usr/bin/env bash - Swivel
1
也许值得一提的是,它不能与单个 [ 运算符一起使用(双括号 [[ 与通配符可能是必需的)。 - Hakim
2
是的,这个解决方案不是POSIX标准。在许多Docker容器中无法与sh一起使用。 - t7e

976

如果您更喜欢正则表达式的方法:

string='My string';

if [[ $string =~ "My" ]]; then
   echo "It's there!"
fi

3
不得不在Bash脚本中替换一个egrep正则表达式,这个方案完美解决了问题! - blast_hardcheese
122
=~ 运算符已经在整个字符串中搜索匹配项;这里的 .* 是多余的。此外,引号通常比反斜杠更可取:[[ $string =~ "My s" ]] - bukzor
27
从Bash 3.2+开始,引号包含的引用已经不再适用于引用。最好将其分配给一个变量(使用引号),然后进行比较。像这样:re="My s"; if [[ $string =~ $re ]]。参考链接为:http://tiswww.case.edu/php/chet/bash/FAQ E14 - seanf
65
测试字符串是否不包含某个子字符串:if [[ ! "abc" =~ "d" ]] 的值为真。 - KrisWebDev

482

我不确定是否使用if语句,但您可以通过使用case语句获得类似的效果:

case "$string" in 
  *foo*)
    # Do stuff
    ;;
esac

106
这可能是最好的解决方案,因为它可以在posix shell中移植使用,也就是说没有bash相关的特性。 - technosaurus
40
@technosaurus,我觉得在一个只有Bash标签的问题中批评“Bashism”相当奇怪 :) - P.P
69
@P.P. 这并不是批评,而是更倾向于一种更通用的解决方案而非局限于某种特定情况。请注意,多年后像我这样的人会来寻找这个答案,他们可能会很高兴发现一个在更广泛范围内有用的答案。正如开源世界所说:“选择是好的!” - Carl Smotricz
2
@technosaurus,值得一提的是,在某些POSIX兼容的sh版本(例如Solaris 10上的/usr/xpg4/bin/sh和ksh(>= 88)中,[[ $string == *foo* ]]也可以工作。 - maxschlepzig
@maxschlepzig 现在许多人都使用Docker容器工作。大多数默认的Docker镜像没有Bash,因此类似于[[ $string == *foo* ]]这样的命令将无法运行。 - t7e

346

stringContain 变体(兼容或不区分大小写)

由于这些 Stack Overflow 答案大多涉及 Bash,因此我在本帖的末尾发布了一个 不区分大小写 的 Bash 函数...

无论如何,这是我的

兼容性答案

由于已经有很多使用 Bash 特定特性的答案了,所以这里介绍一种在功能较差的 shell(如 BusyBox)中工作的方法:

[ -z "${string##*$reqsubstr*}" ]

在实践中,这可能会产生:
string='echo "My string"'
for reqsubstr in 'o "M' 'alt' 'str';do
  if [ -z "${string##*$reqsubstr*}" ] ;then
      echo "String '$string' contain substring: '$reqsubstr'."
    else
      echo "String '$string' don't contain substring: '$reqsubstr'."
    fi
  done

这个测试已在Bash、DashKornShell (ksh)和ash (BusyBox)下进行过,结果总是如下:
String 'echo "My string"' contain substring: 'o "M'.
String 'echo "My string"' don't contain substring: 'alt'.
String 'echo "My string"' contain substring: 'str'.

合并为一个函数

正如@EeroAaltonen所要求的,这是同一个演示的版本,在相同的shell下进行了测试:

myfunc() {
    reqsubstr="$1"
    shift
    string="$@"
    if [ -z "${string##*$reqsubstr*}" ] ;then
        echo "String '$string' contain substring: '$reqsubstr'.";
      else
        echo "String '$string' don't contain substring: '$reqsubstr'."
    fi
}

然后:

$ myfunc 'o "M' 'echo "My String"'
String 'echo "My String"' contain substring 'o "M'.

$ myfunc 'alt' 'echo "My String"'
String 'echo "My String"' don't contain substring 'alt'.

注意:您必须转义或双引号包含引号和/或双引号:

$ myfunc 'o "M' echo "My String"
String 'echo My String' don't contain substring: 'o "M'.

$ myfunc 'o "M' echo \"My String\"
String 'echo "My String"' contain substring: 'o "M'.

简单函数

该函数在BusyBox、Dash和Bash下均经过测试:

stringContain() { [ -z "${2##*$1*}" ]; }

然后现在:

$ if stringContain 'o "M3' 'echo "My String"';then echo yes;else echo no;fi
no
$ if stringContain 'o "M' 'echo "My String"';then echo yes;else echo no;fi
yes

如果提交的字符串可能为空,正如@Sjlver指出的那样,该函数将变为:

stringContain() { [ -z "${2##*$1*}" ] && [ -z "$1" -o -n "$2" ]; }

或者按照Adrian Günter的评论所建议的,避免使用-o开关:

stringContain() { [ -z "${2##*$1*}" ] && { [ -z "$1" ] || [ -n "$2" ];};}

最终(简单)函数:

将测试反转,使它们可能更快:

stringContain() { [ -z "$1" ] || { [ -z "${2##*$1*}" ] && [ -n "$2" ];};}

使用空字符串:
$ if stringContain '' ''; then echo yes; else echo no; fi
yes
$ if stringContain 'o "M' ''; then echo yes; else echo no; fi
no

不区分大小写(仅适用于Bash!)

要测试字符串而不考虑大小写,只需将每个字符串转换为小写:

stringContain() {
    local _lc=${2,,}
    [ -z "$1" ] || { [ -z "${_lc##*${1,,}*}" ] && [ -n "$2" ] ;} ;}

检查:

stringContain 'o "M3' 'echo "my string"' && echo yes || echo no
no
stringContain 'o "My' 'echo "my string"' && echo yes || echo no
yes
if stringContain '' ''; then echo yes; else echo no; fi
yes
if stringContain 'o "M' ''; then echo yes; else echo no; fi
no

1
如果你能想出一种方法将其放入一个函数中,那就更好了。 - Eero Aaltonen
2
@EeroAaltonen 你觉得我(新添加的)函数怎么样? - F. Hauri - Give Up GitHub
2
我知道!find . -name "*" | xargs grep "myfunc" 2> /dev/null - eggmatters
8
这很棒,因为它非常兼容。但有一个问题:如果被搜索的字符串为空,它将无法工作。正确的版本应该是string_contains() { [ -z "${2##*$1*}" ] && [ -n "$2" -o -z "$1" ]; }最后思考一下:空字符串是否包含空字符串?上面的版本认为是(因为有-o -z "$1"这部分代码)。 - Sjlver
1
特殊情况:要搜索的字符串是每行中的第一个单词(在/proc/cpuinfo中的processor),请参见此答案以从命令行获取Linux中的CPU /核心数量? - F. Hauri - Give Up GitHub
显示剩余3条评论

186

请记住,shell脚本编程更像是一组命令,而不是一种语言。直觉上您可能认为这种“语言”需要您在if后面跟[[[。实际上,它们只是返回指示成功或失败的退出状态的命令(就像其他所有命令一样)。由于这个原因,我会使用grep而不是[命令。

只需执行:

if grep -q foo <<<"$string"; then
    echo "It's there"
fi

既然你正在将if视为测试其后的命令的退出状态(包括分号),那么为什么不重新考虑一下你要测试的字符串的来源呢?

## Instead of this
filetype="$(file -b "$1")"
if grep -q "tar archive" <<<"$filetype"; then
#...

## Simply do this
if file -b "$1" | grep -q "tar archive"; then
#...

-q 选项使得 grep 不输出任何内容,因为我们只需要返回代码。<<< 让 shell 扩展下一个单词并将其用作命令的输入,这是 << here 文档的一行版本(我不确定这是否是标准或 Bashism)。


9
它们被称为“here strings (3.6.7)”(在此处),我相信这是bash特有的语法。 - alex.pilon
13
如果想要查找某个字符串是否包含在另一段文本中,可以使用进程替换if grep -q foo <(echo somefoothing); then - larsr
1
@nyuszika7h echo本身是非常可移植的。但是标志不是。如果你发现自己在考虑-e-n,请使用printf - Bruno Bronosky
6
这样做的成本非常昂贵:执行 grep -q foo <<<"$mystring" 意味着要进行1次 fork,并且是* bashism *,而 echo $mystring | grep -q foo 意味着要进行2次 fork(一次用于管道,第二次用于运行 /path/to/grep)。 - F. Hauri - Give Up GitHub
1
如果参数字符串包含反斜杠序列,则没有标志的 echo 可能仍然存在意想不到的可移植性问题。例如,在某些平台上,echo "nope\c" 的预期行为类似于在其他一些平台上使用 echo -e "nope"printf '%s' "nope"printf '%s\n' 'nope\c' - tripleee
显示剩余3条评论

106

最佳答案已经被接受,但由于有多种方法可以实现它,这里提供另一种解决方案:

if [ "$string" != "${string/foo/}" ]; then
    echo "It's there!"
fi

${var/search/replace} 是将 $var 中第一个出现的 search 替换为 replace,如果找到的话(它不会更改 $var)。 如果你尝试用空字符串替换 foo,并且字符串已经改变,那么显然是找到了 foo


5
在使用BusyBox的shell ash时,ephemient上面提供的解决方案:
if [ "$string" != "${string/foo/}" ]; then echo "It's there!" fi 非常有用。由于一些bash正则表达式未实现,所以接受的解决方案在BusyBox中无法工作。请注意,此翻译不包括任何解释性内容。
- TPoschel
3
差异的不平等。挺奇怪的思想!我喜欢它。 - nitinr708
1
除非你的字符串是'foo',否则请返回已翻译的文本。 - venimus
2
@hanshenrik 你正在将 $XDG_CURRENT_DESKTOP$string 进行比较。你想要的表达式是 if [ "$XDG_CURRENT_DESKTOP" != "${XDG_CURRENT_DESKTOP/GNOME/}" ]; then echo MATCHES GNOME; fi - Todd Lewis
1
@venimus 是的,"x$string" != "x${string/foo/}" 更好。 - tomo_iris427
显示剩余4条评论

82

因此,对于这个问题有很多有用的解决方案 - 但哪个是最快的/使用最少的资源?

使用这个框架进行反复测试:

/usr/bin/time bash -c 'a=two;b=onetwothree; x=100000; while [ $x -gt 0 ]; do TEST ; x=$(($x-1)); done'

每次替换TEST:

[[ $b =~ $a ]]           2.92 user 0.06 system 0:02.99 elapsed 99% CPU

[ "${b/$a//}" = "$b" ]   3.16 user 0.07 system 0:03.25 elapsed 99% CPU

[[ $b == *$a* ]]         1.85 user 0.04 system 0:01.90 elapsed 99% CPU

case $b in *$a):;;esac   1.80 user 0.02 system 0:01.83 elapsed 99% CPU

doContain $a $b          4.27 user 0.11 system 0:04.41 elapsed 99%CPU

(doContain 在 F. Houri 的回答中)

只是为了好玩:

echo $b|grep -q $a       12.68 user 30.86 system 3:42.40 elapsed 19% CPU !ouch!

因此,无论是在扩展测试还是案例中,简单的替换选项都可以可靠地获胜。该案例是可移植的。

将输出管道传递到100000个grep肯定是令人痛苦的!有关不必要使用外部工具的旧规则仍然正确。


8
整洁的基准测试。让我相信使用 [[ $b == *$a* ]] - Mad Physicist
2
如果我理解正确的话,case 的总时间消耗最小。不过,在 $b in *$a 后面你漏掉了一个星号。当修复了这个 bug 之后,我得到的 [[ $b == *$a* ]] 的结果比 case 稍微快一些,但当然也可能取决于其他因素。 - tripleee
1
https://ideone.com/5roEVt 是我的实验,修复了一些额外的错误,并测试了不同的情况(其中字符串实际上不存在于较长的字符串中)。结果大体相似;[[ $b == *$a* ]] 很快,而 case 几乎一样快(并且令人愉悦地符合 POSIX 标准)。 - tripleee
条件表达式[[ $b == *$a* ]]和case语句case $b in *$a):;;esac在无匹配条件下是不等价的。交换$a$b会导致条件表达式[[的退出代码为1,而case语句的退出代码为0。根据help case:退出状态:返回最后一个执行的命令的状态。如果没有匹配到任何模式,则返回状态为零,这可能不是预期的行为。为了在无匹配条件下返回1,应该使用以下语句:case $b in *$a*):;; *) false ;; esac - r a

65

Bash 4+示例。注意:在单词包含空格等情况下不使用引号会导致问题。在Bash中始终使用引号,这是我的建议。

以下是一些Bash 4+的例子:

示例1,在字符串中检查'yes'(大小写不敏感):

    if [[ "${str,,}" == *"yes"* ]] ;then

示例2,检查字符串中是否包含“yes”(不区分大小写):

    if [[ "$(echo "$str" | tr '[:upper:]' '[:lower:]')" == *"yes"* ]] ;then

示例3,检查字符串中是否包含'yes'(区分大小写):

     if [[ "${str}" == *"yes"* ]] ;then

示例4:检查字符串中是否有“yes”(区分大小写):

     if [[ "${str}" =~ "yes" ]] ;then

示例5,精确匹配(区分大小写):

     if [[ "${str}" == "yes" ]] ;then

示例6,精确匹配(不区分大小写):

     if [[ "${str,,}" == "yes" ]] ;then

示例7,精确匹配:

     if [ "$a" = "$b" ] ;then

示例 8,通配符匹配 .ext(不区分大小写):

     if echo "$a" | egrep -iq "\.(mp[3-4]|txt|css|jpg|png)" ; then

示例9:在字符串上使用grep进行区分大小写的匹配:

     if echo "SomeString" | grep -q "String"; then

例子10:使用grep查找字符串时不区分大小写:

     if echo "SomeString" | grep -iq "string"; then

示例11,对带通配符的字符串使用不区分大小写的grep:

     if echo "SomeString" | grep -iq "Some.*ing"; then

示例12,使用doublehash进行比较(如果变量为空可能会导致假阳性等情况)(区分大小写):

     if [[ ! ${str##*$substr*} ]] ;then  #found

享受。


2
啊啊啊 - 直到我发现 ${str,,} 中的两个逗号将 $str 转换为小写字母,我才理解它。伟大的解决方案/伟大的列表! - hey
如果我正在测试一个变量,那么现在该怎么处理 ${str}${$MYVAR,,} 有效吗?Bash 显示 bad substitution - Sam Sirry
@SamSirry,你的示例中有一个拼写错误,请删除第二个$。它应该是${VAR,,}。请注意,如果这不起作用,则表示您使用的是旧的shell,并且必须使用上面提到的其他选项之一。 - Mike Q
@MikeQ,谢谢。我现在明白${str}中的str是一个变量名而不是字面字符串。这解决了我的问题 :) - Sam Sirry

31

这个也可以工作:

if printf -- '%s' "$haystack" | egrep -q -- "$needle"
then
  printf "Found needle in haystack"
fi

负面测试结果为:

if ! printf -- '%s' "$haystack" | egrep -q -- "$needle"
then
  echo "Did not find needle in haystack"
fi

我想这种风格更经典一些,不太依赖于Bash shell的特性。

--参数是出于纯POSIX的谨慎考虑,用于防止与选项相似的输入字符串,例如--abc-a

注意:在紧密循环中,此代码将比使用内部Bash shell特性慢得多,因为将创建一个(或两个)单独的进程,并通过管道连接。


5
但是帖子没有说明使用的是哪个版本的Bash;例如,较旧的Bash(如Solaris中经常出现的)可能不包含这些新的Bash功能。我曾在使用Bash 2.0的Solaris上遇到过这个确切问题(即Bash模式匹配未实现)。 - michael
2
“echo” 不可移植,你应该使用 “printf '%s' "$haystack"” 代替。 - alexia
2
不要使用 echo 输出除了 - 以外的字面文本或带转义的文本。尽管它可能适用于你的情况,但它并不可移植。甚至在 Bash 中,echo 命令的行为也会根据是否设置了 xpg_echo 选项而有所不同。附言: 我之前的评论忘记关闭双引号了。 - alexia
1
@kevinarpe 我不确定,--POSIX spec for printf 中没有列出,但是你应该始终使用 printf '%s' "$anything",以避免 $anything 包含 % 字符时出现问题。 - alexia
1
基于那个,很可能是这样的。 - alexia
显示剩余3条评论

26

正如Paul在他的性能比较中提到的

if echo "abcdefg" | grep -q "bcdef"; then
    echo "String contains is true."
else
    echo "String contains is not true."
fi

这个类似于 POSIX 标准的 'case "$string" in' Marcus 给出的答案,但比 case 语句的答案更易读。同时请注意,这种方法比使用 case 语句慢得多得多。正如 Paul 指出的那样,请勿在循环中使用。


而且它是唯一一个在现代GNU bash(5.x)中真正有效的。 - mirekphd

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