Bash 的模式拓展

警告
本文最后更新于 2023-10-22,文中内容可能已过时。

Bash 的模式拓展

在最开始学习 Linux 的时候,曾经做过笔记,研究过 globbing 和正则表达式的区别《Linux 中的正则表达式.docx》,但是没有研究其原理,今天在这里补充完整了

简介

我们在《Bash 的基本语法》的命令的格式小节中提到过,Shell 接收到用户输入的命令以后,会根据空格将用户的输入,拆分成一个个参数,这里我们换个称呼,我们称之为词元(token)。然后,Shell 会扩展词元里面的特殊字符,扩展完成后才会调用相应的命令。

对应官方文档的 3.5 小节 Shell Expansions

这种特殊字符的扩展,称为模式扩展(globbing)。其中有些用到通配符,又称为通配符扩展(wildcard expansion)。Bash 一共提供八种扩展。

  • 波浪线扩展,文件名拓展
  • ? 字符扩展,文件名拓展
  • * 字符扩展,文件名拓展
  • 方括号扩展,文件名拓展
  • 大括号扩展
  • 变量扩展
  • 子命令扩展
  • 算术扩展

本章介绍这八种扩展。

文件名拓展的意思就是,拓展结果只能是当前路径下的文件名,即使执行的命令跟查看文件无关,拓展结果也跟当前路径下存在的文件相关,比如echo

有一点需要注意的是,Bash 是先进行扩展,再执行命令。因此,扩展的结果是由 Bash 负责的,与所要执行的命令无关。命令本身并不存在参数扩展,收到什么参数就原样执行。这一点务必需要记住。例如我们调用 command 传入了 arg1 和 arg2 两个参数

1
$ command arg1 arg2 

恰好 arg1 和 arg2 中都包含特殊字符,因此经过 Bash 的拓展,变成了 ARG1 和 ARG2,然后再将其传递给 command 命令,对于 command 命令来说,他是不知道 arg1 被拓展成 ARG1 的,他只是拿到参数,然后执行,模式拓展跟其无关。

模块扩展的英文单词是globbing,这个词来自于早期的 Unix 系统有一个/etc/glob文件,保存扩展的模板。后来 Bash 内置了这个功能,但是这个名字就保留了下来。

模式拓展主要用于文件名路径名的匹配。

Bash 允许用户关闭扩展。

1
2
3
$ set -o noglob
# 或者
$ set -f

下面的命令可以重新打开扩展。

1
2
3
$ set +o noglob
# 或者
$ set +f

关于 set 命令,请看《set 命令和 shopt 命令》

和正则表达式的区分

模式扩展与正则表达式的关系是,模式扩展早于正则表达式出现,可以看作是原始的正则表达式。它的功能没有正则那么强大灵活,但是优点是简单和方便。

模式扩展的特殊字符数量明显比正则表达式少,而且有的符号意义还不一样,比如{}

那什么时候会进行模式拓展,什么时候进行正则匹配呢?

只要是明确指定参数为正则表达式的地方,都是正则表达式,比如 find -regexgrepegrep,其他地方都是模式拓展。

模式拓展在表示路径和文件名的时候不能也没法用引号包起来,但是正则表达式一定要用引号包起来。还是很好区分的

波浪线拓展

波浪线~会自动扩展成当前用户的主目录。后面跟上用户名会拓展成指定用户的家目录,如果用户不存在,则不会拓展包含~的字符串,直接将原字符串传递给命令。

1
2
3
4
5
6
# echo ~
/root
$ echo ~admin
/home/admin
$ echo ~usernotexist
~usernotexist

~+会扩展成当前所在的目录,等同于pwd命令。一般用在脚本中。

1
2
# echo ~+
/root

问号扩展

?字符用于匹配单个字符,可匹配文件路径里面的任意单个字符,但是不包括空字符。比如,test???匹配所有test后面跟着三个字符的文件名,但不匹配test后面跟着两个字符或者一个字符或者没有字符的文件名。

1
2
3
4
5
6
7
8
9
$ ls
test111.txt  test222.txt  test33.txt  test4.txt  test.txt
$ ls test???.txt
test111.txt  test222.txt
$ ^C
$ ls test??.txt
test33.txt
$ ls test?.txt
test4.txt

? 字符扩展属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展。如果不存在匹配的文件,扩展就不会发生。

1
2
3
4
5
6
$ ls
test111.txt  test222.txt  test33.txt  test4.txt  test.txt
$ ls aaa?.txt
ls: cannot access aaa?.txt: No such file or directory
$ echo aaa?.txt
aaa?.txt

星号拓展

*字符匹配零个或者多个(任意)字符,代表文件路径里面的任意数量的任意字符,包括零个字符。

1
2
3
4
$ ls
aaa.txt  test111.txt  test222.txt  test33.txt  test4.txt  test.txt
$ ls test*.txt
test111.txt  test222.txt  test33.txt  test4.txt  test.txt

通过ls *可以直接输出当前目录下的所有可见文件,但是不会输出文件名以.开头的隐藏文件,如果希望匹配隐藏文件,需要写成.*。实际上.*匹配的是当前路径下的所有文件(可见文件 + 隐藏文件)和父路径下的可见文件,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ ls
cc.txt
$ ls .*
.xx.txt

.:
cc.txt

..:
aaa.txt  bbb  test111.txt  test222.txt  test33.txt  test4.txt  test.txt

如果要匹配当前目录下的隐藏文件,同时要排除...这两个特殊的隐藏文件,可以与方括号扩展结合使用,写成.[!.]*

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ ll -a
total 8
drwxr-xr-x   2 root root 4096 Aug 13 18:56 .
drwxr-xr-x. 12 root root 4096 Aug 13 18:38 ..
-rw-r--r--   1 root root    0 Aug 13 18:45 aaa.txt
-rw-r--r--   1 root root    0 Aug 13 18:46 .b.txt
-rw-r--r--   1 root root    0 Aug 13 18:39 test111.txt
-rw-r--r--   1 root root    0 Aug 13 18:39 test222.txt
-rw-r--r--   1 root root    0 Aug 13 18:39 test33.txt
-rw-r--r--   1 root root    0 Aug 13 18:39 test4.txt
-rw-r--r--   1 root root    0 Aug 13 18:39 test.txt
$ ls
aaa.txt  test111.txt  test222.txt  test33.txt  test4.txt  test.txt
$ ls .[!.]*
.b.txt

如果想要匹配当前目录下的所有子目录中的文件,可以这样写:ls */*.txt。有几层子目录,就必须写几层星号。

Bash 4.0 引入了一个参数globstar,当该参数打开时,允许**匹配零个或多个子目录。因此,**/*.txt可以匹配顶层的文本文件和任意深度子目录的文本文件。该参数默认是关闭的。详细介绍请看后面shopt命令的介绍。

注意,*字符扩展属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展。如果不存在匹配的文件,就会原样输出。

1
2
3
4
5
6
7
8
$ ls
aaa.txt  test111.txt  test222.txt  test33.txt  test4.txt  test.txt
$ ls cc*.txt
ls: cannot access cc*.txt: No such file or directory
$ echo cc*.txt
cc*.txt
$ echo test*.txt
test111.txt test222.txt test33.txt test4.txt test.txt

方括号扩展

[]匹配指定集合中的任意单个字符,比如[abc]表示匹配单个字符 a 或者 b 或者 c,但是不匹配空字符。

注意是单个字符,无法匹配多个字符,想要匹配多个字符,得使用{},具体请看大括号拓展

1
2
3
4
5
6
7
8
$ ls
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt
$ ls file[12].txt
file1.txt  file2.txt
$ ls file[abde].txt
filea.txt  fileb.txt  filed.txt  filee.txt
$ ls file[abc12].txt
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt

方括号扩展有一个简写形式[start-end],表示匹配一个连续的范围。比如,[a-c]等同于[abc][0-9]匹配[0123456789]

