Skip to content

Commit

Permalink
feat: support case insensitive matching
Browse files Browse the repository at this point in the history
  • Loading branch information
haltcase committed Jul 21, 2018
1 parent 0a98bf6 commit fe17bdd
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 45 deletions.
123 changes: 79 additions & 44 deletions src/glob.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ``<dir>/**/*``
## ``GlobOption.Hidden`` yield hidden files or directories
## ``GlobOption.Directories`` yield directories
Expand All @@ -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.
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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
Expand All @@ -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..<pattern.len])

func glob* (pattern: string, isDos = isDosDefault): Glob =
func glob* (pattern: string, isDos = isDosDefault, ignoreCase = isDosDefault): Glob =
## Constructs a new `Glob <#Glob>`_ 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,
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion src/glob/regexer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
10 changes: 10 additions & 0 deletions tests.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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", @[
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit fe17bdd

Please sign in to comment.