mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
667 lines
22 KiB
Nim
667 lines
22 KiB
Nim
#
|
|
#
|
|
# Nim Grep Utility
|
|
# (c) Copyright 2012 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
import
|
|
os, strutils, parseopt, pegs, re, terminal
|
|
|
|
const
|
|
Version = "1.5"
|
|
Usage = "nimgrep - Nim Grep Utility Version " & Version & """
|
|
|
|
(c) 2012 Andreas Rumpf
|
|
Usage:
|
|
nimgrep [options] [pattern] [replacement] (file/directory)*
|
|
Options:
|
|
--find, -f find the pattern (default)
|
|
--replace, -! replace the pattern
|
|
--peg pattern is a peg
|
|
--re pattern is a regular expression (default)
|
|
--rex, -x use the "extended" syntax for the regular expression
|
|
so that whitespace is not significant
|
|
--recursive, -r process directories recursively
|
|
--follow follow all symlinks when processing recursively
|
|
--confirm confirm each occurrence/replacement; there is a chance
|
|
to abort any time without touching the file
|
|
--stdin read pattern from stdin (to avoid the shell's confusing
|
|
quoting rules)
|
|
--word, -w the match should have word boundaries (buggy for pegs!)
|
|
--ignoreCase, -i be case insensitive
|
|
--ignoreStyle, -y be style insensitive
|
|
--ext:EX1|EX2|... only search the files with the given extension(s),
|
|
empty one ("--ext") means files with missing extension
|
|
--noExt:EX1|... exclude files having given extension(s), use empty one to
|
|
skip files with no extension (like some binary files are)
|
|
--includeFile:PAT include only files whose names match the given regex PAT
|
|
--excludeFile:PAT skip files whose names match the given regex pattern PAT
|
|
--excludeDir:PAT skip directories whose names match the given regex PAT
|
|
--nocolor output will be given without any colours
|
|
--color[:always] force color even if output is redirected
|
|
--colorTheme:THEME select color THEME from 'simple' (default),
|
|
'bnw' (black and white) ,'ack', or 'gnu' (GNU grep)
|
|
--afterContext:N,
|
|
-a:N print N lines of trailing context after every match
|
|
--beforeContext:N,
|
|
-b:N print N lines of leading context before every match
|
|
--context:N, -c:N print N lines of leading context before every match and
|
|
N lines of trailing context after it
|
|
--group, -g group matches by file
|
|
--newLine, -l display every matching line starting from a new line
|
|
--verbose be verbose: list every processed file
|
|
--filenames find the pattern in the filenames, not in the contents
|
|
of the file
|
|
--help, -h shows this help
|
|
--version, -v shows the version
|
|
"""
|
|
|
|
type
|
|
TOption = enum
|
|
optFind, optReplace, optPeg, optRegex, optRecursive, optConfirm, optStdin,
|
|
optWord, optIgnoreCase, optIgnoreStyle, optVerbose, optFilenames,
|
|
optRex, optFollow
|
|
TOptions = set[TOption]
|
|
TConfirmEnum = enum
|
|
ceAbort, ceYes, ceAll, ceNo, ceNone
|
|
Pattern = Regex | Peg
|
|
|
|
using pattern: Pattern
|
|
|
|
var
|
|
filenames: seq[string] = @[]
|
|
pattern = ""
|
|
replacement = ""
|
|
extensions: seq[string] = @[]
|
|
options: TOptions = {optRegex}
|
|
skipExtensions: seq[string] = @[]
|
|
excludeFile: seq[Regex]
|
|
includeFile: seq[Regex]
|
|
excludeDir: seq[Regex]
|
|
useWriteStyled = true
|
|
oneline = true
|
|
linesBefore = 0
|
|
linesAfter = 0
|
|
linesContext = 0
|
|
colorTheme = "simple"
|
|
newLine = false
|
|
|
|
proc ask(msg: string): string =
|
|
stdout.write(msg)
|
|
stdout.flushFile()
|
|
result = stdin.readLine()
|
|
|
|
proc confirm: TConfirmEnum =
|
|
while true:
|
|
case normalize(ask(" [a]bort; [y]es, a[l]l, [n]o, non[e]: "))
|
|
of "a", "abort": return ceAbort
|
|
of "y", "yes": return ceYes
|
|
of "l", "all": return ceAll
|
|
of "n", "no": return ceNo
|
|
of "e", "none": return ceNone
|
|
else: discard
|
|
|
|
func countLineBreaks(s: string, first, last: int): int =
|
|
# count line breaks (unlike strutils.countLines starts count from 0)
|
|
var i = first
|
|
while i <= last:
|
|
if s[i] == '\c':
|
|
inc result
|
|
if i < last and s[i+1] == '\l': inc(i)
|
|
elif s[i] == '\l':
|
|
inc result
|
|
inc i
|
|
|
|
func beforePattern(s: string, pos: int, nLines = 1): int =
|
|
var linesLeft = nLines
|
|
result = min(pos, s.len-1)
|
|
while true:
|
|
while result >= 0 and s[result] notin {'\c', '\l'}: dec(result)
|
|
if result == -1: break
|
|
if s[result] == '\l':
|
|
dec(linesLeft)
|
|
if linesLeft == 0: break
|
|
dec(result)
|
|
if result >= 0 and s[result] == '\c': dec(result)
|
|
else: # '\c'
|
|
dec(linesLeft)
|
|
if linesLeft == 0: break
|
|
dec(result)
|
|
inc(result)
|
|
|
|
proc afterPattern(s: string, pos: int, nLines = 1): int =
|
|
result = max(0, pos)
|
|
var linesScanned = 0
|
|
while true:
|
|
while result < s.len and s[result] notin {'\c', '\l'}: inc(result)
|
|
inc(linesScanned)
|
|
if linesScanned == nLines: break
|
|
if result < s.len:
|
|
if s[result] == '\l':
|
|
inc(result)
|
|
elif s[result] == '\c':
|
|
inc(result)
|
|
if result < s.len and s[result] == '\l': inc(result)
|
|
else: break
|
|
dec(result)
|
|
|
|
template whenColors(body: untyped) =
|
|
if useWriteStyled:
|
|
body
|
|
else:
|
|
stdout.write(s)
|
|
|
|
proc printFile(s: string) =
|
|
whenColors:
|
|
case colorTheme
|
|
of "simple": stdout.write(s)
|
|
of "bnw": stdout.styledWrite(styleUnderscore, s)
|
|
of "ack": stdout.styledWrite(fgGreen, s)
|
|
of "gnu": stdout.styledWrite(fgMagenta, s)
|
|
|
|
proc printBlockFile(s: string) =
|
|
whenColors:
|
|
case colorTheme
|
|
of "simple": stdout.styledWrite(styleBright, s)
|
|
of "bnw": stdout.styledWrite(styleUnderscore, s)
|
|
of "ack": stdout.styledWrite(styleUnderscore, fgGreen, s)
|
|
of "gnu": stdout.styledWrite(styleUnderscore, fgMagenta, s)
|
|
|
|
proc printError(s: string) =
|
|
whenColors:
|
|
case colorTheme
|
|
of "simple", "bnw": stdout.styledWriteLine(styleBright, s)
|
|
of "ack", "gnu": stdout.styledWriteLine(styleReverse, fgRed, bgDefault, s)
|
|
stdout.flushFile()
|
|
|
|
const alignment = 6
|
|
|
|
proc printLineN(s: string, isMatch: bool) =
|
|
whenColors:
|
|
case colorTheme
|
|
of "simple": stdout.write(s)
|
|
of "bnw":
|
|
if isMatch: stdout.styledWrite(styleBright, s)
|
|
else: stdout.styledWrite(s)
|
|
of "ack":
|
|
if isMatch: stdout.styledWrite(fgYellow, s)
|
|
else: stdout.styledWrite(fgGreen, s)
|
|
of "gnu":
|
|
if isMatch: stdout.styledWrite(fgGreen, s)
|
|
else: stdout.styledWrite(fgCyan, s)
|
|
|
|
proc printBlockLineN(s: string) =
|
|
whenColors:
|
|
case colorTheme
|
|
of "simple": stdout.styledWrite(styleBright, s)
|
|
of "bnw": stdout.styledWrite(styleUnderscore, styleBright, s)
|
|
of "ack": stdout.styledWrite(styleUnderscore, fgYellow, s)
|
|
of "gnu": stdout.styledWrite(styleUnderscore, fgGreen, s)
|
|
|
|
type
|
|
SearchInfo = tuple[buf: string, filename: string]
|
|
MatchInfo = tuple[first: int, last: int;
|
|
lineBeg: int, lineEnd: int, match: string]
|
|
|
|
proc writeColored(s: string) =
|
|
whenColors:
|
|
case colorTheme
|
|
of "simple": terminal.writeStyled(s, {styleUnderscore, styleBright})
|
|
of "bnw": stdout.styledWrite(styleReverse, s)
|
|
# Try styleReverse & bgDefault as a work-around against nasty feature
|
|
# "Background color erase" (sticky background after line wraps):
|
|
of "ack": stdout.styledWrite(styleReverse, fgYellow, bgDefault, s)
|
|
of "gnu": stdout.styledWrite(fgRed, s)
|
|
|
|
proc writeArrow(s: string) =
|
|
whenColors:
|
|
stdout.styledWrite(styleReverse, s)
|
|
|
|
proc blockHeader(filename: string, line: int|string, replMode=false) =
|
|
if replMode:
|
|
writeArrow(" ->\n")
|
|
elif newLine:
|
|
if oneline:
|
|
printBlockFile(filename)
|
|
printBlockLineN(":" & $line & ":")
|
|
else:
|
|
printBlockLineN($line.`$`.align(alignment) & ":")
|
|
stdout.write("\n")
|
|
|
|
proc lineHeader(filename: string, line: int|string, isMatch: bool) =
|
|
let lineSym =
|
|
if isMatch: $line & ":"
|
|
else: $line & " "
|
|
if not newLine:
|
|
if oneline:
|
|
printFile(filename)
|
|
printLineN(":" & lineSym, isMatch)
|
|
else:
|
|
printLineN(lineSym.align(alignment+1), isMatch)
|
|
stdout.write(" ")
|
|
|
|
proc printMatch(fileName: string, mi: MatchInfo) =
|
|
let lines = mi.match.splitLines()
|
|
for i, l in lines:
|
|
if i > 0:
|
|
lineHeader(filename, mi.lineBeg + i, isMatch = true)
|
|
writeColored(l)
|
|
if i < lines.len - 1:
|
|
stdout.write("\n")
|
|
|
|
proc printLinesBefore(si: SearchInfo, curMi: MatchInfo, nLines: int,
|
|
replMode=false) =
|
|
# start block: print 'linesBefore' lines before current match `curMi`
|
|
let first = beforePattern(si.buf, curMi.first-1, nLines)
|
|
let lines = splitLines(substr(si.buf, first, curMi.first-1))
|
|
let startLine = curMi.lineBeg - lines.len + 1
|
|
blockHeader(si.filename, curMi.lineBeg, replMode=replMode)
|
|
for i, l in lines:
|
|
lineHeader(si.filename, startLine + i, isMatch = (i == lines.len - 1))
|
|
stdout.write(l)
|
|
if i < lines.len - 1:
|
|
stdout.write("\n")
|
|
|
|
proc printLinesAfter(si: SearchInfo, mi: MatchInfo, nLines: int) =
|
|
# finish block: print 'linesAfter' lines after match `mi`
|
|
let s = si.buf
|
|
let last = afterPattern(s, mi.last+1, nLines)
|
|
let lines = splitLines(substr(s, mi.last+1, last))
|
|
if lines.len == 0: # EOF
|
|
stdout.write("\n")
|
|
else:
|
|
stdout.write(lines[0]) # complete the line after match itself
|
|
stdout.write("\n")
|
|
let skipLine = # workaround posix line ending at the end of file
|
|
if last == s.len-1 and s.len >= 2 and s[^1] == '\l' and s[^2] != '\c': 1
|
|
else: 0
|
|
for i in 1 ..< lines.len - skipLine:
|
|
lineHeader(si.filename, mi.lineEnd + i, isMatch = false)
|
|
stdout.write(lines[i])
|
|
stdout.write("\n")
|
|
if linesAfter + linesBefore >= 2 and not newLine: stdout.write("\n")
|
|
|
|
proc printBetweenMatches(si: SearchInfo, prevMi: MatchInfo, curMi: MatchInfo) =
|
|
# continue block: print between `prevMi` and `curMi`
|
|
let lines = si.buf.substr(prevMi.last+1, curMi.first-1).splitLines()
|
|
stdout.write(lines[0]) # finish the line of previous Match
|
|
if lines.len > 1:
|
|
stdout.write("\n")
|
|
for i in 1 ..< lines.len:
|
|
lineHeader(si.filename, prevMi.lineEnd + i,
|
|
isMatch = (i == lines.len - 1))
|
|
stdout.write(lines[i])
|
|
if i < lines.len - 1:
|
|
stdout.write("\n")
|
|
|
|
proc printContextBetween(si: SearchInfo, prevMi, curMi: MatchInfo) =
|
|
# print context after previous match prevMi and before current match curMi
|
|
let nLinesBetween = curMi.lineBeg - prevMi.lineEnd
|
|
if nLinesBetween <= linesAfter + linesBefore + 1: # print as 1 block
|
|
printBetweenMatches(si, prevMi, curMi)
|
|
else: # finalize previous block and then print next block
|
|
printLinesAfter(si, prevMi, 1+linesAfter)
|
|
printLinesBefore(si, curMi, linesBefore+1)
|
|
|
|
proc printReplacement(si: SearchInfo, mi: MatchInfo, repl: string,
|
|
showRepl: bool, curPos: int,
|
|
newBuf: string, curLine: int) =
|
|
printLinesBefore(si, mi, linesBefore+1)
|
|
printMatch(si.fileName, mi)
|
|
printLinesAfter(si, mi, 1+linesAfter)
|
|
stdout.flushFile()
|
|
if showRepl:
|
|
let newSi: SearchInfo = (buf: newBuf, filename: si.filename)
|
|
let miForNewBuf: MatchInfo =
|
|
(first: newBuf.len, last: newBuf.len,
|
|
lineBeg: curLine, lineEnd: curLine, match: "")
|
|
printLinesBefore(newSi, miForNewBuf, linesBefore+1, replMode=true)
|
|
|
|
let replLines = countLineBreaks(repl, 0, repl.len-1)
|
|
let miFixLines: MatchInfo =
|
|
(first: mi.first, last: mi.last,
|
|
lineBeg: curLine, lineEnd: curLine + replLines, match: repl)
|
|
printMatch(si.fileName, miFixLines)
|
|
printLinesAfter(si, miFixLines, 1+linesAfter)
|
|
stdout.flushFile()
|
|
|
|
proc doReplace(si: SearchInfo, mi: MatchInfo, i: int, r: string;
|
|
newBuf: var string, curLine: var int, reallyReplace: var bool) =
|
|
newBuf.add(si.buf.substr(i, mi.first-1))
|
|
inc(curLine, countLineBreaks(si.buf, i, mi.first-1))
|
|
if optConfirm in options:
|
|
printReplacement(si, mi, r, showRepl=true, i, newBuf, curLine)
|
|
case confirm()
|
|
of ceAbort: quit(0)
|
|
of ceYes: reallyReplace = true
|
|
of ceAll:
|
|
reallyReplace = true
|
|
options.excl(optConfirm)
|
|
of ceNo:
|
|
reallyReplace = false
|
|
of ceNone:
|
|
reallyReplace = false
|
|
options.excl(optConfirm)
|
|
else:
|
|
printReplacement(si, mi, r, showRepl=reallyReplace, i, newBuf, curLine)
|
|
if reallyReplace:
|
|
newBuf.add(r)
|
|
inc(curLine, countLineBreaks(r, 0, r.len-1))
|
|
else:
|
|
newBuf.add(mi.match)
|
|
inc(curLine, countLineBreaks(mi.match, 0, mi.match.len-1))
|
|
|
|
proc processFile(pattern; filename: string; counter: var int, errors: var int) =
|
|
var filenameShown = false
|
|
template beforeHighlight =
|
|
if not filenameShown and optVerbose notin options and not oneline:
|
|
printBlockFile(filename)
|
|
stdout.write("\n")
|
|
stdout.flushFile()
|
|
filenameShown = true
|
|
|
|
var buffer: string
|
|
if optFilenames in options:
|
|
buffer = filename
|
|
else:
|
|
try:
|
|
buffer = system.readFile(filename)
|
|
except IOError:
|
|
printError "Error: cannot open file: " & filename
|
|
inc(errors)
|
|
return
|
|
if optVerbose in options:
|
|
printFile(filename)
|
|
stdout.write("\n")
|
|
stdout.flushFile()
|
|
var result: string
|
|
|
|
if optReplace in options:
|
|
result = newStringOfCap(buffer.len)
|
|
|
|
var lineRepl = 1
|
|
let si: SearchInfo = (buf: buffer, filename: filename)
|
|
var prevMi, curMi: MatchInfo
|
|
curMi.lineEnd = 1
|
|
var i = 0
|
|
var matches: array[0..re.MaxSubpatterns-1, string]
|
|
for j in 0..high(matches): matches[j] = ""
|
|
var reallyReplace = true
|
|
while i < buffer.len:
|
|
let t = findBounds(buffer, pattern, matches, i)
|
|
if t.first < 0 or t.last < t.first:
|
|
if optReplace notin options and prevMi.lineBeg != 0: # finalize last match
|
|
printLinesAfter(si, prevMi, 1+linesAfter)
|
|
stdout.flushFile()
|
|
break
|
|
|
|
let lineBeg = curMi.lineEnd + countLineBreaks(buffer, i, t.first-1)
|
|
curMi = (first: t.first,
|
|
last: t.last,
|
|
lineBeg: lineBeg,
|
|
lineEnd: lineBeg + countLineBreaks(buffer, t.first, t.last),
|
|
match: buffer.substr(t.first, t.last))
|
|
beforeHighlight()
|
|
inc counter
|
|
if optReplace notin options:
|
|
if prevMi.lineBeg == 0: # no previous match, so no previous block to finalize
|
|
printLinesBefore(si, curMi, linesBefore+1)
|
|
else:
|
|
printContextBetween(si, prevMi, curMi)
|
|
printMatch(si.fileName, curMi)
|
|
if t.last == buffer.len - 1:
|
|
stdout.write("\n")
|
|
stdout.flushFile()
|
|
else:
|
|
let r = replace(curMi.match, pattern, replacement % matches)
|
|
doReplace(si, curMi, i, r, result, lineRepl, reallyReplace)
|
|
|
|
i = t.last+1
|
|
prevMi = curMi
|
|
|
|
if optReplace in options:
|
|
result.add(substr(buffer, i)) # finalize new buffer after last match
|
|
var f: File
|
|
if open(f, filename, fmWrite):
|
|
f.write(result)
|
|
f.close()
|
|
else:
|
|
quit "cannot open file for overwriting: " & filename
|
|
|
|
proc hasRightFileName(path: string): bool =
|
|
let filename = path.lastPathPart
|
|
let ex = filename.splitFile.ext.substr(1) # skip leading '.'
|
|
if extensions.len != 0:
|
|
var matched = false
|
|
for x in items(extensions):
|
|
if os.cmpPaths(x, ex) == 0:
|
|
matched = true
|
|
break
|
|
if not matched: return false
|
|
for x in items(skipExtensions):
|
|
if os.cmpPaths(x, ex) == 0: return false
|
|
if includeFile.len != 0:
|
|
var matched = false
|
|
for x in items(includeFile):
|
|
if filename.match(x):
|
|
matched = true
|
|
break
|
|
if not matched: return false
|
|
for x in items(excludeFile):
|
|
if filename.match(x): return false
|
|
result = true
|
|
|
|
proc hasRightDirectory(path: string): bool =
|
|
let dirname = path.lastPathPart
|
|
for x in items(excludeDir):
|
|
if dirname.match(x): return false
|
|
result = true
|
|
|
|
proc styleInsensitive(s: string): string =
|
|
template addx =
|
|
result.add(s[i])
|
|
inc(i)
|
|
result = ""
|
|
var i = 0
|
|
var brackets = 0
|
|
while i < s.len:
|
|
case s[i]
|
|
of 'A'..'Z', 'a'..'z', '0'..'9':
|
|
addx()
|
|
if brackets == 0: result.add("_?")
|
|
of '_':
|
|
addx()
|
|
result.add('?')
|
|
of '[':
|
|
addx()
|
|
inc(brackets)
|
|
of ']':
|
|
addx()
|
|
if brackets > 0: dec(brackets)
|
|
of '?':
|
|
addx()
|
|
if s[i] == '<':
|
|
addx()
|
|
while s[i] != '>' and s[i] != '\0': addx()
|
|
of '\\':
|
|
addx()
|
|
if s[i] in strutils.Digits:
|
|
while s[i] in strutils.Digits: addx()
|
|
else:
|
|
addx()
|
|
else: addx()
|
|
|
|
proc walker(pattern; dir: string; counter: var int, errors: var int) =
|
|
if existsDir(dir):
|
|
for kind, path in walkDir(dir):
|
|
case kind
|
|
of pcFile:
|
|
if path.hasRightFileName:
|
|
processFile(pattern, path, counter, errors)
|
|
of pcLinkToFile:
|
|
if optFollow in options and path.hasRightFileName:
|
|
processFile(pattern, path, counter, errors)
|
|
of pcDir:
|
|
if optRecursive in options and path.hasRightDirectory:
|
|
walker(pattern, path, counter, errors)
|
|
of pcLinkToDir:
|
|
if optFollow in options and optRecursive in options and
|
|
path.hasRightDirectory:
|
|
walker(pattern, path, counter, errors)
|
|
elif existsFile(dir):
|
|
processFile(pattern, dir, counter, errors)
|
|
else:
|
|
printError "Error: no such file or directory: " & dir
|
|
inc(errors)
|
|
|
|
proc reportError(msg: string) =
|
|
printError "Error: " & msg
|
|
quit "Run nimgrep --help for the list of options"
|
|
|
|
proc writeHelp() =
|
|
stdout.write(Usage)
|
|
stdout.flushFile()
|
|
quit(0)
|
|
|
|
proc writeVersion() =
|
|
stdout.write(Version & "\n")
|
|
stdout.flushFile()
|
|
quit(0)
|
|
|
|
proc checkOptions(subset: TOptions, a, b: string) =
|
|
if subset <= options:
|
|
quit("cannot specify both '$#' and '$#'" % [a, b])
|
|
|
|
when defined(posix):
|
|
useWriteStyled = terminal.isatty(stdout)
|
|
# that should be before option processing to allow override of useWriteStyled
|
|
|
|
for kind, key, val in getopt():
|
|
case kind
|
|
of cmdArgument:
|
|
if options.contains(optStdin):
|
|
filenames.add(key)
|
|
elif pattern.len == 0:
|
|
pattern = key
|
|
elif options.contains(optReplace) and replacement.len == 0:
|
|
replacement = key
|
|
else:
|
|
filenames.add(key)
|
|
of cmdLongOption, cmdShortOption:
|
|
case normalize(key)
|
|
of "find", "f": incl(options, optFind)
|
|
of "replace", "!": incl(options, optReplace)
|
|
of "peg":
|
|
excl(options, optRegex)
|
|
incl(options, optPeg)
|
|
of "re":
|
|
incl(options, optRegex)
|
|
excl(options, optPeg)
|
|
of "rex", "x":
|
|
incl(options, optRex)
|
|
incl(options, optRegex)
|
|
excl(options, optPeg)
|
|
of "recursive", "r": incl(options, optRecursive)
|
|
of "follow": incl(options, optFollow)
|
|
of "confirm": incl(options, optConfirm)
|
|
of "stdin": incl(options, optStdin)
|
|
of "word", "w": incl(options, optWord)
|
|
of "ignorecase", "i": incl(options, optIgnoreCase)
|
|
of "ignorestyle", "y": incl(options, optIgnoreStyle)
|
|
of "ext": extensions.add val.split('|')
|
|
of "noext": skipExtensions.add val.split('|')
|
|
of "excludedir", "exclude-dir": excludeDir.add rex(val)
|
|
of "includefile", "include-file": includeFile.add rex(val)
|
|
of "excludefile", "exclude-file": excludeFile.add rex(val)
|
|
of "nocolor": useWriteStyled = false
|
|
of "color":
|
|
case val
|
|
of "auto": discard
|
|
of "never", "false": useWriteStyled = false
|
|
of "", "always", "true": useWriteStyled = true
|
|
else: reportError("invalid value '" & val & "' for --color")
|
|
of "colortheme":
|
|
colortheme = normalize(val)
|
|
if colortheme notin ["simple", "bnw", "ack", "gnu"]:
|
|
reportError("unknown colortheme '" & val & "'")
|
|
of "beforecontext", "before-context", "b":
|
|
try:
|
|
linesBefore = parseInt(val)
|
|
except ValueError:
|
|
reportError("option " & key & " requires an integer but '" &
|
|
val & "' was given")
|
|
of "aftercontext", "after-context", "a":
|
|
try:
|
|
linesAfter = parseInt(val)
|
|
except ValueError:
|
|
reportError("option " & key & " requires an integer but '" &
|
|
val & "' was given")
|
|
of "context", "c":
|
|
try:
|
|
linesContext = parseInt(val)
|
|
except ValueError:
|
|
reportError("option --context requires an integer but '" &
|
|
val & "' was given")
|
|
of "newline", "l": newLine = true
|
|
of "oneline": oneline = true
|
|
of "group", "g": oneline = false
|
|
of "verbose": incl(options, optVerbose)
|
|
of "filenames": incl(options, optFilenames)
|
|
of "help", "h": writeHelp()
|
|
of "version", "v": writeVersion()
|
|
else: reportError("unrecognized option '" & key & "'")
|
|
of cmdEnd: assert(false) # cannot happen
|
|
|
|
checkOptions({optFind, optReplace}, "find", "replace")
|
|
checkOptions({optPeg, optRegex}, "peg", "re")
|
|
checkOptions({optIgnoreCase, optIgnoreStyle}, "ignore_case", "ignore_style")
|
|
checkOptions({optFilenames, optReplace}, "filenames", "replace")
|
|
|
|
linesBefore = max(linesBefore, linesContext)
|
|
linesAfter = max(linesAfter, linesContext)
|
|
|
|
if optStdin in options:
|
|
pattern = ask("pattern [ENTER to exit]: ")
|
|
if pattern.len == 0: quit(0)
|
|
if optReplace in options:
|
|
replacement = ask("replacement [supports $1, $# notations]: ")
|
|
|
|
if pattern.len == 0:
|
|
reportError("empty pattern was given")
|
|
else:
|
|
var counter = 0
|
|
var errors = 0
|
|
if filenames.len == 0:
|
|
filenames.add(os.getCurrentDir())
|
|
if optRegex notin options:
|
|
if optWord in options:
|
|
pattern = r"(^ / !\letter)(" & pattern & r") !\letter"
|
|
if optIgnoreStyle in options:
|
|
pattern = "\\y " & pattern
|
|
elif optIgnoreCase in options:
|
|
pattern = "\\i " & pattern
|
|
let pegp = peg(pattern)
|
|
for f in items(filenames):
|
|
walker(pegp, f, counter, errors)
|
|
else:
|
|
var reflags = {reStudy}
|
|
if optIgnoreStyle in options:
|
|
pattern = styleInsensitive(pattern)
|
|
if optWord in options:
|
|
# see https://github.com/nim-lang/Nim/issues/13528#issuecomment-592786443
|
|
pattern = r"(^|\W)(:?" & pattern & r")($|\W)"
|
|
if {optIgnoreCase, optIgnoreStyle} * options != {}:
|
|
reflags.incl reIgnoreCase
|
|
let rep = if optRex in options: rex(pattern, reflags)
|
|
else: re(pattern, reflags)
|
|
for f in items(filenames):
|
|
walker(rep, f, counter, errors)
|
|
if errors != 0:
|
|
printError $errors & " errors"
|
|
stdout.write($counter & " matches\n")
|
|
if errors != 0:
|
|
quit(1)
|