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:
parent
0e7338d65c
commit
26074f594d
2 changed files with 406 additions and 102 deletions
|
|
@ -458,6 +458,11 @@ proc eraseScreen*(f: File) =
|
||||||
else:
|
else:
|
||||||
f.write("\e[2J")
|
f.write("\e[2J")
|
||||||
|
|
||||||
|
when not defined(windows):
|
||||||
|
var
|
||||||
|
gFG {.threadvar.}: int
|
||||||
|
gBG {.threadvar.}: int
|
||||||
|
|
||||||
proc resetAttributes*(f: File) =
|
proc resetAttributes*(f: File) =
|
||||||
## Resets all attributes.
|
## Resets all attributes.
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
|
|
@ -468,6 +473,8 @@ proc resetAttributes*(f: File) =
|
||||||
discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr)
|
discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr)
|
||||||
else:
|
else:
|
||||||
f.write(ansiResetCode)
|
f.write(ansiResetCode)
|
||||||
|
gFG = 0
|
||||||
|
gBG = 0
|
||||||
|
|
||||||
type
|
type
|
||||||
Style* = enum ## different styles for text output
|
Style* = enum ## different styles for text output
|
||||||
|
|
@ -481,11 +488,6 @@ type
|
||||||
styleHidden, ## hidden text
|
styleHidden, ## hidden text
|
||||||
styleStrikethrough ## strikethrough
|
styleStrikethrough ## strikethrough
|
||||||
|
|
||||||
when not defined(windows):
|
|
||||||
var
|
|
||||||
gFG {.threadvar.}: int
|
|
||||||
gBG {.threadvar.}: int
|
|
||||||
|
|
||||||
proc ansiStyleCode*(style: int): string =
|
proc ansiStyleCode*(style: int): string =
|
||||||
result = fmt"{stylePrefix}{style}m"
|
result = fmt"{stylePrefix}{style}m"
|
||||||
|
|
||||||
|
|
@ -938,6 +940,11 @@ when not defined(testing) and isMainModule:
|
||||||
stdout.styledWriteLine(" ordinary text ")
|
stdout.styledWriteLine(" ordinary text ")
|
||||||
stdout.styledWriteLine(fgGreen, "green 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(fgRed, "red text ")
|
||||||
stdout.styledWrite(fgWhite, bgRed, "white text in red background")
|
stdout.styledWrite(fgWhite, bgRed, "white text in red background")
|
||||||
stdout.styledWrite(" ordinary text ")
|
stdout.styledWrite(" ordinary text ")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import
|
||||||
os, strutils, parseopt, pegs, re, terminal
|
os, strutils, parseopt, pegs, re, terminal
|
||||||
|
|
||||||
const
|
const
|
||||||
Version = "1.4"
|
Version = "1.5"
|
||||||
Usage = "nimgrep - Nim Grep Utility Version " & Version & """
|
Usage = "nimgrep - Nim Grep Utility Version " & Version & """
|
||||||
|
|
||||||
(c) 2012 Andreas Rumpf
|
(c) 2012 Andreas Rumpf
|
||||||
|
|
@ -19,12 +19,13 @@ Usage:
|
||||||
nimgrep [options] [pattern] [replacement] (file/directory)*
|
nimgrep [options] [pattern] [replacement] (file/directory)*
|
||||||
Options:
|
Options:
|
||||||
--find, -f find the pattern (default)
|
--find, -f find the pattern (default)
|
||||||
--replace, -r replace the pattern
|
--replace, -! replace the pattern
|
||||||
--peg pattern is a peg
|
--peg pattern is a peg
|
||||||
--re pattern is a regular expression (default)
|
--re pattern is a regular expression (default)
|
||||||
--rex, -x use the "extended" syntax for the regular expression
|
--rex, -x use the "extended" syntax for the regular expression
|
||||||
so that whitespace is not significant
|
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
|
--confirm confirm each occurrence/replacement; there is a chance
|
||||||
to abort any time without touching the file
|
to abort any time without touching the file
|
||||||
--stdin read pattern from stdin (to avoid the shell's confusing
|
--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!)
|
--word, -w the match should have word boundaries (buggy for pegs!)
|
||||||
--ignoreCase, -i be case insensitive
|
--ignoreCase, -i be case insensitive
|
||||||
--ignoreStyle, -y be style 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
|
--nocolor output will be given without any colours
|
||||||
--color[:always] force color even if output is redirected
|
--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
|
--verbose be verbose: list every processed file
|
||||||
--filenames find the pattern in the filenames, not in the contents
|
--filenames find the pattern in the filenames, not in the contents
|
||||||
of the file
|
of the file
|
||||||
|
|
@ -47,7 +63,7 @@ type
|
||||||
TOption = enum
|
TOption = enum
|
||||||
optFind, optReplace, optPeg, optRegex, optRecursive, optConfirm, optStdin,
|
optFind, optReplace, optPeg, optRegex, optRecursive, optConfirm, optStdin,
|
||||||
optWord, optIgnoreCase, optIgnoreStyle, optVerbose, optFilenames,
|
optWord, optIgnoreCase, optIgnoreStyle, optVerbose, optFilenames,
|
||||||
optRex
|
optRex, optFollow
|
||||||
TOptions = set[TOption]
|
TOptions = set[TOption]
|
||||||
TConfirmEnum = enum
|
TConfirmEnum = enum
|
||||||
ceAbort, ceYes, ceAll, ceNo, ceNone
|
ceAbort, ceYes, ceAll, ceNo, ceNone
|
||||||
|
|
@ -61,8 +77,17 @@ var
|
||||||
replacement = ""
|
replacement = ""
|
||||||
extensions: seq[string] = @[]
|
extensions: seq[string] = @[]
|
||||||
options: TOptions = {optRegex}
|
options: TOptions = {optRegex}
|
||||||
|
skipExtensions: seq[string] = @[]
|
||||||
|
excludeFile: seq[Regex]
|
||||||
|
includeFile: seq[Regex]
|
||||||
|
excludeDir: seq[Regex]
|
||||||
useWriteStyled = true
|
useWriteStyled = true
|
||||||
oneline = false
|
oneline = true
|
||||||
|
linesBefore = 0
|
||||||
|
linesAfter = 0
|
||||||
|
linesContext = 0
|
||||||
|
colorTheme = "simple"
|
||||||
|
newLine = false
|
||||||
|
|
||||||
proc ask(msg: string): string =
|
proc ask(msg: string): string =
|
||||||
stdout.write(msg)
|
stdout.write(msg)
|
||||||
|
|
@ -79,63 +104,262 @@ proc confirm: TConfirmEnum =
|
||||||
of "e", "none": return ceNone
|
of "e", "none": return ceNone
|
||||||
else: discard
|
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
|
var i = first
|
||||||
while i <= last:
|
while i <= last:
|
||||||
if s[i] == '\13':
|
if s[i] == '\c':
|
||||||
inc result
|
inc result
|
||||||
if i < last and s[i+1] == '\10': inc(i)
|
if i < last and s[i+1] == '\l': inc(i)
|
||||||
elif s[i] == '\10':
|
elif s[i] == '\l':
|
||||||
inc result
|
inc result
|
||||||
inc i
|
inc i
|
||||||
|
|
||||||
proc beforePattern(s: string, first: int): int =
|
func beforePattern(s: string, pos: int, nLines = 1): int =
|
||||||
result = first-1
|
var linesLeft = nLines
|
||||||
while result >= 0:
|
result = min(pos, s.len-1)
|
||||||
if s[result] in Newlines: break
|
while true:
|
||||||
dec(result)
|
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)
|
inc(result)
|
||||||
|
|
||||||
proc afterPattern(s: string, last: int): int =
|
proc afterPattern(s: string, pos: int, nLines = 1): int =
|
||||||
result = last+1
|
result = max(0, pos)
|
||||||
while result < s.len:
|
var linesScanned = 0
|
||||||
if s[result] in Newlines: break
|
while true:
|
||||||
inc(result)
|
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)
|
dec(result)
|
||||||
|
|
||||||
proc writeColored(s: string) =
|
template whenColors(body: untyped) =
|
||||||
if useWriteStyled:
|
if useWriteStyled:
|
||||||
terminal.writeStyled(s, {styleUnderscore, styleBright})
|
body
|
||||||
else:
|
else:
|
||||||
stdout.write(s)
|
stdout.write(s)
|
||||||
|
|
||||||
proc highlight(s, match, repl: string, t: tuple[first, last: int],
|
proc printFile(s: string) =
|
||||||
filename:string, line: int, showRepl: bool) =
|
whenColors:
|
||||||
const alignment = 6
|
case colorTheme
|
||||||
if oneline:
|
of "simple": stdout.write(s)
|
||||||
stdout.write(filename, ":", line, ": ")
|
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:
|
else:
|
||||||
stdout.write(line.`$`.align(alignment), ": ")
|
stdout.write(lines[0]) # complete the line after match itself
|
||||||
var x = beforePattern(s, t.first)
|
stdout.write("\n")
|
||||||
var y = afterPattern(s, t.last)
|
let skipLine = # workaround posix line ending at the end of file
|
||||||
for i in x .. t.first-1: stdout.write(s[i])
|
if last == s.len-1 and s.len >= 2 and s[^1] == '\l' and s[^2] != '\c': 1
|
||||||
writeColored(match)
|
else: 0
|
||||||
for i in t.last+1 .. y: stdout.write(s[i])
|
for i in 1 ..< lines.len - skipLine:
|
||||||
stdout.write("\n")
|
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()
|
stdout.flushFile()
|
||||||
if showRepl:
|
if showRepl:
|
||||||
stdout.write(spaces(alignment-1), "-> ")
|
let newSi: SearchInfo = (buf: newBuf, filename: si.filename)
|
||||||
for i in x .. t.first-1: stdout.write(s[i])
|
let miForNewBuf: MatchInfo =
|
||||||
writeColored(repl)
|
(first: newBuf.len, last: newBuf.len,
|
||||||
for i in t.last+1 .. y: stdout.write(s[i])
|
lineBeg: curLine, lineEnd: curLine, match: "")
|
||||||
stdout.write("\n")
|
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()
|
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
|
var filenameShown = false
|
||||||
template beforeHighlight =
|
template beforeHighlight =
|
||||||
if not filenameShown and optVerbose notin options and not oneline:
|
if not filenameShown and optVerbose notin options and not oneline:
|
||||||
stdout.writeLine(filename)
|
printBlockFile(filename)
|
||||||
|
stdout.write("\n")
|
||||||
stdout.flushFile()
|
stdout.flushFile()
|
||||||
filenameShown = true
|
filenameShown = true
|
||||||
|
|
||||||
|
|
@ -146,59 +370,58 @@ proc processFile(pattern; filename: string; counter: var int) =
|
||||||
try:
|
try:
|
||||||
buffer = system.readFile(filename)
|
buffer = system.readFile(filename)
|
||||||
except IOError:
|
except IOError:
|
||||||
echo "cannot open file: ", filename
|
printError "Error: cannot open file: " & filename
|
||||||
|
inc(errors)
|
||||||
return
|
return
|
||||||
if optVerbose in options:
|
if optVerbose in options:
|
||||||
stdout.writeLine(filename)
|
printFile(filename)
|
||||||
|
stdout.write("\n")
|
||||||
stdout.flushFile()
|
stdout.flushFile()
|
||||||
var result: string
|
var result: string
|
||||||
|
|
||||||
if optReplace in options:
|
if optReplace in options:
|
||||||
result = newStringOfCap(buffer.len)
|
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 i = 0
|
||||||
var matches: array[0..re.MaxSubpatterns-1, string]
|
var matches: array[0..re.MaxSubpatterns-1, string]
|
||||||
for j in 0..high(matches): matches[j] = ""
|
for j in 0..high(matches): matches[j] = ""
|
||||||
var reallyReplace = true
|
var reallyReplace = true
|
||||||
while i < buffer.len:
|
while i < buffer.len:
|
||||||
let t = findBounds(buffer, pattern, matches, i)
|
let t = findBounds(buffer, pattern, matches, i)
|
||||||
if t.first < 0 or t.last < t.first: break
|
if t.first < 0 or t.last < t.first:
|
||||||
inc(line, countLines(buffer, i, t.first-1))
|
if optReplace notin options and prevMi.lineBeg != 0: # finalize last match
|
||||||
|
printLinesAfter(si, prevMi, 1+linesAfter)
|
||||||
var wholeMatch = buffer.substr(t.first, t.last)
|
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()
|
beforeHighlight()
|
||||||
inc counter
|
inc counter
|
||||||
if optReplace notin options:
|
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:
|
else:
|
||||||
let r = replace(wholeMatch, pattern, replacement % matches)
|
let r = replace(curMi.match, pattern, replacement % matches)
|
||||||
if optConfirm in options:
|
doReplace(si, curMi, i, r, result, lineRepl, reallyReplace)
|
||||||
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))
|
|
||||||
|
|
||||||
inc(line, countLines(buffer, t.first, t.last))
|
|
||||||
i = t.last+1
|
i = t.last+1
|
||||||
|
prevMi = curMi
|
||||||
|
|
||||||
if optReplace in options:
|
if optReplace in options:
|
||||||
result.add(substr(buffer, i))
|
result.add(substr(buffer, i)) # finalize new buffer after last match
|
||||||
var f: File
|
var f: File
|
||||||
if open(f, filename, fmWrite):
|
if open(f, filename, fmWrite):
|
||||||
f.write(result)
|
f.write(result)
|
||||||
|
|
@ -206,10 +429,34 @@ proc processFile(pattern; filename: string; counter: var int) =
|
||||||
else:
|
else:
|
||||||
quit "cannot open file for overwriting: " & filename
|
quit "cannot open file for overwriting: " & filename
|
||||||
|
|
||||||
proc hasRightExt(filename: string, exts: seq[string]): bool =
|
proc hasRightFileName(path: string): bool =
|
||||||
var y = splitFile(filename).ext.substr(1) # skip leading '.'
|
let filename = path.lastPathPart
|
||||||
for x in items(exts):
|
let ex = filename.splitFile.ext.substr(1) # skip leading '.'
|
||||||
if os.cmpPaths(x, y) == 0: return true
|
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 =
|
proc styleInsensitive(s: string): string =
|
||||||
template addx =
|
template addx =
|
||||||
|
|
@ -245,17 +492,32 @@ proc styleInsensitive(s: string): string =
|
||||||
addx()
|
addx()
|
||||||
else: addx()
|
else: addx()
|
||||||
|
|
||||||
proc walker(pattern; dir: string; counter: var int) =
|
proc walker(pattern; dir: string; counter: var int, errors: var int) =
|
||||||
for kind, path in walkDir(dir):
|
if existsDir(dir):
|
||||||
case kind
|
for kind, path in walkDir(dir):
|
||||||
of pcFile:
|
case kind
|
||||||
if extensions.len == 0 or path.hasRightExt(extensions):
|
of pcFile:
|
||||||
processFile(pattern, path, counter)
|
if path.hasRightFileName:
|
||||||
of pcDir:
|
processFile(pattern, path, counter, errors)
|
||||||
if optRecursive in options:
|
of pcLinkToFile:
|
||||||
walker(pattern, path, counter)
|
if optFollow in options and path.hasRightFileName:
|
||||||
else: discard
|
processFile(pattern, path, counter, errors)
|
||||||
if existsFile(dir): processFile(pattern, dir, counter)
|
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() =
|
proc writeHelp() =
|
||||||
stdout.write(Usage)
|
stdout.write(Usage)
|
||||||
|
|
@ -275,7 +537,6 @@ when defined(posix):
|
||||||
useWriteStyled = terminal.isatty(stdout)
|
useWriteStyled = terminal.isatty(stdout)
|
||||||
# that should be before option processing to allow override of useWriteStyled
|
# that should be before option processing to allow override of useWriteStyled
|
||||||
|
|
||||||
oneline = true
|
|
||||||
for kind, key, val in getopt():
|
for kind, key, val in getopt():
|
||||||
case kind
|
case kind
|
||||||
of cmdArgument:
|
of cmdArgument:
|
||||||
|
|
@ -290,7 +551,7 @@ for kind, key, val in getopt():
|
||||||
of cmdLongOption, cmdShortOption:
|
of cmdLongOption, cmdShortOption:
|
||||||
case normalize(key)
|
case normalize(key)
|
||||||
of "find", "f": incl(options, optFind)
|
of "find", "f": incl(options, optFind)
|
||||||
of "replace", "r": incl(options, optReplace)
|
of "replace", "!": incl(options, optReplace)
|
||||||
of "peg":
|
of "peg":
|
||||||
excl(options, optRegex)
|
excl(options, optRegex)
|
||||||
incl(options, optPeg)
|
incl(options, optPeg)
|
||||||
|
|
@ -301,27 +562,55 @@ for kind, key, val in getopt():
|
||||||
incl(options, optRex)
|
incl(options, optRex)
|
||||||
incl(options, optRegex)
|
incl(options, optRegex)
|
||||||
excl(options, optPeg)
|
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 "confirm": incl(options, optConfirm)
|
||||||
of "stdin": incl(options, optStdin)
|
of "stdin": incl(options, optStdin)
|
||||||
of "word", "w": incl(options, optWord)
|
of "word", "w": incl(options, optWord)
|
||||||
of "ignorecase", "i": incl(options, optIgnoreCase)
|
of "ignorecase", "i": incl(options, optIgnoreCase)
|
||||||
of "ignorestyle", "y": incl(options, optIgnoreStyle)
|
of "ignorestyle", "y": incl(options, optIgnoreStyle)
|
||||||
of "ext": extensions.add val.split('|')
|
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 "nocolor": useWriteStyled = false
|
||||||
of "color":
|
of "color":
|
||||||
case val
|
case val
|
||||||
of "auto": discard
|
of "auto": discard
|
||||||
of "never", "false": useWriteStyled = false
|
of "never", "false": useWriteStyled = false
|
||||||
of "", "always", "true": useWriteStyled = true
|
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 "oneline": oneline = true
|
||||||
of "group": oneline = false
|
of "group", "g": oneline = false
|
||||||
of "verbose": incl(options, optVerbose)
|
of "verbose": incl(options, optVerbose)
|
||||||
of "filenames": incl(options, optFilenames)
|
of "filenames": incl(options, optFilenames)
|
||||||
of "help", "h": writeHelp()
|
of "help", "h": writeHelp()
|
||||||
of "version", "v": writeVersion()
|
of "version", "v": writeVersion()
|
||||||
else: writeHelp()
|
else: reportError("unrecognized option '" & key & "'")
|
||||||
of cmdEnd: assert(false) # cannot happen
|
of cmdEnd: assert(false) # cannot happen
|
||||||
|
|
||||||
checkOptions({optFind, optReplace}, "find", "replace")
|
checkOptions({optFind, optReplace}, "find", "replace")
|
||||||
|
|
@ -329,6 +618,9 @@ checkOptions({optPeg, optRegex}, "peg", "re")
|
||||||
checkOptions({optIgnoreCase, optIgnoreStyle}, "ignore_case", "ignore_style")
|
checkOptions({optIgnoreCase, optIgnoreStyle}, "ignore_case", "ignore_style")
|
||||||
checkOptions({optFilenames, optReplace}, "filenames", "replace")
|
checkOptions({optFilenames, optReplace}, "filenames", "replace")
|
||||||
|
|
||||||
|
linesBefore = max(linesBefore, linesContext)
|
||||||
|
linesAfter = max(linesAfter, linesContext)
|
||||||
|
|
||||||
if optStdin in options:
|
if optStdin in options:
|
||||||
pattern = ask("pattern [ENTER to exit]: ")
|
pattern = ask("pattern [ENTER to exit]: ")
|
||||||
if pattern.len == 0: quit(0)
|
if pattern.len == 0: quit(0)
|
||||||
|
|
@ -336,9 +628,10 @@ if optStdin in options:
|
||||||
replacement = ask("replacement [supports $1, $# notations]: ")
|
replacement = ask("replacement [supports $1, $# notations]: ")
|
||||||
|
|
||||||
if pattern.len == 0:
|
if pattern.len == 0:
|
||||||
writeHelp()
|
reportError("empty pattern was given")
|
||||||
else:
|
else:
|
||||||
var counter = 0
|
var counter = 0
|
||||||
|
var errors = 0
|
||||||
if filenames.len == 0:
|
if filenames.len == 0:
|
||||||
filenames.add(os.getCurrentDir())
|
filenames.add(os.getCurrentDir())
|
||||||
if optRegex notin options:
|
if optRegex notin options:
|
||||||
|
|
@ -350,7 +643,7 @@ else:
|
||||||
pattern = "\\i " & pattern
|
pattern = "\\i " & pattern
|
||||||
let pegp = peg(pattern)
|
let pegp = peg(pattern)
|
||||||
for f in items(filenames):
|
for f in items(filenames):
|
||||||
walker(pegp, f, counter)
|
walker(pegp, f, counter, errors)
|
||||||
else:
|
else:
|
||||||
var reflags = {reStudy}
|
var reflags = {reStudy}
|
||||||
if optIgnoreStyle in options:
|
if optIgnoreStyle in options:
|
||||||
|
|
@ -362,5 +655,9 @@ else:
|
||||||
let rep = if optRex in options: rex(pattern, reflags)
|
let rep = if optRex in options: rex(pattern, reflags)
|
||||||
else: re(pattern, reflags)
|
else: re(pattern, reflags)
|
||||||
for f in items(filenames):
|
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")
|
stdout.write($counter & " matches\n")
|
||||||
|
if errors != 0:
|
||||||
|
quit(1)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue