diff --git a/src/glob.nim b/src/glob.nim index df9925d..e3411b7 100644 --- a/src/glob.nim +++ b/src/glob.nim @@ -140,7 +140,8 @@ supported yet but will potentially be added in the future. This includes: import future import os -from strutils import contains, endsWith, startsWith +import strutils +from sequtils import toSeq import regex @@ -181,6 +182,7 @@ type ## flag meaning ## ============================ =========================================================== ## ``GlobOption.Absolute`` yield paths as absolute rather than relative to root + ## ``GlobOption.IgnoreCase`` matching will ignore case differences ## ``GlobOption.NoExpandDirs`` if pattern is a directory don't treat it as ``/**/*`` ## ``GlobOption.Hidden`` yield hidden files or directories ## ``GlobOption.Directories`` yield directories @@ -189,8 +191,8 @@ type ## ``GlobOption.FileLinks`` yield links to files ## ``GlobOption.FollowLinks`` recurse into directories through links ## ============================ =========================================================== - Absolute, NoExpandDirs, FollowLinks, ## iterator behavior - Hidden, Files, Directories, FileLinks, DirLinks ## to yield or not to yield + Absolute, IgnoreCase, NoExpandDirs, FollowLinks, ## iterator behavior + Hidden, Files, Directories, FileLinks, DirLinks ## to yield or not to yield GlobOptions* = set[GlobOption] ## The ``set`` type containing flags for controlling glob behavior. @@ -225,6 +227,10 @@ when defined Nimdoc: ## .. code-block:: nim ## const optsNoFiles = defaultGlobOptions - {Files} ## const optsHiddenNoLinks = defaultGlobOptions + {Hidden} - {FileLinks, DirLinks} + ## + ## On Windows systems, this also includes ``GlobOption.IgnoreCase``. +elif defined windows: + const defaultGlobOptions* = {Files, FileLinks, DirLinks, IgnoreCase} else: const defaultGlobOptions* = {Files, FileLinks, DirLinks} @@ -264,9 +270,9 @@ func expandGlob (pattern: string): string = elif pattern.existsDir: pattern & "/**" else: pattern -func globToRegex* (pattern: string, isDos = isDosDefault): Regex = +func globToRegex* (pattern: string, isDos = isDosDefault, ignoreCase = isDosDefault): Regex = ## Converts a string glob pattern to a regex pattern. - globToRegexString(pattern, isDos).toPattern + globToRegexString(pattern, isDos, ignoreCase).toPattern func splitPattern* (pattern: string): PatternStems = ## Splits the given pattern into two parts: the ``base`` which is the part @@ -291,9 +297,9 @@ func splitPattern* (pattern: string): PatternStems = let start = if head.len == 0: head.len else: head.len + 1 result = (head, pattern[start..`_ object from the given ``pattern``. - let rgx = globToRegexString(pattern, isDos) + let rgx = globToRegexString(pattern, isDos, ignoreCase) let (base, magic) = pattern.splitPattern result = Glob( pattern: pattern, @@ -317,16 +323,46 @@ func matches* (input: string, glob: Glob): bool = input.contains(glob.regex) -func matches* (input, pattern: string; isDos = isDosDefault): bool = - ## Constructs a `Glob <#Glob>`_ object from the given ``pattern`` and returns - ## ``true`` if ``input`` is a match. Shortcut for ``matches(input, glob(pattern, isDos))``. +func matches* (input, pattern: string; isDos = isDosDefault, ignoreCase = isDosDefault): bool = + ## Constructs a `Glob <#Glob>`_ object from the given ``pattern`` and returns ``true`` + ## if ``input`` is a match. Shortcut for ``matches(input, glob(pattern, isDos, ignoreCase))``. runnableExamples: when defined posix: doAssert "src/dir/foo.nim".matches("src/**/*.nim") elif defined windows: doAssert r"src\dir\foo.nim".matches("src/**/*.nim") - input.contains(globToRegex(pattern, isDos)) + input.contains(globToRegex(pattern, isDos, ignoreCase)) + +func makeCaseInsensitive (pattern: string): string = + result = "" + for c in pattern: + let isLetter = c in Letters + if isLetter: + result.add '[' + result.add c.toLowerAscii + result.add c.toUpperAscii + result.add ']' + else: + result.add c + +iterator initStack ( + pattern: string, + kinds = {pcFile, pcLinkToFile, pcDir, pcLinkToDir}, + ignoreCase = false +): tuple[kind: PathComponent, path: string] = + template push (path: string) = + var kind: PathComponent + if path.pathType(kind): yield (kind, path) + + when FileSystemCaseSensitive: + if ignoreCase: + for path in walkPattern(pattern.makeCaseInsensitive): + push path + else: + push pattern + else: + push pattern iterator walkGlobKinds* ( pattern: string | Glob, @@ -347,62 +383,61 @@ iterator walkGlobKinds* ( for path, kind in walkGlobKinds("src/**/*", options = options): doAssert kind notin {pcLinkToFile, pcLinkToDir} - var - dir = if root == "": getCurrentDir() else: root - matchPattern = when pattern is Glob: pattern.pattern else: pattern - proceed = matchPattern.hasMagic + let internalRoot = if root == "": getCurrentDir() else: root + var matchPattern = when pattern is Glob: pattern.pattern else: pattern + var proceed = matchPattern.hasMagic template push (path: string, kind: PathComponent, dir = "") = if filterYield.isNil or filterYield(path, kind): yield ( unixToNativePath( - if Absolute in options or dir == "": path + if Absolute in options or dir == "": maybeJoin(dir, path) else: path.toRelative(dir) ), kind ) if not proceed: - var kind: PathComponent - if matchPattern.pathType(kind): - if Hidden in options or not matchPattern.isHidden: - case kind - of pcDir, pcLinkToDir: - if Directories in options and (kind == pcDir or DirLinks in options): - push(matchPattern, kind, dir) - if NoExpandDirs notin options: - proceed = true - matchPattern &= "/**" - of pcFile: - if Files in options: push(matchPattern, kind, dir) - of pcLinkToFile: - if FileLinks in options: push(matchPattern, kind, dir) - - var base: string + for kind, path in initStack(matchPattern, ignoreCase = IgnoreCase in options): + if Hidden notin options and path.isHidden: continue + + case kind + of pcDir, pcLinkToDir: + if Directories in options and (kind == pcDir or DirLinks in options): + push(path, kind, internalRoot) + if NoExpandDirs notin options: + proceed = true + matchPattern &= "/**" + of pcFile: + if Files in options: push(path, kind, internalRoot) + of pcLinkToFile: + if FileLinks in options: push(path, kind, internalRoot) + + var dir: string when pattern is Glob: - dir = maybeJoin(dir, pattern.base) - base = pattern.base + dir = maybeJoin(internalRoot, pattern.base) matchPattern = pattern.magic.expandGlob else: - (base, matchPattern) = splitPattern(matchPattern) - dir = maybeJoin(dir, base) + let stems = splitPattern(matchPattern) + dir = maybeJoin(internalRoot, stems.base) + matchPattern = stems.magic if proceed: - let matcher = matchPattern.glob - let isRec = matchPattern.contains("**") + let matcher = matchPattern.globToRegex(ignoreCase = IgnoreCase in options) + let isRec = "**" in matchPattern - var stack = @[dir] + var stack = toSeq(initStack(dir, {pcDir, pcLinkToDir}, IgnoreCase in options)) var last = dir while stack.len > 0: - let subdir = stack.pop + let (_, subdir) = stack.pop for kind, path in walkDir(subdir): if Hidden notin options and path.isHidden: continue let rel = path.toRelative(dir) - isMatch = rel.matches(matcher) + isMatch = matcher in rel resultPath = unixToNativePath( - if Absolute in options: path else: base / rel + if Absolute in options: path else: path.toRelative(internalRoot) ) case kind @@ -418,13 +453,13 @@ iterator walkGlobKinds* ( last = subdir if isRec and (filterDescend.isNil or filterDescend(resultPath)): - stack.add(path) + stack.add((kind, path)) of pcDir: if Directories in options and isMatch: push(resultPath, kind) if isRec and (filterDescend.isNil or filterDescend(resultPath)): - stack.add(path) + stack.add((kind, path)) of pcLinkToFile: if FileLinks in options and isMatch: push(resultPath, kind) diff --git a/src/glob/regexer.nim b/src/glob/regexer.nim index 4e7bc1f..5769d50 100644 --- a/src/glob/regexer.nim +++ b/src/glob/regexer.nim @@ -43,7 +43,11 @@ template fail (message, pattern: string, index: int) = let errLines = 2.spaces & pattern & "\p" & (2 + index).spaces & "^" & "\p\p" raise newException(GlobSyntaxError, message & "\p\p" & errLines) -proc globToRegexString* (pattern: string, isDos = isDosDefault): string = +proc globToRegexString* ( + pattern: string, + isDos = isDosDefault, + ignoreCase = isDosDefault +): string = ## Parses the given ``pattern`` glob string and returns a regex string. ## Syntactic errors will cause a ``GlobSyntaxError`` to be raised. var @@ -67,6 +71,8 @@ proc globToRegexString* (pattern: string, isDos = isDosDefault): string = template isNext (cmp: char): bool = peek(i + 1) == cmp + if ignoreCase: add "(?i)" + while i < pattern.len - 1: inc i var c = pattern[i] diff --git a/tests.nim b/tests.nim index 892c67e..b80cdf6 100644 --- a/tests.nim +++ b/tests.nim @@ -50,6 +50,7 @@ suite "procs accept both string & glob": test "matches": check "src/dir/foo.nim".matches("src/**/*.nim", false) check "src/dir/foo.nim".matches(glob("src/**/*.nim", false)) + check "SRC/FOO.NIM".matches("src/*.nim", ignoreCase = true) test "walkGlob, walkGlobKinds": let cleanup = createStructure("temp", @[ @@ -337,6 +338,15 @@ suite "pattern walking / listing": "temp" / "shallow.nim" ]) + test "`IgnoreCase` enables case insensitive matching": + let o = defaultGlobOptions + {IgnoreCase} + check seqsEqual(toSeq(walkGlob("TEMP/**", options = o)), @[ + "temp" / "deep" / "dir" / "file.nim", + "temp" / "not_as" / "deep.jpg", + "temp" / "not_as" / "deep.nim", + "temp" / "shallow.nim" + ]) + test "`NoExpandDirs` disables the default directory expansion behavior": check seqsEqual(toSeq(walkGlob("temp")), @[ "temp" / "deep" / "dir" / "file.nim",