有时候需要一些简单的模式匹配,正则有点杀鸡用牛刀的意思,可以考虑The GNU C Library提供的fnmatch函数:
#include
int fnmatch(const char *pattern, const char *string, int flags);
fnmatch用来检查string是否匹配pattern所表示的模式,其使用shell通配符模式,具体规则参见glob(7)
,主要有:
?
(不在中括号内的):匹配单个字符
*
(不在中括号内的):匹配一个字符串,包括空字符串
对于一个路径,匹配分别应用于其被/
分隔的各个部分,/
不会被?
或*
匹配到
.
开头的文件必须显式匹配,例如rm *
并不会删除.profile
[...]
(第一个字符不是!
):匹配被中括号包围的任一字符,括号间不可为空,例如[][!]
匹配]
,[
或!
中括号内可以使用range,例如[1-5]
匹配1,2,3,4,5,如果要匹配-
,要把-
放到括号中的最前或最后位置。[--0]
匹配-
,.
,0
,/
不会被匹配,因为它是目录分隔符
如果中括号内首字符是!
:表示取反,其中字符不会被匹配到
关于参数flag
FNM_NOESCAPE 将反斜杠视为普通字符
FNM_PATHNAME(或FNM_FILE_NAME) *
,?
和[]
中的/
不匹配字符串中的/
,只能显式指定/
,比如:
fnmatch("*", "a/b", FNM_PATHNAME) 不匹配
fnmatch("*", "a/b", 0) 匹配
FNM_PERIOD 模式不能匹配.
开头的字符串,除非显式指定,如果同时设置了FNM_PATHNAME,上述特殊处理还适用于字符串中/
之后.
开头的子串,比如:
fnmatch("*", ".b", FNM_PERIOD) 不匹配
fnmatch(".*", ".b", FNM_PERIOD) 匹配
fnmatch("*/*", "a/.b", FNM_PATHNAME|FNM_PERIOD) 不匹配
FNM_CASEFOLD 忽略大小写
关于返回值
如果匹配成功返回0,如果没有匹配返回FNM_NOMATCH,如果有错误返回其他非零值
Extended Globbing
ksh引入了extended patterns,现在也被其他shell支持。默认情况下extglob是关闭的,需要用shopt -s extglob
开启1。
格式如下(可参考fnmatch(3)):
?(pattern-list) – Matches zero or one occurrence of the given patterns
*(pattern-list) – Matches zero or more occurrences of the given patterns
+(pattern-list) – Matches one or more occurrences of the given patterns
@(pattern-list) – Matches exactly one occurrence of the given patterns
!(pattern-list) – Matches anything except the given patterns
除了@,the leading character和正则表达式中的含义类似2:
Bash Regular Expression
?(pattern-list) (...)?
*(pattern-list) (...)*
+(pattern-list) (...)+
@(pattern-list) not a RE syntax
!(pattern-list) "!" used as for negative assertions in RE syntax
举例来说,
+(2).jpg
会匹配2.jpg,22.jpg,222.jpg……
+(ab).jpg
会匹配ab.jpg,abab.jpg,ababab.jpg……
pattern-list可以由多个pattern组成,每个pattern用|
隔开,例如:
@(ab|def)*@(.jpg|.gif)
匹配已ab或def开头的jpg或gif图片
1122199m匹配模式+(1??|?1?2)m
,但不匹配@(1??|?1?2)m
,因为1122匹配上了?1?2,199匹配上了1??,共匹配上了两次。
多字节字符串和宽字符串
fnmatch只能处理单字节字符串,如果源文件是utf8编码的,???
才匹配一个中文,如果源文件是gbk编码的,??
匹配一个中文。但我们在使用shell时,?却能匹配一个中文字符。
当shell在命令的参数中遇到了通配符时,会将其当做路径或文件名区在磁盘上搜寻最可能的匹配,若符合要求的匹配存在,则进行替换,否则就将该通配符作为一个普通字符传递给命令,然后由命令进行处理。3
所以模式匹配是由shell实现的,我找了 bash 的源码,发现其有一个glob的库来实现匹配功能,它有两个函数实现了与fnmatch近似的功能,并且参数和返回值含义也相同:
int strmatch(const char *pattern, const char *string, int flags);
int wcsmatch(const wchar_t *wpattern, const wchar_t *wstring, int flags);
strmatch支持多字节字符串,wcsmatch用于宽字符串。strmatch内部是先将多字节字符串转换为宽字符串,再做的匹配。窄字符串和宽字符的匹配方法则是相同的。
处理多字节字符串和宽字符串时,如果源文件使用utf8编码,需要用setlocale将LC_CTYPE也设置为utf8,比如setlocale(LC_CTYPE, "en_US.utf8")
。
其他模式匹配方法
glob用于寻找当前目录下符合模式的pathnames。
int glob(const char *pattern, int flags, int (*errfunc) (const char *epath, int eerrno), glob_t *pglob);
输出当前目录下所有txt文件:
glob_t gbuf;
gbuf.gl_offs = 0;
glob("*.txt", GLOB_DOOFFS, NULL, &gbuf);
for(size_t i = 1; i <= gbuf.gl_pathc; ++i)
{
printf("%d: %s\n", i, gbuf.gl_pathv[i-1]);
}
globfree(&gbuf);
glob函数中No tilde expansion or parameter substitution is done; if you want these, use wordexp。
int wordexp(const char *s, wordexp_t *p, int flags);
同样这两个函数也只支持单字节字符串。
fnmatch、glob、wordexp都是GNU C Library第10章模式匹配中的一部分,该章还包括正则表达式匹配,详细参见 Pattern Matching。
Extended globbing ↩︎
Bash Extended Globbing ↩︎
Linux Bash之通配符 ↩︎