下面是一些常用简写的例子。

  • [a-z]:所有小写字母。
  • [a-zA-Z]:所有小写字母与大写字母。
  • [a-zA-Z0-9]:所有小写字母、大写字母与数字。
1
2
3
4
5
6
$ ls
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt  file.txt
$ ls file[a-d].txt
filea.txt  fileb.txt  filec.txt  filed.txt
$ ls file[a-z1-9].txt
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt

[]扩展属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展。如果不存在匹配的文件,就会原样输出。

1
2
3
4
5
6
7
8
$ ls
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt
$ ls file[89].txt
ls: cannot access file[89].txt: No such file or directory
$ echo file[abc12].txt
file1.txt file2.txt filea.txt fileb.txt filec.txt
$ echo file[89].txt
file[89].txt

方括号扩展还有两种变体:[^...][!...]。它们表示匹配不在方括号里面的字符,这两种写法是等价的。比如,[^abc][!abc]表示匹配除了abc以外的字符。

1
2
3
4
5
6
7
8
$ ls
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt
$ ls file[!ab89].txt
file1.txt  file2.txt  filec.txt  filed.txt  filee.txt
$ ls file[!1-9].txt
filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt
$ ls file[!a-z].txt
file1.txt  file2.txt

注意,如果需要匹配[字符,可以放在方括号内,比如[[aeiou]。如果需要匹配连字号-,只能放在方括号内部的开头或结尾,比如[-aeiou][aeiou-]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ ls ab*.txt
ab-.txt  ab[.txt
$ ls ab[sd[].txt
ab[.txt
$ ls ab[12-].txt
ab-.txt
$ ls ab[-12].txt
ab-.txt
$ ls ab[12-3].txt
ls: cannot access ab[12-3].txt: No such file or directory

字符类拓展

[[: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
 4
 5
 6
 7
 8
 9
10
$ ls
file_10.txt  file_2.txt  file_4.txt  file_6.txt  file_8.txt
file_1.txt   file_3.txt  file_5.txt  file_7.txt  file_9.txt
$ ls file_[[:digit:]].txt
file_1.txt  file_3.txt  file_5.txt  file_7.txt  file_9.txt
file_2.txt  file_4.txt  file_6.txt  file_8.txt
$ ls file_[![:digit:]].txt
ls: cannot access file_[![:digit:]].txt: No such file or directory
$ echo file_[![:digit:]].txt
file_[![:digit:]].txt

大括号扩展

大括号的扩展行为和大括号在正则匹配中的作用差别非常大

大括号拓展的结果是一个序列 / 列表

大括号扩展{}表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔。比如,{1,2,3}扩展成1 2 3。注意,元素和逗号之间不能有空格,不然 shell 会将空格的两边单程独立的参数。元素可以为空,也就是可以出现逗号连着都逗号的情况{,,}

有一点需要注意,就是大括号的拓展的结果只与大括号中的元素有关,每一个元素即使是空元素都会被拓展为一个结果,可以简单大括号拓展理解为一个 for 循环,而与当前目录下是否存在对应的文件无关,这是因为大括号扩展不是文件名扩展。它会扩展成所有给定的值,而不管是否有对应的文件存在。

这与方括号扩展[]完全不同,如果匹配的文件不存在,方括号就不会扩展。这一点要注意区分。

大括号可以用于多字符的模式,例如{abc,12,78},方括号不行,方括号只能匹配单字符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ls
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt  file.txt
$ echo {1,2,3,4,5}
1 2 3 4 5
$ ls file{1,2,3,4,5,6}.txt
ls: cannot access file3.txt: No such file or directory
ls: cannot access file4.txt: No such file or directory
ls: cannot access file5.txt: No such file or directory
ls: cannot access file6.txt: No such file or directory
file1.txt  file2.txt
$ ls file{1,2,,,}.txt
file1.txt  file2.txt  file.txt  file.txt  file.txt

而且{}中的元素可以是其他拓展字符组成,也就是说{}可以包含{}~?*[]、而且大括号总是先于其他模式进行扩展,即,先拓展大括号,再拓展大括号内部的元素,注意,此时文件名扩展字符的拓展行为依然与当前路径下存在的文件相关

1
2
3
4
5
6
7
$ ls
file1.txt  file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt  file.txt
$ echo file{[a-z],{1,2,3,4,5}}.txt
filea.txt fileb.txt filec.txt filed.txt filee.txt file1.txt file2.txt file3.txt file4.txt file5.txt
$ ls file{[a-z],*}.txt
file1.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt  file.txt
file2.txt  filea.txt  fileb.txt  filec.txt  filed.txt  filee.txt

为了不让拓展结果与当前路径下的文件相关,一般我们只在大括号中使用大括号

1
2
$ echo file.{mp{3,4},txt}
file.mp3 file.mp4 file.txt

大括号扩展有一个简写形式{start..end},表示扩展成一个连续序列(或者称为列表)。比如,{a..z}可以扩展成 26 个小写英文字母。用起来非常方便

快速输出数字序列,用于 for 循环

关于 for 循环,请看《Bash 循环》的for...in 循环小节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ echo {1..10}
1 2 3 4 5 6 7 8 9 10
$ for code in {1..10}; do echo  "${code}"; done
1
2
3
4
5
6
7
8
9
10

数字序列支持前面补 0,这样,生成的数字的长度就是一致的

1
2
$ echo {001..100}
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100

支持逆序

1
2
$ echo {100..90}
100 99 98 97 96 95 94 93 92 91 90

支持指定步长,格式为{start..end..step}

1
2
$ echo {1..10..2}
1 3 5 7 9

但是太复杂的序列,shell 可能无法理解,就不会进行拓展

1
2
$ echo {a1..a6}
{a1..a6}

简单应用:

快速创建后缀为 1-10 的十个文件

1
2
3
4
$ touch file_{1..10}.txt
$ ls
file_10.txt  file_2.txt  file_4.txt  file_6.txt  file_8.txt
file_1.txt   file_3.txt  file_5.txt  file_7.txt  file_9.txt

快速创建一年的所有日期的文件夹(假设每个月 30 天)

1
$ mkdir -p {1..12}/{1..30}

先拓展第一个{},拓展到一半,发现了第二个{},那就把第二个{}拓展完,再继续拓展第一个{}。这种连续两个{}的写法的效果很像嵌套 for 循环,非常好用

变量拓展

Bash 将美元符号$开头的词元视为变量,将其扩展成变量值,变量名除了放在美元符号后面,也可以放在${}里面。详见《Bash 变量》一章。

1
2
3
$ name=xiashuo;echo $name;echo ${name}
xiashuo
xiashuo

${!string*}${!string@}返回所有匹配给定字符串string的环境变量名。

一般在 shell 中能直接访问的变量即为环境变量

1
2
$ echo ${!S*}
SECONDS SHELL SHELLOPTS SHLVL SSH_CLIENT SSH_CONNECTION SSH_TTY

子命令拓展

$(...)可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。$(...)可以嵌套,比如$(ls $(pwd)),然后用于循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ ls
file_10.txt  file_2.txt  file_4.txt  file_6.txt  file_8.txt
file_1.txt   file_3.txt  file_5.txt  file_7.txt  file_9.txt
$ for file in $(ls $(pwd)); do echo  "${file}"; done
file_10.txt
file_1.txt
file_2.txt
file_3.txt
file_4.txt
file_5.txt
file_6.txt
file_7.txt
file_8.txt
file_9.txt

实际上我们经常通过子命令拓展来将一个命令的结果赋予一个变量,在《Bash 变量》一章中有介绍。

而且我们经常通过子命令拓展将一个命令的结果作为另一个命令的参数,比如echo $(ls),效果等同于ls

1
files=$(ls)

还有另一种较老的语法,子命令放在反引号之中,也可以扩展成命令的运行结果。但是一般不使用这种语法,可读性太差了。

1
2
3
4
$ echo `date`
Sun Aug 13 23:41:38 CST 2023
$ echo $(date)
Sun Aug 13 23:41:52 CST 2023

算术扩展

$((...))可以扩展成整数运算的结果,详见《Bash 的算术运算》一章

1
2
3
4
5
6
$ echo $((2+2))
4
$ echo $((2/2))
1
$ echo $((2*20))
40

使用注意点

通配符有一些使用注意点,简单总结如下

  • 通配符是先解释,再执行:Bash 接收到命令以后,发现里面有通配符,会进行通配符扩展,然后再执行命令。

  • 文件名扩展在不匹配时,会原样输出。

  • 只适用于单层路径。

    所有文件名扩展只匹配单层路径,不能跨目录匹配,即无法匹配子目录里面的文件。或者说,?*这样的通配符,不能匹配路径分隔符(/)。

    如果要匹配子目录里面的文件,可以写成下面这样。

    1
    
    $ ls */*.txt

    Bash 4.0 新增了一个globstar参数,允许**匹配零个或多个子目录,详见后面shopt命令的介绍。

  • Bash 允许文件名使用通配符,即文件名包括特殊字符

    1
    2
    3
    
    $ touch *.txt
    $ ls
    *.txt

量词语法

量词语法用来控制模式匹配的次数。它只有在 Bash 的extglob参数打开的情况下才能使用,一般是默认关闭的。下面的命令可以查询。

1
2
$ shopt extglob
extglob            off

如果extglob参数是关闭的,可以用下面的命令打开。

1
$ shopt -s extglob

量词语法有下面几个。

  • ?(pattern-list):模式匹配零次或一次。
  • *(pattern-list):模式匹配零次或多次。
  • +(pattern-list):模式匹配一次或多次。
  • @(pattern-list):只匹配一次模式。
  • !(pattern-list):匹配给定模式以外的任何内容。

而且在()中,我们可以写模式,每一个模式可以由我们前面学过的特殊字符和普通字符组成,也可以全部都是普通字符,而且可以同时写多个模式,模式之间通过|将分开即可。

量词语法也属于文件名扩展,只有存在匹配文件的前提下,才会发生扩展,如果不存在可匹配的文件,就会原样输出。

简单实践如下:

1
2
3
4
5
6
7
8
9
$ ls
aaa_10.txt  aaa_2.txt  aaa_4.txt  aaa_6.txt  aaa_8.txt  file_1.txt  file_3.txt  file_5.txt
aaa_1.txt   aaa_3.txt  aaa_5.txt  aaa_7.txt  aaa_9.txt  file_2.txt  file_4.txt  *.txt
$ echo +([a-z])_+([1-9]).txt
aaa_1.txt aaa_2.txt aaa_3.txt aaa_4.txt aaa_5.txt aaa_6.txt aaa_7.txt aaa_8.txt aaa_9.txt file_1.txt file_2.txt file_3.txt file_4.txt file_5.txt
$ echo @(file|aaa)_+([1-9]).txt
aaa_1.txt aaa_2.txt aaa_3.txt aaa_4.txt aaa_5.txt aaa_6.txt aaa_7.txt aaa_8.txt aaa_9.txt file_1.txt file_2.txt file_3.txt file_4.txt file_5.txt
$ echo bbb_+([1-9]).txt
bbb_+([1-9]).txt

shopt 命令

很好记,show option 简写为 shopt

后面在《set 命令和 shopt 命令》的shopt 命令小节我们还会对 shopt 进行学习

shopt命令可以调整 Bash 的行为。它有好几个参数跟通配符扩展有关。

shopt命令的使用方法如下。

1
2
3
4
5
6
7
8
# 打开某个参数,只在当前session中生效
$ shopt -s [optionname]

# 关闭某个参数,只在当前session中生效
$ shopt -u [optionname]

# 查询某个参数关闭还是打开
$ shopt [optionname]

常用参数如下:

  • 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

0%