mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
nimgrep improvements (#12779)
* fix sticky colors in styledWrite * nimgrep: context printing, colorthemes and other * add context printing (lines after and before a match) * nimgrep: add exclude/include options * nimgrep: improve error printing & symlink handling * nimgrep: rename dangerous `-r` argument * add a `--newLine` style option for starting matching/context lines from a new line * add color themes: 3 new themes besides default `simple` * enable printing of multi-line matches with line numbers * proper display of replace when there was another match replaced at the same line / context block * improve cmdline arguments error reporting
This commit is contained in:
committed by
Andreas Rumpf
parent
0e7338d65c
commit
26074f594d
@@ -458,6 +458,11 @@ proc eraseScreen*(f: File) =
|
||||
else:
|
||||
f.write("\e[2J")
|
||||
|
||||
when not defined(windows):
|
||||
var
|
||||
gFG {.threadvar.}: int
|
||||
gBG {.threadvar.}: int
|
||||
|
||||
proc resetAttributes*(f: File) =
|
||||
## Resets all attributes.
|
||||
when defined(windows):
|
||||
@@ -468,6 +473,8 @@ proc resetAttributes*(f: File) =
|
||||
discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr)
|
||||
else:
|
||||
f.write(ansiResetCode)
|
||||
gFG = 0
|
||||
gBG = 0
|
||||
|
||||
type
|
||||
Style* = enum ## different styles for text output
|
||||
@@ -481,11 +488,6 @@ type
|
||||
styleHidden, ## hidden text
|
||||
styleStrikethrough ## strikethrough
|
||||
|
||||
when not defined(windows):
|
||||
var
|
||||
gFG {.threadvar.}: int
|
||||
gBG {.threadvar.}: int
|
||||
|
||||
proc ansiStyleCode*(style: int): string =
|
||||
result = fmt"{stylePrefix}{style}m"
|
||||
|
||||
@@ -938,6 +940,11 @@ when not defined(testing) and isMainModule:
|
||||
stdout.styledWriteLine(" ordinary text ")
|
||||
stdout.styledWriteLine(fgGreen, "green text")
|
||||
|
||||
writeStyled("underscored text", {styleUnderscore})
|
||||
stdout.styledWrite(fgRed, " red text ")
|
||||
writeStyled("bright text ", {styleBright})
|
||||
echo "ordinary text"
|
||||
|
||||
stdout.styledWrite(fgRed, "red text ")
|
||||
stdout.styledWrite(fgWhite, bgRed, "white text in red background")
|
||||
stdout.styledWrite(" ordinary text ")
|
||||
|
||||
@@ -11,7 +11,7 @@ import
|
||||
os, strutils, parseopt, pegs, re, terminal
|
||||
|
||||
const
|
||||
Version = "1.4"
|
||||
Version = "1.5"
|
||||
Usage = "nimgrep - Nim Grep Utility Version " & Version & """
|
||||
|
||||
(c) 2012 Andreas Rumpf
|
||||
@@ -19,12 +19,13 @@ Usage:
|
||||
nimgrep [options] [pattern] [replacement] (file/directory)*
|
||||
Options:
|
||||
--find, -f find the pattern (default)
|
||||
--replace, -r replace the pattern
|
||||
--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 process directories recursively
|
||||
--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
|
||||
@@ -32,10 +33,25 @@ Options:
|
||||
--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)
|
||||
--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
|
||||
--group group matches by file
|
||||
--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
|
||||
@@ -47,7 +63,7 @@ type
|
||||
TOption = enum
|
||||
optFind, optReplace, optPeg, optRegex, optRecursive, optConfirm, optStdin,
|
||||
optWord, optIgnoreCase, optIgnoreStyle, optVerbose, optFilenames,
|
||||
optRex
|
||||
optRex, optFollow
|
||||
TOptions = set[TOption]
|
||||
TConfirmEnum = enum
|
||||
ceAbort, ceYes, ceAll, ceNo, ceNone
|
||||
@@ -61,8 +77,17 @@ var
|
||||
replacement = ""
|
||||
extensions: seq[string] = @[]
|
||||
options: TOptions = {optRegex}
|
||||
skipExtensions: seq[string] = @[]
|
||||
excludeFile: seq[Regex]
|
||||
includeFile: seq[Regex]
|
||||
excludeDir: seq[Regex]
|
||||
useWriteStyled = true
|
||||
oneline = false
|
||||
oneline = true
|
||||
linesBefore = 0
|
||||
linesAfter = 0
|
||||
linesContext = 0
|
||||
colorTheme = "simple"
|
||||
newLine = false
|
||||
|
||||
proc ask(msg: string): string =
|
||||
stdout.write(msg)
|
||||
@@ -79,63 +104,262 @@ proc confirm: TConfirmEnum =
|
||||
of "e", "none": return ceNone
|
||||
else: discard
|
||||
|
||||
proc countLines(s: string, first, last: int): int =
|
||||
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] == '\13':
|
||||
if s[i] == '\c':
|
||||
inc result
|
||||
if i < last and s[i+1] == '\10': inc(i)
|
||||
elif s[i] == '\10':
|
||||
if i < last and s[i+1] == '\l': inc(i)
|
||||
elif s[i] == '\l':
|
||||
inc result
|
||||
inc i
|
||||
|
||||
proc beforePattern(s: string, first: int): int =
|
||||
result = first-1
|
||||
while result >= 0:
|
||||
if s[result] in Newlines: break
|
||||
dec(result)
|
||||
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, last: int): int =
|
||||
result = last+1
|
||||
while result < s.len:
|
||||
if s[result] in Newlines: break
|
||||
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)
|
||||
|
||||
proc writeColored(s: string) =
|
||||
template whenColors(body: untyped) =
|
||||
if useWriteStyled:
|
||||
terminal.writeStyled(s, {styleUnderscore, styleBright})
|
||||
body
|
||||
else:
|
||||
stdout.write(s)
|
||||
|
||||
proc highlight(s, match, repl: string, t: tuple[first, last: int],
|
||||
filename:string, line: int, showRepl: bool) =
|
||||
const alignment = 6
|
||||
if oneline:
|
||||
stdout.write(filename, ":", line, ": ")
|
||||
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(line.`$`.align(alignment), ": ")
|
||||
var x = beforePattern(s, t.first)
|
||||
var y = afterPattern(s, t.last)
|
||||
for i in x .. t.first-1: stdout.write(s[i])
|
||||
writeColored(match)
|
||||
for i in t.last+1 .. y: stdout.write(s[i])
|
||||
stdout.write("\n")
|
||||
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:
|
||||
stdout.write(spaces(alignment-1), "-> ")
|
||||
for i in x .. t.first-1: stdout.write(s[i])
|
||||
writeColored(repl)
|
||||
for i in t.last+1 .. y: stdout.write(s[i])
|
||||
stdout.write("\n")
|
||||
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 processFile(pattern; filename: string; counter: var int) =
|
||||
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:
|
||||
stdout.writeLine(filename)
|
||||
printBlockFile(filename)
|
||||
stdout.write("\n")
|
||||
stdout.flushFile()
|
||||
filenameShown = true
|
||||
|
||||
@@ -146,59 +370,58 @@ proc processFile(pattern; filename: string; counter: var int) =
|
||||
try:
|
||||
buffer = system.readFile(filename)
|
||||
except IOError:
|
||||
echo "cannot open file: ", filename
|
||||
printError "Error: cannot open file: " & filename
|
||||
inc(errors)
|
||||
return
|
||||
if optVerbose in options:
|
||||
stdout.writeLine(filename)
|
||||
printFile(filename)
|
||||
stdout.write("\n")
|
||||
stdout.flushFile()
|
||||
var result: string
|
||||
|
||||
if optReplace in options:
|
||||
result = newStringOfCap(buffer.len)
|
||||
|
||||
var line = 1
|
||||
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: break
|
||||
inc(line, countLines(buffer, i, t.first-1))
|
||||
|
||||
var wholeMatch = buffer.substr(t.first, t.last)
|
||||
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:
|
||||
highlight(buffer, wholeMatch, "", t, filename, line, showRepl=false)
|
||||
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)
|
||||
stdout.flushFile()
|
||||
else:
|
||||
let r = replace(wholeMatch, pattern, replacement % matches)
|
||||
if optConfirm in options:
|
||||
highlight(buffer, wholeMatch, r, t, filename, line, showRepl=true)
|
||||
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:
|
||||
highlight(buffer, wholeMatch, r, t, filename, line, showRepl=reallyReplace)
|
||||
if reallyReplace:
|
||||
result.add(buffer.substr(i, t.first-1))
|
||||
result.add(r)
|
||||
else:
|
||||
result.add(buffer.substr(i, t.last))
|
||||
let r = replace(curMi.match, pattern, replacement % matches)
|
||||
doReplace(si, curMi, i, r, result, lineRepl, reallyReplace)
|
||||
|
||||
inc(line, countLines(buffer, t.first, t.last))
|
||||
i = t.last+1
|
||||
prevMi = curMi
|
||||
|
||||
if optReplace in options:
|
||||
result.add(substr(buffer, i))
|
||||
result.add(substr(buffer, i)) # finalize new buffer after last match
|
||||
var f: File
|
||||
if open(f, filename, fmWrite):
|
||||
f.write(result)
|
||||
@@ -206,10 +429,34 @@ proc processFile(pattern; filename: string; counter: var int) =
|
||||
else:
|
||||
quit "cannot open file for overwriting: " & filename
|
||||
|
||||
proc hasRightExt(filename: string, exts: seq[string]): bool =
|
||||
var y = splitFile(filename).ext.substr(1) # skip leading '.'
|
||||
for x in items(exts):
|
||||
if os.cmpPaths(x, y) == 0: return true
|
||||
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 =
|
||||
@@ -245,17 +492,32 @@ proc styleInsensitive(s: string): string =
|
||||
addx()
|
||||
else: addx()
|
||||
|
||||
proc walker(pattern; dir: string; counter: var int) =
|
||||
for kind, path in walkDir(dir):
|
||||
case kind
|
||||
of pcFile:
|
||||
if extensions.len == 0 or path.hasRightExt(extensions):
|
||||
processFile(pattern, path, counter)
|
||||
of pcDir:
|
||||
if optRecursive in options:
|
||||
walker(pattern, path, counter)
|
||||
else: discard
|
||||
if existsFile(dir): processFile(pattern, dir, counter)
|
||||
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)
|
||||
@@ -275,7 +537,6 @@ when defined(posix):
|
||||
useWriteStyled = terminal.isatty(stdout)
|
||||
# that should be before option processing to allow override of useWriteStyled
|
||||
|
||||
oneline = true
|
||||
for kind, key, val in getopt():
|
||||
case kind
|
||||
of cmdArgument:
|
||||
@@ -290,7 +551,7 @@ for kind, key, val in getopt():
|
||||
of cmdLongOption, cmdShortOption:
|
||||
case normalize(key)
|
||||
of "find", "f": incl(options, optFind)
|
||||
of "replace", "r": incl(options, optReplace)
|
||||
of "replace", "!": incl(options, optReplace)
|
||||
of "peg":
|
||||
excl(options, optRegex)
|
||||
incl(options, optPeg)
|
||||
@@ -301,27 +562,55 @@ for kind, key, val in getopt():
|
||||
incl(options, optRex)
|
||||
incl(options, optRegex)
|
||||
excl(options, optPeg)
|
||||
of "recursive": incl(options, optRecursive)
|
||||
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: writeHelp()
|
||||
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": oneline = false
|
||||
of "group", "g": oneline = false
|
||||
of "verbose": incl(options, optVerbose)
|
||||
of "filenames": incl(options, optFilenames)
|
||||
of "help", "h": writeHelp()
|
||||
of "version", "v": writeVersion()
|
||||
else: writeHelp()
|
||||
else: reportError("unrecognized option '" & key & "'")
|
||||
of cmdEnd: assert(false) # cannot happen
|
||||
|
||||
checkOptions({optFind, optReplace}, "find", "replace")
|
||||
@@ -329,6 +618,9 @@ 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)
|
||||
@@ -336,9 +628,10 @@ if optStdin in options:
|
||||
replacement = ask("replacement [supports $1, $# notations]: ")
|
||||
|
||||
if pattern.len == 0:
|
||||
writeHelp()
|
||||
reportError("empty pattern was given")
|
||||
else:
|
||||
var counter = 0
|
||||
var errors = 0
|
||||
if filenames.len == 0:
|
||||
filenames.add(os.getCurrentDir())
|
||||
if optRegex notin options:
|
||||
@@ -350,7 +643,7 @@ else:
|
||||
pattern = "\\i " & pattern
|
||||
let pegp = peg(pattern)
|
||||
for f in items(filenames):
|
||||
walker(pegp, f, counter)
|
||||
walker(pegp, f, counter, errors)
|
||||
else:
|
||||
var reflags = {reStudy}
|
||||
if optIgnoreStyle in options:
|
||||
@@ -362,5 +655,9 @@ else:
|
||||
let rep = if optRex in options: rex(pattern, reflags)
|
||||
else: re(pattern, reflags)
|
||||
for f in items(filenames):
|
||||
walker(rep, f, counter)
|
||||
walker(rep, f, counter, errors)
|
||||
if errors != 0:
|
||||
printError $errors & " errors"
|
||||
stdout.write($counter & " matches\n")
|
||||
if errors != 0:
|
||||
quit(1)
|
||||
|
||||
Reference in New Issue
Block a user