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 @@ -398,6 +398,7 @@ 'phutil_escape_uri' => 'markup/render.php', 'phutil_escape_uri_path_component' => 'markup/render.php', 'phutil_exit' => 'utils/utils.php', + 'phutil_fnmatch' => 'utils/utils.php', 'phutil_format_bytes' => 'utils/viewutils.php', 'phutil_format_relative_time' => 'utils/viewutils.php', 'phutil_format_relative_time_detailed' => 'utils/viewutils.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,65 @@ phutil_var_export(new PhutilTestPhobject())); } + public function testFnmatch() { + $cases = array( + '' => array( + array(''), + array('.', '/'), + ), + '*' => array( + array('file'), + array('dir/', '/dir'), + ), + '**' => array( + array('file', 'dir/', '/dir', 'dir/subdir/file'), + array(), + ), + '**/file' => array( + array('file', 'dir/file', 'dir/subdir/file', 'dir/subdir/subdir/file'), + array('file/', 'file/dir'), + ), + 'file.*' => array( + array('file.php', 'file.a', 'file.'), + array('files.php', 'file.php/blah'), + ), + 'fo?' => array( + array('foo', 'fot'), + array('fooo', 'ffoo', 'fo/', 'foo/'), + ), + 'fo{o,t}' => array( + array('foo', 'fot'), + array('fob', 'fo/', 'foo/'), + ), + 'fo{o,\\,}' => array( + array('foo', 'fo,'), + array('foo/', 'fo,/'), + ), + 'fo{o,\\\\}' => array( + array('foo', 'fo\\'), + array('foo/', 'fo\\/'), + ), + '/foo' => array( + array('/foo'), + array('foo', '/foo/'), + ), + ); + + foreach ($cases as $input => $expect) { + list($matches, $no_matches) = $expect; + + foreach ($matches as $match) { + $this->assertTrue( + phutil_fnmatch($input, $match), + pht('Expecting "%s" to match "%s".', $input, $match)); + } + + foreach ($no_matches as $no_match) { + $this->assertFalse( + phutil_fnmatch($input, $no_match), + pht('Expecting "%s" not to match "%s".', $input, $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,68 @@ // Let PHP handle everything else. return var_export($var, true); } + + +/** + * An improved version of `fnmatch`. + * + * @param string A glob pattern. + * @param string A path. + * @return bool + */ +function phutil_fnmatch($glob, $path) { + // Modify the glob to allow `**/` to match files in the root directory. + $glob = preg_replace('@(?:(?