Commit 49e2c493 by John Was

advanced file excluding/including ported from git

parent ab5d7bd5
...@@ -22,6 +22,11 @@ use Yii; ...@@ -22,6 +22,11 @@ use Yii;
*/ */
class BaseFileHelper class BaseFileHelper
{ {
const EXC_FLAG_NODIR = 1;
const EXC_FLAG_ENDSWITH = 4;
const EXC_FLAG_MUSTBEDIR = 8;
const EXC_FLAG_NEGATIVE = 16;
/** /**
* Normalizes a file/directory path. * Normalizes a file/directory path.
* After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`, * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`,
...@@ -249,6 +254,16 @@ class BaseFileHelper ...@@ -249,6 +254,16 @@ class BaseFileHelper
*/ */
public static function findFiles($dir, $options = []) public static function findFiles($dir, $options = [])
{ {
if (!isset($options['basePath'])) {
$options['basePath'] = realpath($dir);
// this should also be done only once
if (isset($options['except'])) {
$options['except'] = array_map(array(__CLASS__, 'parseExcludePattern'), $options['except']);
}
if (isset($options['only'])) {
$options['only'] = array_map(array(__CLASS__, 'parseExcludePattern'), $options['only']);
}
}
$list = []; $list = [];
$handle = opendir($dir); $handle = opendir($dir);
while (($file = readdir($handle)) !== false) { while (($file = readdir($handle)) !== false) {
...@@ -289,24 +304,20 @@ class BaseFileHelper ...@@ -289,24 +304,20 @@ class BaseFileHelper
} }
$path = str_replace('\\', '/', $path); $path = str_replace('\\', '/', $path);
if ($isDir = is_dir($path)) { /*if ($isDir = is_dir($path)) {
$path .= '/'; $path .= '/';
} }*/
$n = StringHelper::byteLength($path);
if (!empty($options['except'])) { if (!empty($options['except'])) {
foreach ($options['except'] as $name) { if (($except=self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null) {
if (StringHelper::byteSubstr($path, -StringHelper::byteLength($name), $n) === $name) { return $except['flags'] & self::EXC_FLAG_NEGATIVE;
return false;
}
} }
} }
if (!$isDir && !empty($options['only'])) { if (!is_dir($path) && !empty($options['only'])) {
foreach ($options['only'] as $name) { if (($except=self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only'])) !== null) {
if (StringHelper::byteSubstr($path, -StringHelper::byteLength($name), $n) === $name) { // don't check EXC_FLAG_NEGATIVE since those entries are not prefixed with !
return true; return true;
}
} }
return false; return false;
} }
...@@ -338,4 +349,136 @@ class BaseFileHelper ...@@ -338,4 +349,136 @@ class BaseFileHelper
chmod($path, $mode); chmod($path, $mode);
return $result; return $result;
} }
public static function matchBasename($baseName, $pattern, $hasWildcard, $flags)
{
if ($hasWildcard === false) {
if ($pattern === $baseName) {
return true;
}
} else if ($flags & self::EXC_FLAG_ENDSWITH) {
/* "*literal" matching against "fooliteral" */
$n = StringHelper::byteLength($pattern);
if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) {
return true;
}
}
return fnmatch($pattern, $baseName, 0);
}
public static function matchPathname($path, $basePath, $pattern, $prefix, $flags)
{
// match with FNM_PATHNAME; the pattern has base implicitly in front of it.
if (isset($pattern[0]) && $pattern[0] == '/') {
$pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
if ($prefix !== 0) {
$prefix--;
}
}
$namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1);
$name = StringHelper::byteSubstr($path, -$namelen, $namelen);
if ($prefix !== 0) {
if ($prefix === false) {
$prefix = StringHelper::byteLength($pattern);
}
// if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
if ($prefix > $namelen) {
return false;
}
if (strncmp($pattern, $name, $prefix)) {
return false;
}
$pattern = StringHelper::byteSubstr($pattern, $prefix, StringHelper::byteLength($pattern));
$name = StringHelper::byteSubstr($name, $prefix, $namelen);
// If the whole pattern did not have a wildcard, then our prefix match is all we need; we do not need to call fnmatch at all.
if (empty($pattern) && empty($name)) {
return true;
}
}
return fnmatch($pattern, $name, FNM_PATHNAME);
}
/**
* Scan the given exclude list in reverse to see whether pathname
* should be ignored. The first match (i.e. the last on the list), if
* any, determines the fate. Returns the element which
* matched, or null for undecided.
*
* Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources.
*
* @param string $basePath
* @param string $path
* @param array $excludes list of patterns to match $path against
* @return string null or one of $excludes item as an array with keys: 'pattern', 'flags'
*/
public static function lastExcludeMatchingFromList($basePath, $path, $excludes)
{
foreach(array_reverse($excludes) as $exclude) {
if (is_string($exclude)) {
$exclude = self::parseExcludePattern($exclude);
}
if ($exclude['flags'] & self::EXC_FLAG_MUSTBEDIR && !is_dir($path)) {
continue;
}
if ($exclude['flags'] & self::EXC_FLAG_NODIR) {
if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['hasWildcard'], $exclude['flags'])) {
return $exclude;
}
continue;
}
if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['hasWildcard'], $exclude['flags'])) {
return $exclude;
}
}
return null;
}
public static function parseExcludePattern($pattern)
{
if (!is_string($pattern)) {
throw new \yii\base\Exception('Exclude/include pattern must be a string.');
}
$result = array(
'pattern' => $pattern,
'flags' => 0,
'hasWildcard' => false,
);
if (!isset($pattern[0]))
return $result;
if ($pattern[0] == '!') {
$result['flags'] |= self::EXC_FLAG_NEGATIVE;
$pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
}
$len = StringHelper::byteLength($pattern);
if ($len && StringHelper::byteSubstr($pattern, -1, 1) == '/') {
$pattern = StringHelper::byteSubstr($pattern, 0, -1);
$len--;
$result['flags'] |= self::EXC_FLAG_MUSTBEDIR;
}
if (strpos($pattern, '/') === false)
$result['flags'] |= self::EXC_FLAG_NODIR;
$result['hasWildcard'] = self::firstWildcardInPattern($pattern);
if ($pattern[0] == '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false)
$result['flags'] |= self::EXC_FLAG_ENDSWITH;
$result['pattern'] = $pattern;
return $result;
}
public static function firstWildcardInPattern($pattern)
{
$wildcards = array('*','?','[','\\');
$wildcardSearch = function($r, $c) use ($pattern) {
$p = strpos($pattern, $c);
return $r===false ? $p : ($p===false ? $r : min($r, $p));
};
return array_reduce($wildcards, $wildcardSearch, false);
}
} }
...@@ -22,17 +22,14 @@ return [ ...@@ -22,17 +22,14 @@ return [
// boolean, whether to remove messages that no longer appear in the source code. // boolean, whether to remove messages that no longer appear in the source code.
// Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks.
'removeUnused' => false, 'removeUnused' => false,
// array, list of patterns that specify which files/directories should be processed. // array, list of patterns that specify which files/directories should NOT be processed.
// If empty or not set, all files/directories will be processed. // If empty or not set, all files/directories will be processed.
// A path matches a pattern if it contains the pattern string at its end. For example, // A path matches a pattern if it contains the pattern string at its end. For example,
// '/a/b' will match all files and directories ending with '/a/b'; // '/a/b' will match all files and directories ending with '/a/b';
// and the '.svn' will match all files and directories whose name ends with '.svn'. // the '*.svn' will match all files and directories whose name ends with '.svn'.
// and the '.svn' will match all files and directories named exactly '.svn'.
// Note, the '/' characters in a pattern matches both '/' and '\'. // Note, the '/' characters in a pattern matches both '/' and '\'.
// If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
'only' => ['.php'],
// array, list of patterns that specify which files/directories should NOT be processed.
// If empty or not set, all files/directories will be processed.
// Please refer to "only" for details about the patterns.
'except' => [ 'except' => [
'.svn', '.svn',
'.git', '.git',
...@@ -42,6 +39,10 @@ return [ ...@@ -42,6 +39,10 @@ return [
'.hgkeep', '.hgkeep',
'/messages', '/messages',
], ],
// array, list of patterns that specify which files should be processed.
// If empty or not set, all files will be processed.
// Please refer to "except" for details about the patterns.
'only' => ['*.php'],
// Generated file format. Can be either "php" or "po". // Generated file format. Can be either "php" or "po".
'format' => 'php', 'format' => 'php',
]; ];
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment