Bash 的模式拓展
Bash 的模式拓展
在最开始学习 Linux 的时候,曾经做过笔记,研究过 globbing 和正则表达式的区别《Linux 中的正则表达式.docx》,但是没有研究其原理,今天在这里补充完整了
简介
我们在《Bash 的基本语法》的命令的格式
小节中提到过,Shell 接收到用户输入的命令以后,会根据空格将用户的输入,拆分成一个个参数,这里我们换个称呼,我们称之为词元(token)。然后,Shell 会扩展词元里面的特殊字符,扩展完成后才会调用相应的命令。
对应官方文档的 3.5 小节 Shell Expansions
这种特殊字符的扩展,称为模式扩展(globbing)。其中有些用到通配符,又称为通配符扩展(wildcard expansion)。Bash 一共提供八种扩展。
- 波浪线扩展,文件名拓展
?
字符扩展,文件名拓展*
字符扩展,文件名拓展- 方括号扩展,文件名拓展
- 大括号扩展
- 变量扩展
- 子命令扩展
- 算术扩展
本章介绍这八种扩展。
文件名拓展的意思就是,拓展结果只能是当前路径下的文件名,即使执行的命令跟查看文件无关,拓展结果也跟当前路径下存在的文件相关,比如
echo
有一点需要注意的是,Bash 是先进行扩展,再执行命令。因此,扩展的结果是由 Bash 负责的,与所要执行的命令无关。命令本身并不存在参数扩展,收到什么参数就原样执行。这一点务必需要记住。例如我们调用 command 传入了 arg1 和 arg2 两个参数
|
|
恰好 arg1 和 arg2 中都包含特殊字符,因此经过 Bash 的拓展,变成了 ARG1 和 ARG2,然后再将其传递给 command 命令,对于 command 命令来说,他是不知道 arg1 被拓展成 ARG1 的,他只是拿到参数,然后执行,模式拓展跟其无关。
模块扩展的英文单词是
globbing
,这个词来自于早期的 Unix 系统有一个/etc/glob
文件,保存扩展的模板。后来 Bash 内置了这个功能,但是这个名字就保留了下来。
模式拓展主要用于文件名路径名的匹配。
Bash 允许用户关闭扩展。
|
|
下面的命令可以重新打开扩展。
|
|
关于 set 命令,请看《set 命令和 shopt 命令》
和正则表达式的区分
模式扩展与正则表达式的关系是,模式扩展早于正则表达式出现,可以看作是原始的正则表达式。它的功能没有正则那么强大灵活,但是优点是简单和方便。
模式扩展的特殊字符数量明显比正则表达式少,而且有的符号意义还不一样,比如{}
。
那什么时候会进行模式拓展,什么时候进行正则匹配呢?
只要是明确指定参数为正则表达式的地方,都是正则表达式,比如 find -regex
、 grep
、 egrep
,其他地方都是模式拓展。
模式拓展在表示路径和文件名的时候不能也没法用引号包起来,但是正则表达式一定要用引号包起来。还是很好区分的
波浪线拓展
波浪线~
会自动扩展成当前用户的主目录。后面跟上用户名会拓展成指定用户的家目录,如果用户不存在,则不会拓展包含~
的字符串,直接将原字符串传递给命令。
|
|
~+
会扩展成当前所在的目录,等同于pwd
命令。一般用在脚本中。
|
|
问号扩展
?
字符用于匹配单个字符,可匹配文件路径里面的任意单个字符,但是不包括空字符。比如,test???
匹配所有test
后面跟着三个字符的文件名,但不匹配test
后面跟着两个字符或者一个字符或者没有字符的文件名。
|
|
?
字符扩展属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展。如果不存在匹配的文件,扩展就不会发生。
|
|
星号拓展
*
字符匹配零个或者多个(任意)字符,代表文件路径里面的任意数量的任意字符,包括零个字符。
|
|
通过ls *
可以直接输出当前目录下的所有可见文件,但是不会输出文件名以.
开头的隐藏文件,如果希望匹配隐藏文件,需要写成.*
。实际上.*
匹配的是当前路径下的所有文件(可见文件 + 隐藏文件)和父路径下的可见文件,
|
|
如果要匹配当前目录下的隐藏文件,同时要排除.
和..
这两个特殊的隐藏文件,可以与方括号扩展结合使用,写成.[!.]*
。
|
|
如果想要匹配当前目录下的所有子目录中的文件,可以这样写:ls */*.txt
。有几层子目录,就必须写几层星号。
Bash 4.0 引入了一个参数
globstar
,当该参数打开时,允许**
匹配零个或多个子目录。因此,**/*.txt
可以匹配顶层的文本文件和任意深度子目录的文本文件。该参数默认是关闭的。详细介绍请看后面shopt
命令的介绍。
注意,*
字符扩展属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展。如果不存在匹配的文件,就会原样输出。
|
|
方括号扩展
[]
匹配指定集合中的任意单个字符,比如[abc]
表示匹配单个字符 a 或者 b 或者 c,但是不匹配空字符。
注意是单个字符,无法匹配多个字符,想要匹配多个字符,得使用
{}
,具体请看大括号拓展
|
|
方括号扩展有一个简写形式[start-end]
,表示匹配一个连续的范围。比如,[a-c]
等同于[abc]
,[0-9]
匹配[0123456789]
。
下面是一些常用简写的例子。
[a-z]
:所有小写字母。[a-zA-Z]
:所有小写字母与大写字母。[a-zA-Z0-9]
:所有小写字母、大写字母与数字。
|
|
[]
扩展属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展。如果不存在匹配的文件,就会原样输出。
|
|
方括号扩展还有两种变体:[^...]
和[!...]
。它们表示匹配不在方括号里面的字符,这两种写法是等价的。比如,[^abc]
或[!abc]
表示匹配除了a
、b
、c
以外的字符。
|
|
注意,如果需要匹配[
字符,可以放在方括号内,比如[[aeiou]
。如果需要匹配连字号-
,只能放在方括号内部的开头或结尾,比如[-aeiou]
或[aeiou-]
。
|
|
字符类拓展
[[:class:]]
表示一个字符类,一个字符类会被扩展成一个属于此类型的字符。常用的字符类如下。
一般用的不多,因为写起来有点麻烦
[[:alnum:]]
:匹配任意英文字母与数字[[:alpha:]]
:匹配任意英文字母[[:blank:]]
:空格和 Tab 键。[[:cntrl:]]
:ASCII 码 0-31 的不可打印字符。[[:digit:]]
:匹配任意数字 0-9。[[:graph:]]
:A-Z、a-z、0-9 和标点符号。[[:lower:]]
:匹配任意小写字母 a-z。[[:print:]]
:ASCII 码 32-127 的可打印字符。[[:punct:]]
:标点符号(除了 A-Z、a-z、0-9 的可打印字符)。[[:space:]]
:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)。[[:upper:]]
:匹配任意大写字母 A-Z。[[:xdigit:]]
:16 进制字符(A-F、a-f、0-9)。
字符类的第一个方括号后面,可以加上感叹号!
,表示否定。
[![:alnum:]]
:匹配任意不是英文字母与数字的字符[![:alpha:]]
:匹配任意不是英文字母的字符[![:blank:]]
:匹配空格和 Tab 键以外的字符。[![:cntrl:]]
:匹配 ASCII 码为 0-31 的不可打印字符以外的字符。[![:digit:]]
:匹配数字 0-9 以外的字符。[![:graph:]]
:匹配 A-Z、a-z、0-9 和标点符号以外的字符。[![:lower:]]
:匹配小写字母 a-z 以外的字符。[![:print:]]
:匹配 ASCII 码为 32-127 的可打印字符以外的字符。[![:punct:]]
:匹配标点符号以外的字符。[![:space:]]
:匹配空白符以外的字符。[![:upper:]]
:匹配大写字母 A-Z 以外的字符。[![:xdigit:]]
:匹配 16 进制字符(A-F、a-f、0-9)以外的字符。
字符类也属于文件名扩展,如果没有匹配的文件名,字符类就会原样输出
|
|
大括号扩展
大括号的扩展行为和大括号在正则匹配中的作用差别非常大
大括号拓展的结果是一个序列 / 列表
大括号扩展{}
表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔。比如,{1,2,3}
扩展成1 2 3
。注意,元素和逗号之间不能有空格,不然 shell 会将空格的两边单程独立的参数。元素可以为空,也就是可以出现逗号连着都逗号的情况{,,}
。
有一点需要注意,就是大括号的拓展的结果只与大括号中的元素有关,每一个元素即使是空元素都会被拓展为一个结果,可以简单大括号拓展理解为一个 for 循环,而与当前目录下是否存在对应的文件无关,这是因为大括号扩展不是文件名扩展。它会扩展成所有给定的值,而不管是否有对应的文件存在。
这与方括号扩展
[]
完全不同,如果匹配的文件不存在,方括号就不会扩展。这一点要注意区分。大括号可以用于多字符的模式,例如{abc,12,78},方括号不行,方括号只能匹配单字符。
|
|
而且{}
中的元素可以是其他拓展字符组成,也就是说{}
可以包含{}
、~
、?
、*
、[]
、而且大括号总是先于其他模式进行扩展,即,先拓展大括号,再拓展大括号内部的元素,注意,此时文件名扩展字符的拓展行为依然与当前路径下存在的文件相关。
|
|
为了不让拓展结果与当前路径下的文件相关,一般我们只在大括号中使用大括号
|
|
大括号扩展有一个简写形式{start..end}
,表示扩展成一个连续序列(或者称为列表)。比如,{a..z}
可以扩展成 26 个小写英文字母。用起来非常方便
快速输出数字序列,用于 for 循环
关于 for 循环,请看《Bash 循环》的
for...in 循环
小节
|
|
数字序列支持前面补 0,这样,生成的数字的长度就是一致的
|
|
支持逆序
|
|
支持指定步长,格式为{start..end..step}
|
|
但是太复杂的序列,shell 可能无法理解,就不会进行拓展
|
|
简单应用:
快速创建后缀为 1-10 的十个文件
|
|
快速创建一年的所有日期的文件夹(假设每个月 30 天)
|
|
先拓展第一个{}
,拓展到一半,发现了第二个{}
,那就把第二个{}
拓展完,再继续拓展第一个{}
。这种连续两个{}
的写法的效果很像嵌套 for 循环,非常好用
变量拓展
Bash 将美元符号$
开头的词元视为变量,将其扩展成变量值,变量名除了放在美元符号后面,也可以放在${}
里面。详见《Bash 变量》一章。
|
|
${!string*}
或${!string@}
返回所有匹配给定字符串string
的环境变量名。
一般在 shell 中能直接访问的变量即为环境变量
|
|
子命令拓展
$(...)
可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。$(...)
可以嵌套,比如$(ls $(pwd))
,然后用于循环
|
|
实际上我们经常通过子命令拓展来将一个命令的结果赋予一个变量,在《Bash 变量》一章中有介绍。
而且我们经常通过子命令拓展将一个命令的结果作为另一个命令的参数,比如
echo $(ls)
,效果等同于ls
|
|
还有另一种较老的语法,子命令放在反引号之中,也可以扩展成命令的运行结果。但是一般不使用这种语法,可读性太差了。
|
|
算术扩展
$((...))
可以扩展成整数运算的结果,详见《Bash 的算术运算》一章
|
|
使用注意点
通配符有一些使用注意点,简单总结如下
-
通配符是先解释,再执行:Bash 接收到命令以后,发现里面有通配符,会进行通配符扩展,然后再执行命令。
-
文件名扩展在不匹配时,会原样输出。
-
只适用于单层路径。
所有文件名扩展只匹配单层路径,不能跨目录匹配,即无法匹配子目录里面的文件。或者说,
?
或*
这样的通配符,不能匹配路径分隔符(/
)。如果要匹配子目录里面的文件,可以写成下面这样。
1
$ ls */*.txt
Bash 4.0 新增了一个
globstar
参数,允许**
匹配零个或多个子目录,详见后面shopt
命令的介绍。 -
Bash 允许文件名使用通配符,即文件名包括特殊字符
1 2 3
$ touch *.txt $ ls *.txt
量词语法
量词语法用来控制模式匹配的次数。它只有在 Bash 的extglob
参数打开的情况下才能使用,一般是默认关闭的。下面的命令可以查询。
|
|
如果extglob
参数是关闭的,可以用下面的命令打开。
|
|
量词语法有下面几个。
?(pattern-list)
:模式匹配零次或一次。*(pattern-list)
:模式匹配零次或多次。+(pattern-list)
:模式匹配一次或多次。@(pattern-list)
:只匹配一次模式。!(pattern-list)
:匹配给定模式以外的任何内容。
而且在()
中,我们可以写模式,每一个模式可以由我们前面学过的特殊字符和普通字符组成,也可以全部都是普通字符,而且可以同时写多个模式,模式之间通过|
将分开即可。
量词语法也属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展,如果不存在可匹配的文件,就会原样输出。
简单实践如下:
|
|
shopt 命令
很好记,show option 简写为 shopt
后面在《set 命令和 shopt 命令》的
shopt 命令
小节我们还会对 shopt 进行学习
shopt
命令可以调整 Bash 的行为。它有好几个参数跟通配符扩展有关。
shopt
命令的使用方法如下。
|
|
常用参数如下:
-
dotglob(默认关闭):开启后可以让扩展结果包括隐藏文件(即
.
开头的文件) -
nullglob(默认关闭):开启后可以让通配符不匹配任何文件名时,返回空字符。默认行为是直接将用户输入的字符串传递给命令
-
failglob(默认关闭,建议开启):开启后使得通配符不匹配任何文件名时,Bash 会直接报错,而不是让各个命令去处理。
-
extglob(默认关闭,建议开启):开启后可以支持量词语法。在
量词语法
小节学习过 -
nocaseglob(默认关闭):开启后可以让通配符扩展的时候不区分大小写。
-
globstar(默认关闭,建议开启):可以使得
**
匹配零个或多个子目录。有这样一个目录层级结构(创建方式为
mkdir -p {aaa,bbb,ccc}/{1..10};touch test.txt {aaa,bbb,ccc}/test.txt {aaa,bbb,ccc}/{1..10}/test.txt
)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
. ├── aaa │ ├── 1 │ │ └── test.txt │ ├── 10 │ │ └── test.txt │ ├── 2 │ │ └── test.txt │ ├── 3 │ │ └── test.txt │ ├── 4 │ │ └── test.txt │ ├── 5 │ │ └── test.txt │ ├── 6 │ │ └── test.txt │ ├── 7 │ │ └── test.txt │ ├── 8 │ │ └── test.txt │ ├── 9 │ │ └── test.txt │ └── test.txt ├── bbb │ ├── 1 │ │ └── test.txt │ ├── 10 │ │ └── test.txt │ ├── 2 │ │ └── test.txt │ ├── 3 │ │ └── test.txt │ ├── 4 │ │ └── test.txt │ ├── 5 │ │ └── test.txt │ ├── 6 │ │ └── test.txt │ ├── 7 │ │ └── test.txt │ ├── 8 │ │ └── test.txt │ ├── 9 │ │ └── test.txt │ └── test.txt ├── ccc │ ├── 1 │ │ └── test.txt │ ├── 10 │ │ └── test.txt │ ├── 2 │ │ └── test.txt │ ├── 3 │ │ └── test.txt │ ├── 4 │ │ └── test.txt │ ├── 5 │ │ └── test.txt │ ├── 6 │ │ └── test.txt │ ├── 7 │ │ └── test.txt │ ├── 8 │ │ └── test.txt │ ├── 9 │ │ └── test.txt │ └── test.txt └── test.txt
想要直接输出所有的子文件夹下的文件,就得把所有的层级都写出来,
1 2
$ echo {,*/,*/*/}*.txt test.txt aaa/test.txt bbb/test.txt ccc/test.txt aaa/10/test.txt aaa/1/test.txt aaa/2/test.txt aaa/3/test.txt aaa/4/test.txt aaa/5/test.txt aaa/6/test.txt aaa/7/test.txt aaa/8/test.txt aaa/9/test.txt bbb/10/test.txt bbb/1/test.txt bbb/2/test.txt bbb/3/test.txt bbb/4/test.txt bbb/5/test.txt bbb/6/test.txt bbb/7/test.txt bbb/8/test.txt bbb/9/test.txt ccc/10/test.txt ccc/1/test.txt ccc/2/test.txt ccc/3/test.txt ccc/4/test.txt ccc/5/test.txt ccc/6/test.txt ccc/7/test.txt ccc/8/test.txt ccc/9/test.txt
现在,我们可以开启
globstar
1
shopt -s globstar
然后直接使用
**
即可1 2
$ echo **/*.txt aaa/10/test.txt aaa/1/test.txt aaa/2/test.txt aaa/3/test.txt aaa/4/test.txt aaa/5/test.txt aaa/6/test.txt aaa/7/test.txt aaa/8/test.txt aaa/9/test.txt aaa/test.txt bbb/10/test.txt bbb/1/test.txt bbb/2/test.txt bbb/3/test.txt bbb/4/test.txt bbb/5/test.txt bbb/6/test.txt bbb/7/test.txt bbb/8/test.txt bbb/9/test.txt bbb/test.txt ccc/10/test.txt ccc/1/test.txt ccc/2/test.txt ccc/3/test.txt ccc/4/test.txt ccc/5/test.txt ccc/6/test.txt ccc/7/test.txt ccc/8/test.txt ccc/9/test.txt ccc/test.txt test.txt
-
login_shell 返回当前 shell 是否式登录式 shell