diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -408,6 +408,7 @@ 'phutil_get_library_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php', 'phutil_get_signal_name' => 'future/exec/execx.php', + 'phutil_glob_to_regex' => 'utils/utils.php', 'phutil_implode_html' => 'markup/render.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', 'phutil_is_utf8' => 'utils/utf8.php', diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -616,4 +616,72 @@ phutil_var_export(new PhutilTestPhobject())); } + public function testGlobToRegex() { + $cases = array( + '' => array( + array(''), + array('f', '/'), + ), + '*' => array( + array('foo'), + array('foo/', '/foo'), + ), + '**' => array( + array('foo', 'foo/', '/foo', 'foo/bar/baz'), + array(), + ), + 'foo.*' => array( + array('foo.php', 'foo.a', 'foo.'), + array('fooo.php', 'foo.php/foo'), + ), + 'fo?' => array( + array('foo', 'fot'), + array('fooo', 'ffoo', 'fo/'), + ), + 'fo{o,t}' => array( + array('foo', 'fot'), + array('fob', 'fo/'), + ), + 'foo(bar|foo)' => array( + array('foo(bar|foo)'), + array('foobar', 'foofoo'), + ), + 'foo,bar' => array( + array('foo,bar'), + array('foo', 'bar'), + ), + 'fo{o,\\,}' => array( + array('foo', 'fo,'), + array(), + ), + 'fo{o,\\\\}' => array( + array('foo', 'fo\\'), + array(), + ), + '/foo' => array( + array('/foo'), + array('foo'), + ), + ); + + foreach ($cases as $input => $expect) { + list($matches, $no_matches) = $expect; + + $regex = phutil_glob_to_regex($input); + PhutilTypeSpec::newFromString('regex')->check($regex); + + foreach ($matches as $match) { + $this->assertTrue( + (bool)preg_match($regex, $match), + pht('Expecting "%s" to match "%s".', $regex, $match)); + } + + foreach ($no_matches as $no_match) { + $this->assertFalse( + (bool)preg_match($regex, $no_match), + pht('Expecting "%s" not to match "%s".', $regex, $no_match)); + } + } + } + } diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1121,3 +1121,66 @@ // Let PHP handle everything else. return var_export($var, true); } + + +/** + * Returns a regular expression which is equivalent to the given glob pattern. + * + * This function was adapted from + * https://github.com/symfony/Finder/blob/master/Glob.php. + * + * @param string A glob pattern. + * @return regex + */ +function phutil_glob_to_regex($glob) { + $escaping = false; + $in_curlies = 0; + $regex = ''; + + for ($i = 0; $i < strlen($glob); $i++) { + $char = $glob[$i]; + $next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null; + + if (in_array($char, array('.', '(', ')', '|', '+', '^', '$'))) { + $regex .= "\\$char"; + } else if ($char === '*') { + if ($escaping) { + $regex .= '\\*'; + } else { + if ($next_char === '*') { + $regex .= '.*'; + } else { + $regex .= '[^/]*'; + } + } + } else if ($char === '?') { + $regex .= $escaping ? '\\?' : '[^/]'; + } else if ($char === '{') { + $regex .= $escaping ? '\\{' : '('; + if (!$escaping) { + ++$in_curlies; + } + } else if ($char === '}' && $in_curlies) { + $regex .= $escaping ? '}' : ')'; + if (!$escaping) { + --$in_curlies; + } + } else if ($char === ',' && $in_curlies) { + $regex .= $escaping ? ',' : '|'; + } else if ($char === '\\') { + if ($escaping) { + $regex .= '\\\\'; + $escaping = false; + } else { + $escaping = true; + } + + continue; + } else { + $regex .= $char; + } + $escaping = false; + } + + return '#^'.$regex.'$#'; +}