Glob Matching Can Be Simple and Fast Too
research.swtch.com
research.swtch.com
Python's glob() (fnmatch really) translates the glob to a regular expression then uses its re library:
https://github.com/python-git/python/blob/715a6e5035bb21ac49...
https://github.com/python-git/python/blob/715a6e5035bb21ac49...
Seriously, though, it was a bit of an afterthought. A team at Google was |ing together a ton of regexps and came to me for something better, so I wrote RE2::Set. I'm glad it helps others too.
It's specifically a CPython optimisation, it may (and usually will) not hold up on alternate implementations like pypy or jyton.
If PyPy or Jython want to reuse that core library code, it's on them to make whatever changes are needed to make it fast on those implementations.
https://github.com/borgbackup/borg/blob/master/src/borg/shel... http://borgbackup.readthedocs.io/en/stable/usage.html#borg-h... ("Shell-style patterns, selector sh:")
Other Russ' papers on regular expression matching: https://swtch.com/~rsc/regexp/
It was probably one of the more useful/understandable pages I found in that process.
1. The glob crate, which is stdlib ejecta from pre-1.0, and appears to be in maintenance mode (it hasn't seen a "real" commit since Jan 2016).
2. The regex crate, which is very actively developed (and in fact inspired by rsc's writings and Go's regex implementation).
If it's the former, then indeed it's probably as disinteresting as "nobody has bothered to ever benchmark this crate". But if it's the latter then I bet burntsushi would be very interested!
I understand why monolithic utility and frameworks need constant updating. It seems that in the "micro library" world, though, we should see and encourage "finished" libraries.
EDIT: It's also worth mentioning that there's an important mental distinction between a "1.0-level" maintenance mode crate and a pre-1.0 maintenance mode crate, as the former at least implies that somebody considers the crate to be marginally usable and feature-complete. glob is a pre-1.0 crate, so even though patches are accepted, I wouldn't necessarily consider it to be well-vetted without doing more research.
https://github.com/BurntSushi/ripgrep/tree/master/globset
> This crate implements globs by converting them to regular expressions, and executing them with the regex crate.
extern crate glob;
extern crate time;
use glob::glob;
use time::PreciseTime;
fn main() {
std::env::set_current_dir("/tmp/glob").expect("cd");
for i in 0..100 {
let pattern = "a*".repeat(i)+"b";
let t = PreciseTime::now();
let mul = 100;
for j in 0..mul {
for entry in glob(&pattern).expect("Failed to read glob pattern") {
match entry {
Ok(path) => println!("{:?}", path.display()),
Err(e) => println!("{:?}", e),
}
}
}
let t1 = PreciseTime::now();
println!("{:?} {:?}", i, (t.to(t1).num_nanoseconds().expect("ok") as f64)/1e9/(mul as f64))
}
}
Honestly I'm not too worried about Rust's performance here: it's on the right side of the fence.The translation from glob to regex at the syntax level is likely a non-factor. It's the compilation of the regex that probably starts to matter.
In the regex article he notes that e.g. perl is subject to pathological behavior when you match a?^na^n against an a^n:
$ time perl -wE 'my $l = shift; my $str = "a" x $l; my $rx = "a?" x $l . $str; $str =~ /${rx}/' 28
real 0m13.278s
However changing the pattern to /${rx}b/ makes it execute almost instantly. This is because the matcher will look ahead for fixed non-pattern strings found in the pattern, and deduce that whatever globbing we're trying to match now it can't possibly matter if the string doesn't have a "b" in it.I wonder if any globbing implementations take advantage of that class of optimization, and if there's any cases where Russ's suggested solution of not backtracking produces different results than you'd get by backtracking, in particular with some of the extended non-POSIX glob syntax out there.
In your example pcre2 performs much better than perl btw: it errors with match limit exceeded (-47), while perl happily burns exponential CPU. It's now even worse than before Russ' original perl article. Now it descends into heap memory, before only into the stack. So now it will keep crunching forever on the whole heap, while before the perl 5.10 rewrite triggered by Russ it died fast on stack overflow.
> That's not globbing but using the regex matcher.
Indeed. I'm just using the regex behavior to show that perl's regex matcher uses a general optimization to defeat patterns like the ones Russ is discussing using a strategy orthogonal to handling backtracking.But Russ doesn't talk about using this strategy to optimize glob(), so it's worth pointing out that it could just as well be used in a glob() implementation.
I.e. just try to find a fixed part later in the pattern, and if it can't be found fail the entire match.
> I'm curious why the graph displays [perl]
> as exponential on linux, where it should be linear.
The glob() routine in perl uses a fork of a BSD glob which ships with perl: https://github.com/Perl/perl5/blob/blead/ext/File-Glob/bsd_g... > pcre2 performs much better than perl btw: it
> errors with match limit exceeded (-47)
The pcre2 library will still perform better if you adjust the match limit so that the pattern actually matches, e.g. with --match-limit=1000000000 to pcre2grep. That finishes in around 1s with PCRE, 10s with Perl.But in general a regex library can't be said to perform better just because it errors out with a match limit error sooner. That just means it's compiled with different defaults.
$ cat tglob.pl
#!/usr/bin/perl
use Time::HiRes qw(clock_gettime);
$| = 1;
chdir "/tmp/glob" || die "$!";
for($i=0; $i<9; $i++) {
$pattern = ("a*"x$i) . "b";
$t = clock_gettime(CLOCK_REALTIME);
$mul = 10;
if($i >= 5){
$mul = 1;
}
for($j=0; $j<$mul; $j++) {
glob $pattern;
}
$t1 = clock_gettime(CLOCK_REALTIME);
printf("%d %.9f\n", $i, ($t1-$t)/$mul);
}
$ perl -v
This is perl 5, version 18, subversion 2 (v5.18.2) built for x86_64-linux-gnu-thread-multi
...
$ perl tglob.pl
0 0.000004911
1 0.000016212
2 0.000088072
3 0.002416682
4 0.030226517
5 0.452545881
6 6.872966528
^C
You're the second person to claim that Perl calls the system glob though (someone in my blog comments did too). Maybe different versions of Perl do different things? This is Ubuntu 14.04 if that matters.macOS only? NetBSD? FreeBSD? OpenBSD?
If you tested on FreeBSD, please file a bug at https://bugs.freebsd.org/bugzilla/enter_bug.cgi?product=Base...
I'm not a project member but I'm a user of the system so it's in my interest that issues like this are resolved.
Please let me know whether or not you file a bug so that if you do I don't duplicate bug reports and if you don't I can do some benchmarking myself.
rm -rf /tmp/glob
mkdir /tmp/glob
cd /tmp/glob
touch $(perl -e 'print "a"x100')
And here's the program: #include <stdio.h>
#include <glob.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <time.h>
int
main(void)
{
glob_t g;
char pattern[1000], *p;
struct timespec t, t2;
double dt;
int i, j, k;
chdir("/tmp/glob");
setlinebuf(stdout);
int mul = 1000;
for(i = 0; i < 100; i++) {
p = pattern;
for (k = 0; k < i; k++) {
*p++ = 'a';
*p++ = '*';
}
*p++ = 'b';
*p = '\0';
printf("# %d %s\n", i, pattern);
clock_gettime(CLOCK_REALTIME, &t);
for (j = 0; j < mul; j++) {
memset(&g, 0, sizeof g);
if(glob(pattern, 0, 0, &g) != GLOB_NOMATCH) {
fprintf(stderr, "error: found matches\n");
exit(2);
}
globfree(&g);
}
clock_gettime(CLOCK_REALTIME, &t2);
t2.tv_sec -= t.tv_sec;
t2.tv_nsec -= t.tv_nsec;
dt = t2.tv_sec + (double)t2.tv_nsec/1e9;
printf("%d %.9f\n", i, dt/mul);
fflush(stdout);
if(dt/mul >= 0.0001)
mul = 1;
if(i >= 8 && dt/mul >= 10)
break;
}
}
I won't be filing any specific bugs myself. I mailed oss-security@ this morning, which should reach relevant BSD maintainers, but more bug filing can't hurt. # 0 b
0 0.000000661
# 1 a*b
1 0.000005204
# 2 a*a*b
2 0.000036157
# 3 a*a*a*b
3 0.001124974
# 4 a*a*a*a*b
4 0.046123932
# 5 a*a*a*a*a*b
5 0.577535034
# 6 a*a*a*a*a*a*b
6 8.666314375
# 7 a*a*a*a*a*a*a*b
7 119.796626615
(I didn't wait for 8.)Please file a bug.
[1] https://swtch.com/~rsc/regexp/regexp1.html
[2] https://web.eecs.utk.edu/~plank/plank/jgraph/jgraph.html
"Unfortunately, none of tehse protections address the cost of matching a single path element of a single file name. In 2005, CVE-2005-0256 was issued for a DoS vulnerability in WU-FTPD 2.6.2, because it ran for a very long time finding even a single match during:"
Very informative article. Thanks for it!
perl -MFile::Glob=bsd_glob -e 'print bsd_glob("{{a,b,c}{1,2,3}{{yuck,Yuck},{urgh,URGH}}}\n")'
Produces 36 lines representing all the iterations. Nest a bit deeper and it gets unwieldy.Looks like it exhibits the slower behavior:
n,elapsed
1,0.07
2,0.07
3,0.07
4,0.07
5,0.16
6,1.43
7,19.90
8,240.76
See this gist for the script https://gist.github.com/mixu/e4803da16e42439480eba2b29fa4448...Do not count WWW browsers amongst the number of those graphical FTP clients. The common WWW browsers that speak FTP use LIST or LIST -l . With the exception of Google Chrome when it thinks that it is talking to a VMS program, they do not pass pattern arguments, though.
However it should be noted that it is non portable to do globbing in Common Lisp, so I expect most users implement it using something CL-FAD or OSICAT and CL-PPCRE, and CL-PPCRE is efficient.
In POSIX, question marks match exactly one character, always. There's no need for backtracking.
* http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3...
> Consider the pattern "a star bx star cy star d". If we end the first star at the first bx, we have the rest of the name to find the cy and then the d. Using any later bx can only remove choices for cy and d; it cannot lead to a successful match that using the first bx missed. So we should implement a star bx star cy star d without any second-guessing, as “find a leading a, then find the earliest bx after that, then find the earliest cy after that, then find a trailing d, or else give up.”
This algorithm doesn't work if the pattern has question marks.
def name := "Hackernews"
# greeting == "Hi Hackernews!"
def greeting := `Hi $name!`
# language == "Lojban"
def `@language is awesome` := "Lojban is awesome"
A quirk of our presentation is that adjacent zero-or-more patterns degenerate, with each subsequent pattern matching the empty string. This mirrors the observation in the post that some systems can coalesce adjacent stars without changing the semantics: # one == "", two == "cool"
def `adjacent @one@two patterns` := "adjacent cool patterns"It's an effective filtering mechanism, but some consider it rather cruel and try to notify posters about it.
The bulk of the haskell code to do this:
parseGlob :: Char -> Char -> String -> Parser Glob
parseGlob escC sepC forbid =
many1' (gpart <|> sep <|> glob <|> alt) >>= return . GGroup . V.fromList
where gpart = globPart escC (sepC : (forbid ++ "{*")) >>= return . GPart
sep = satisfy (== ch2word sepC) >> return GSeparator
alt = do
_ <- AttoC.char '{'
choices <- sepBy' (GEmpty `option` parseGlob escC sepC (",}" ++ forbid)) (char ',')
_ <- AttoC.char '}'
return $ GAlternate $ V.fromList choices
glob = do
res <- takeWhile1 (== ch2word '*')
if B.length res == 1 then
return GSingle
else
return GDouble
wrapParens s = T.concat ["(", s, ")"]
globRegex :: Char -> Glob -> T.Text
globRegex sep GSingle = T.concat ["([^", T.singleton sep, "]*|\\", T.singleton sep, ")"]
globRegex _ GDouble = ".*"
globRegex _ GEmpty = ""
globRegex sep GSeparator = T.singleton sep
globRegex sep (GRepeat a) = T.concat ["(", T.concat (V.toList $ fmap (globRegex sep) a), ")*"]
globRegex sep (GGroup a) = T.concat $ V.toList $ fmap (globRegex sep) a
globRegex _ (GPart p) = T.concatMap efun base
where base = TE.decodeUtf8 p
escChars = S.fromList ".[]()\\{}^$*+"
efun c = if S.member c escChars
then T.concat ["\\", T.singleton c]
else T.singleton c
globRegex sep (GAlternate a) =
if V.null alts
then ""
else T.concat [altsStr, if hasEmpty then "?" else ""]
where hasEmpty = isJust $ V.find (== GEmpty) a
alts = fmap (globRegex sep) $ V.filter (/= GEmpty) a
altsStr = wrapParens $ T.intercalate "|" $ V.toList alts Why write a glob engine at all when you already have a fast regex
you didn't read the article. there are glob implementations that do just that.As an easy additional point to the lesson, it might not be clear to everyone why you have to take care on which regex engine you move to.
Please don't insinuate that someone hasn't read an article. "Did you even read the article? It mentions that" can be shortened to "The article mentions that."
https://github.com/skarnet/execline/raw/master/src/libexecli...
Simple.
http://www.in-ulm.de/~mascheck/various/argmax/
execlineb -c 'elglob a /*/*/*/* ls $a'
(statically-linked execlineb)If I am not mistken, ARG_MAX will be the limit.
Straightforward.