Bash 中的模式匹配

作者:Mitch Frazier

 

通配符已经存在很久了。 有些人甚至声称它们出现在古埃及的象形文字中。通配符允许您简洁地指定 一个匹配一组文件名的模式(例如,*.pdf获取所有 PDF 文件的列表)。通配符通常也称为 glob 模式(或使用它们时,称为“globbing”)。但是 glob 模式的用途不仅仅是生成有用的文件名列表。bash 手册页将 glob 模式简单地称为“模式匹配”。

 

首先,让我们快速回顾一下 bash 的 glob 模式。除了广为人知的简单通配符之外,bash 还具有扩展 globbing,它添加了额外的功能。这些扩展功能通过以下方式启用extglob选项。

模式 描述
* 匹配零个或多个字符
? 匹配任何单个字符
[...] 匹配集合中的任何字符
?(patterns) 匹配零个或一个模式出现次数 (extglob)
*(patterns) 匹配零个或多个模式出现次数 (extglob)
+(patterns) 匹配一个或多个模式出现次数 (extglob)
@(patterns) 匹配一个模式出现次数 (extglob)
!(patterns) 匹配任何与模式不匹配的内容 (extglob)

 

例如

$ ls
a.jpg  b.gif  c.png  d.pdf ee.pdf

$ ls *.jpg
a.jpg

$ ls ?.pdf
d.pdf

$ ls [ab]*
a.jpg  b.gif

$ shopt -s extglob   # turn on extended globbing

$ ls ?(*.jpg|*.gif)
a.jpg  b.gif

$ ls !(*.jpg|*.gif)  # not a jpg or a gif
c.png d.pdf ee.pdf

当首次使用扩展 globbing 时,它们中的许多似乎并没有像我最初认为的那样工作。例如,在我看来,给定a.jpg,模式?(*.jpg|a.jpg)应该不匹配,因为a.jpg匹配了两个模式,并且?是“零个或一个”,对吗?错了。 我的困惑是由于对描述的误读:不是文件名只能匹配一次,而是模式只能匹配一次。从正则表达式的角度来考虑

Glob 正则表达式等效项 描述
?(patterns) (regex)? 匹配一个可选的正则表达式
*(patterns) (regex)* 匹配零个或多个正则表达式出现次数
+(patterns) (regex)+ 匹配一个或多个正则表达式出现次数
@(patterns) (regex) 匹配正则表达式(一次出现)

 

所以,例如

$ ls *.pdf
ee.pdf  e.pdf  .pdf

$ ls ?(e).pdf    # zero or one "e" allowed
e.pdf  .pdf

$ ls *(e).pdf    # zero or more "e"s allowed
ee.pdf  e.pdf  .pdf

$ ls +(e).pdf    # one or more "e"s allowed
ee.pdf  e.pdf

$ ls @(e).pdf    # only one e allowed
e.pdf

当我在比较 glob 模式和正则表达式时,有一个重要的点需要指出,这可能不是立即可见的:glob 模式只是在 bash 中进行通用模式匹配的另一种语法。并且您可以在许多不同的地方使用它们

  • ==在 bash 的[[ expr ]]表达式中。
  • case命令的模式中。
  • 在参数扩展中 (%, %%, #, ##, /, //).

以下示例在 if 语句的表达式中使用模式匹配来测试变量的值是否为“something”或“anything”

$ shopt +s extglob

$ a=something
$ if [[ $a == +(some|any)thing ]]; then echo yes; else echo no; fi
yes

$ a=anything
$ if [[ $a == +(some|any)thing ]]; then echo yes; else echo no; fi
yes

$ a=nothing
$ if [[ $a == +(some|any)thing ]]; then echo yes; else echo no; fi
no

以下示例在 case 语句中使用模式匹配来确定文件是否为图像文件

shopt +s extglob
for f in $*
do
    case $f in
    !(*.gif|*.jpg|*.png))  # ! == does not match
        echo "Not an image: $f"
        ;;
    *)
        echo "Image: $f"
        ;;
    esac
done
$ bash script.sh a.jpg b.gif c.png d.pdf e.pdf
Image: a.jpg
Image: b.gif
Image: c.png
Not an image: d.pdf
Not an image: e.pdf

在上面的示例中,模式!(*.gif|*.jpg|*.png)如果文件名不是 gif、jpg 或 png,则会匹配。

以下示例在%%参数扩展中使用模式匹配来删除所有图像文件的扩展名

shopt -s extglob
for f in $*
do
    echo ${f%%*(.gif|.jpg|.png)}
done
$ bash script.sh a.jpg b.gif c.png d.pdf e.pdf
a
b
c
d.pdf
e.pdf

我最近才意识到一个功能是,您可以一举完成上述操作:如果您使用“*”或“@”作为变量名,则转换将一次对所有命令行参数完成。[给自己提个醒:以后总是要阅读段落的后半部分]

shopt -s extglob
echo ${*%%*(.gif|.jpg|.png)}
$ bash script.sh a.jpg b.gif c.png d.pdf e.pdf
a b c d.pdf e.pdf

这也适用于数组

shopt -s extglob
array=($*)
echo ${array[*]%%*(.gif|.jpg|.png)}
$ bash script.sh a.jpg b.gif c.png d.pdf e.pdf
a b c d.pdf e.pdf

这里最大的收获是停止将通配符视为仅用于获取文件名列表的机制,而开始将它们视为 glob 模式,这些模式可用于在 bash 脚本中进行通用模式匹配。将 glob 模式视为另一种语言的正则表达式。


在我的文章中找到的任何代码都应被视为按以下方式许可

# Copyright 2019 Mitch Frazier <mitch -at- linuxjournal.com>
#
# This software may be used and distributed according to the terms of the
# MIT License or the GNU General Public License version 2 (or any later version).

Mitch Frazier 是 Emerson Electric Co. 的嵌入式系统程序员。自 2000 年代初期以来,Mitch 一直是 Linux Journal 的贡献者和朋友。

加载 Disqus 评论