nimgrep.nim 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. #
  2. #
  3. # Nim Grep Utility
  4. # (c) Copyright 2012 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. import
  10. os, strutils, parseopt, pegs, re, terminal
  11. const
  12. Version = "1.5"
  13. Usage = "nimgrep - Nim Grep Utility Version " & Version & """
  14. (c) 2012 Andreas Rumpf
  15. Usage:
  16. nimgrep [options] [pattern] [replacement] (file/directory)*
  17. Options:
  18. --find, -f find the pattern (default)
  19. --replace, -! replace the pattern
  20. --peg pattern is a peg
  21. --re pattern is a regular expression (default)
  22. --rex, -x use the "extended" syntax for the regular expression
  23. so that whitespace is not significant
  24. --recursive, -r process directories recursively
  25. --follow follow all symlinks when processing recursively
  26. --confirm confirm each occurrence/replacement; there is a chance
  27. to abort any time without touching the file
  28. --stdin read pattern from stdin (to avoid the shell's confusing
  29. quoting rules)
  30. --word, -w the match should have word boundaries (buggy for pegs!)
  31. --ignoreCase, -i be case insensitive
  32. --ignoreStyle, -y be style insensitive
  33. --ext:EX1|EX2|... only search the files with the given extension(s),
  34. empty one ("--ext") means files with missing extension
  35. --noExt:EX1|... exclude files having given extension(s), use empty one to
  36. skip files with no extension (like some binary files are)
  37. --includeFile:PAT include only files whose names match the given regex PAT
  38. --excludeFile:PAT skip files whose names match the given regex pattern PAT
  39. --excludeDir:PAT skip directories whose names match the given regex PAT
  40. --nocolor output will be given without any colours
  41. --color[:always] force color even if output is redirected
  42. --colorTheme:THEME select color THEME from 'simple' (default),
  43. 'bnw' (black and white) ,'ack', or 'gnu' (GNU grep)
  44. --afterContext:N,
  45. -a:N print N lines of trailing context after every match
  46. --beforeContext:N,
  47. -b:N print N lines of leading context before every match
  48. --context:N, -c:N print N lines of leading context before every match and
  49. N lines of trailing context after it
  50. --group, -g group matches by file
  51. --newLine, -l display every matching line starting from a new line
  52. --verbose be verbose: list every processed file
  53. --filenames find the pattern in the filenames, not in the contents
  54. of the file
  55. --help, -h shows this help
  56. --version, -v shows the version
  57. """
  58. type
  59. TOption = enum
  60. optFind, optReplace, optPeg, optRegex, optRecursive, optConfirm, optStdin,
  61. optWord, optIgnoreCase, optIgnoreStyle, optVerbose, optFilenames,
  62. optRex, optFollow
  63. TOptions = set[TOption]
  64. TConfirmEnum = enum
  65. ceAbort, ceYes, ceAll, ceNo, ceNone
  66. Pattern = Regex | Peg
  67. using pattern: Pattern
  68. var
  69. filenames: seq[string] = @[]
  70. pattern = ""
  71. replacement = ""
  72. extensions: seq[string] = @[]
  73. options: TOptions = {optRegex}
  74. skipExtensions: seq[string] = @[]
  75. excludeFile: seq[Regex]
  76. includeFile: seq[Regex]
  77. excludeDir: seq[Regex]
  78. useWriteStyled = true
  79. oneline = true
  80. linesBefore = 0
  81. linesAfter = 0
  82. linesContext = 0
  83. colorTheme = "simple"
  84. newLine = false
  85. proc ask(msg: string): string =
  86. stdout.write(msg)
  87. stdout.flushFile()
  88. result = stdin.readLine()
  89. proc confirm: TConfirmEnum =
  90. while true:
  91. case normalize(ask(" [a]bort; [y]es, a[l]l, [n]o, non[e]: "))
  92. of "a", "abort": return ceAbort
  93. of "y", "yes": return ceYes
  94. of "l", "all": return ceAll
  95. of "n", "no": return ceNo
  96. of "e", "none": return ceNone
  97. else: discard
  98. func countLineBreaks(s: string, first, last: int): int =
  99. # count line breaks (unlike strutils.countLines starts count from 0)
  100. var i = first
  101. while i <= last:
  102. if s[i] == '\c':
  103. inc result
  104. if i < last and s[i+1] == '\l': inc(i)
  105. elif s[i] == '\l':
  106. inc result
  107. inc i
  108. func beforePattern(s: string, pos: int, nLines = 1): int =
  109. var linesLeft = nLines
  110. result = min(pos, s.len-1)
  111. while true:
  112. while result >= 0 and s[result] notin {'\c', '\l'}: dec(result)
  113. if result == -1: break
  114. if s[result] == '\l':
  115. dec(linesLeft)
  116. if linesLeft == 0: break
  117. dec(result)
  118. if result >= 0 and s[result] == '\c': dec(result)
  119. else: # '\c'
  120. dec(linesLeft)
  121. if linesLeft == 0: break
  122. dec(result)
  123. inc(result)
  124. proc afterPattern(s: string, pos: int, nLines = 1): int =
  125. result = max(0, pos)
  126. var linesScanned = 0
  127. while true:
  128. while result < s.len and s[result] notin {'\c', '\l'}: inc(result)
  129. inc(linesScanned)
  130. if linesScanned == nLines: break
  131. if result < s.len:
  132. if s[result] == '\l':
  133. inc(result)
  134. elif s[result] == '\c':
  135. inc(result)
  136. if result < s.len and s[result] == '\l': inc(result)
  137. else: break
  138. dec(result)
  139. template whenColors(body: untyped) =
  140. if useWriteStyled:
  141. body
  142. else:
  143. stdout.write(s)
  144. proc printFile(s: string) =
  145. whenColors:
  146. case colorTheme
  147. of "simple": stdout.write(s)
  148. of "bnw": stdout.styledWrite(styleUnderscore, s)
  149. of "ack": stdout.styledWrite(fgGreen, s)
  150. of "gnu": stdout.styledWrite(fgMagenta, s)
  151. proc printBlockFile(s: string) =
  152. whenColors:
  153. case colorTheme
  154. of "simple": stdout.styledWrite(styleBright, s)
  155. of "bnw": stdout.styledWrite(styleUnderscore, s)
  156. of "ack": stdout.styledWrite(styleUnderscore, fgGreen, s)
  157. of "gnu": stdout.styledWrite(styleUnderscore, fgMagenta, s)
  158. proc printError(s: string) =
  159. whenColors:
  160. case colorTheme
  161. of "simple", "bnw": stdout.styledWriteLine(styleBright, s)
  162. of "ack", "gnu": stdout.styledWriteLine(styleReverse, fgRed, bgDefault, s)
  163. stdout.flushFile()
  164. const alignment = 6
  165. proc printLineN(s: string, isMatch: bool) =
  166. whenColors:
  167. case colorTheme
  168. of "simple": stdout.write(s)
  169. of "bnw":
  170. if isMatch: stdout.styledWrite(styleBright, s)
  171. else: stdout.styledWrite(s)
  172. of "ack":
  173. if isMatch: stdout.styledWrite(fgYellow, s)
  174. else: stdout.styledWrite(fgGreen, s)
  175. of "gnu":
  176. if isMatch: stdout.styledWrite(fgGreen, s)
  177. else: stdout.styledWrite(fgCyan, s)
  178. proc printBlockLineN(s: string) =
  179. whenColors:
  180. case colorTheme
  181. of "simple": stdout.styledWrite(styleBright, s)
  182. of "bnw": stdout.styledWrite(styleUnderscore, styleBright, s)
  183. of "ack": stdout.styledWrite(styleUnderscore, fgYellow, s)
  184. of "gnu": stdout.styledWrite(styleUnderscore, fgGreen, s)
  185. type
  186. SearchInfo = tuple[buf: string, filename: string]
  187. MatchInfo = tuple[first: int, last: int;
  188. lineBeg: int, lineEnd: int, match: string]
  189. proc writeColored(s: string) =
  190. whenColors:
  191. case colorTheme
  192. of "simple": terminal.writeStyled(s, {styleUnderscore, styleBright})
  193. of "bnw": stdout.styledWrite(styleReverse, s)
  194. # Try styleReverse & bgDefault as a work-around against nasty feature
  195. # "Background color erase" (sticky background after line wraps):
  196. of "ack": stdout.styledWrite(styleReverse, fgYellow, bgDefault, s)
  197. of "gnu": stdout.styledWrite(fgRed, s)
  198. proc writeArrow(s: string) =
  199. whenColors:
  200. stdout.styledWrite(styleReverse, s)
  201. proc blockHeader(filename: string, line: int|string, replMode=false) =
  202. if replMode:
  203. writeArrow(" ->\n")
  204. elif newLine:
  205. if oneline:
  206. printBlockFile(filename)
  207. printBlockLineN(":" & $line & ":")
  208. else:
  209. printBlockLineN($line.`$`.align(alignment) & ":")
  210. stdout.write("\n")
  211. proc lineHeader(filename: string, line: int|string, isMatch: bool) =
  212. let lineSym =
  213. if isMatch: $line & ":"
  214. else: $line & " "
  215. if not newLine:
  216. if oneline:
  217. printFile(filename)
  218. printLineN(":" & lineSym, isMatch)
  219. else:
  220. printLineN(lineSym.align(alignment+1), isMatch)
  221. stdout.write(" ")
  222. proc printMatch(fileName: string, mi: MatchInfo) =
  223. let lines = mi.match.splitLines()
  224. for i, l in lines:
  225. if i > 0:
  226. lineHeader(filename, mi.lineBeg + i, isMatch = true)
  227. writeColored(l)
  228. if i < lines.len - 1:
  229. stdout.write("\n")
  230. proc printLinesBefore(si: SearchInfo, curMi: MatchInfo, nLines: int,
  231. replMode=false) =
  232. # start block: print 'linesBefore' lines before current match `curMi`
  233. let first = beforePattern(si.buf, curMi.first-1, nLines)
  234. let lines = splitLines(substr(si.buf, first, curMi.first-1))
  235. let startLine = curMi.lineBeg - lines.len + 1
  236. blockHeader(si.filename, curMi.lineBeg, replMode=replMode)
  237. for i, l in lines:
  238. lineHeader(si.filename, startLine + i, isMatch = (i == lines.len - 1))
  239. stdout.write(l)
  240. if i < lines.len - 1:
  241. stdout.write("\n")
  242. proc printLinesAfter(si: SearchInfo, mi: MatchInfo, nLines: int) =
  243. # finish block: print 'linesAfter' lines after match `mi`
  244. let s = si.buf
  245. let last = afterPattern(s, mi.last+1, nLines)
  246. let lines = splitLines(substr(s, mi.last+1, last))
  247. if lines.len == 0: # EOF
  248. stdout.write("\n")
  249. else:
  250. stdout.write(lines[0]) # complete the line after match itself
  251. stdout.write("\n")
  252. let skipLine = # workaround posix line ending at the end of file
  253. if last == s.len-1 and s.len >= 2 and s[^1] == '\l' and s[^2] != '\c': 1
  254. else: 0
  255. for i in 1 ..< lines.len - skipLine:
  256. lineHeader(si.filename, mi.lineEnd + i, isMatch = false)
  257. stdout.write(lines[i])
  258. stdout.write("\n")
  259. if linesAfter + linesBefore >= 2 and not newLine: stdout.write("\n")
  260. proc printBetweenMatches(si: SearchInfo, prevMi: MatchInfo, curMi: MatchInfo) =
  261. # continue block: print between `prevMi` and `curMi`
  262. let lines = si.buf.substr(prevMi.last+1, curMi.first-1).splitLines()
  263. stdout.write(lines[0]) # finish the line of previous Match
  264. if lines.len > 1:
  265. stdout.write("\n")
  266. for i in 1 ..< lines.len:
  267. lineHeader(si.filename, prevMi.lineEnd + i,
  268. isMatch = (i == lines.len - 1))
  269. stdout.write(lines[i])
  270. if i < lines.len - 1:
  271. stdout.write("\n")
  272. proc printContextBetween(si: SearchInfo, prevMi, curMi: MatchInfo) =
  273. # print context after previous match prevMi and before current match curMi
  274. let nLinesBetween = curMi.lineBeg - prevMi.lineEnd
  275. if nLinesBetween <= linesAfter + linesBefore + 1: # print as 1 block
  276. printBetweenMatches(si, prevMi, curMi)
  277. else: # finalize previous block and then print next block
  278. printLinesAfter(si, prevMi, 1+linesAfter)
  279. printLinesBefore(si, curMi, linesBefore+1)
  280. proc printReplacement(si: SearchInfo, mi: MatchInfo, repl: string,
  281. showRepl: bool, curPos: int,
  282. newBuf: string, curLine: int) =
  283. printLinesBefore(si, mi, linesBefore+1)
  284. printMatch(si.fileName, mi)
  285. printLinesAfter(si, mi, 1+linesAfter)
  286. stdout.flushFile()
  287. if showRepl:
  288. let newSi: SearchInfo = (buf: newBuf, filename: si.filename)
  289. let miForNewBuf: MatchInfo =
  290. (first: newBuf.len, last: newBuf.len,
  291. lineBeg: curLine, lineEnd: curLine, match: "")
  292. printLinesBefore(newSi, miForNewBuf, linesBefore+1, replMode=true)
  293. let replLines = countLineBreaks(repl, 0, repl.len-1)
  294. let miFixLines: MatchInfo =
  295. (first: mi.first, last: mi.last,
  296. lineBeg: curLine, lineEnd: curLine + replLines, match: repl)
  297. printMatch(si.fileName, miFixLines)
  298. printLinesAfter(si, miFixLines, 1+linesAfter)
  299. stdout.flushFile()
  300. proc doReplace(si: SearchInfo, mi: MatchInfo, i: int, r: string;
  301. newBuf: var string, curLine: var int, reallyReplace: var bool) =
  302. newBuf.add(si.buf.substr(i, mi.first-1))
  303. inc(curLine, countLineBreaks(si.buf, i, mi.first-1))
  304. if optConfirm in options:
  305. printReplacement(si, mi, r, showRepl=true, i, newBuf, curLine)
  306. case confirm()
  307. of ceAbort: quit(0)
  308. of ceYes: reallyReplace = true
  309. of ceAll:
  310. reallyReplace = true
  311. options.excl(optConfirm)
  312. of ceNo:
  313. reallyReplace = false
  314. of ceNone:
  315. reallyReplace = false
  316. options.excl(optConfirm)
  317. else:
  318. printReplacement(si, mi, r, showRepl=reallyReplace, i, newBuf, curLine)
  319. if reallyReplace:
  320. newBuf.add(r)
  321. inc(curLine, countLineBreaks(r, 0, r.len-1))
  322. else:
  323. newBuf.add(mi.match)
  324. inc(curLine, countLineBreaks(mi.match, 0, mi.match.len-1))
  325. proc processFile(pattern; filename: string; counter: var int, errors: var int) =
  326. var filenameShown = false
  327. template beforeHighlight =
  328. if not filenameShown and optVerbose notin options and not oneline:
  329. printBlockFile(filename)
  330. stdout.write("\n")
  331. stdout.flushFile()
  332. filenameShown = true
  333. var buffer: string
  334. if optFilenames in options:
  335. buffer = filename
  336. else:
  337. try:
  338. buffer = system.readFile(filename)
  339. except IOError:
  340. printError "Error: cannot open file: " & filename
  341. inc(errors)
  342. return
  343. if optVerbose in options:
  344. printFile(filename)
  345. stdout.write("\n")
  346. stdout.flushFile()
  347. var result: string
  348. if optReplace in options:
  349. result = newStringOfCap(buffer.len)
  350. var lineRepl = 1
  351. let si: SearchInfo = (buf: buffer, filename: filename)
  352. var prevMi, curMi: MatchInfo
  353. curMi.lineEnd = 1
  354. var i = 0
  355. var matches: array[0..re.MaxSubpatterns-1, string]
  356. for j in 0..high(matches): matches[j] = ""
  357. var reallyReplace = true
  358. while i < buffer.len:
  359. let t = findBounds(buffer, pattern, matches, i)
  360. if t.first < 0 or t.last < t.first:
  361. if optReplace notin options and prevMi.lineBeg != 0: # finalize last match
  362. printLinesAfter(si, prevMi, 1+linesAfter)
  363. stdout.flushFile()
  364. break
  365. let lineBeg = curMi.lineEnd + countLineBreaks(buffer, i, t.first-1)
  366. curMi = (first: t.first,
  367. last: t.last,
  368. lineBeg: lineBeg,
  369. lineEnd: lineBeg + countLineBreaks(buffer, t.first, t.last),
  370. match: buffer.substr(t.first, t.last))
  371. beforeHighlight()
  372. inc counter
  373. if optReplace notin options:
  374. if prevMi.lineBeg == 0: # no previous match, so no previous block to finalize
  375. printLinesBefore(si, curMi, linesBefore+1)
  376. else:
  377. printContextBetween(si, prevMi, curMi)
  378. printMatch(si.fileName, curMi)
  379. if t.last == buffer.len - 1:
  380. stdout.write("\n")
  381. stdout.flushFile()
  382. else:
  383. let r = replace(curMi.match, pattern, replacement % matches)
  384. doReplace(si, curMi, i, r, result, lineRepl, reallyReplace)
  385. i = t.last+1
  386. prevMi = curMi
  387. if optReplace in options:
  388. result.add(substr(buffer, i)) # finalize new buffer after last match
  389. var f: File
  390. if open(f, filename, fmWrite):
  391. f.write(result)
  392. f.close()
  393. else:
  394. quit "cannot open file for overwriting: " & filename
  395. proc hasRightFileName(path: string): bool =
  396. let filename = path.lastPathPart
  397. let ex = filename.splitFile.ext.substr(1) # skip leading '.'
  398. if extensions.len != 0:
  399. var matched = false
  400. for x in items(extensions):
  401. if os.cmpPaths(x, ex) == 0:
  402. matched = true
  403. break
  404. if not matched: return false
  405. for x in items(skipExtensions):
  406. if os.cmpPaths(x, ex) == 0: return false
  407. if includeFile.len != 0:
  408. var matched = false
  409. for x in items(includeFile):
  410. if filename.match(x):
  411. matched = true
  412. break
  413. if not matched: return false
  414. for x in items(excludeFile):
  415. if filename.match(x): return false
  416. result = true
  417. proc hasRightDirectory(path: string): bool =
  418. let dirname = path.lastPathPart
  419. for x in items(excludeDir):
  420. if dirname.match(x): return false
  421. result = true
  422. proc styleInsensitive(s: string): string =
  423. template addx =
  424. result.add(s[i])
  425. inc(i)
  426. result = ""
  427. var i = 0
  428. var brackets = 0
  429. while i < s.len:
  430. case s[i]
  431. of 'A'..'Z', 'a'..'z', '0'..'9':
  432. addx()
  433. if brackets == 0: result.add("_?")
  434. of '_':
  435. addx()
  436. result.add('?')
  437. of '[':
  438. addx()
  439. inc(brackets)
  440. of ']':
  441. addx()
  442. if brackets > 0: dec(brackets)
  443. of '?':
  444. addx()
  445. if s[i] == '<':
  446. addx()
  447. while s[i] != '>' and s[i] != '\0': addx()
  448. of '\\':
  449. addx()
  450. if s[i] in strutils.Digits:
  451. while s[i] in strutils.Digits: addx()
  452. else:
  453. addx()
  454. else: addx()
  455. proc walker(pattern; dir: string; counter: var int, errors: var int) =
  456. if existsDir(dir):
  457. for kind, path in walkDir(dir):
  458. case kind
  459. of pcFile:
  460. if path.hasRightFileName:
  461. processFile(pattern, path, counter, errors)
  462. of pcLinkToFile:
  463. if optFollow in options and path.hasRightFileName:
  464. processFile(pattern, path, counter, errors)
  465. of pcDir:
  466. if optRecursive in options and path.hasRightDirectory:
  467. walker(pattern, path, counter, errors)
  468. of pcLinkToDir:
  469. if optFollow in options and optRecursive in options and
  470. path.hasRightDirectory:
  471. walker(pattern, path, counter, errors)
  472. elif existsFile(dir):
  473. processFile(pattern, dir, counter, errors)
  474. else:
  475. printError "Error: no such file or directory: " & dir
  476. inc(errors)
  477. proc reportError(msg: string) =
  478. printError "Error: " & msg
  479. quit "Run nimgrep --help for the list of options"
  480. proc writeHelp() =
  481. stdout.write(Usage)
  482. stdout.flushFile()
  483. quit(0)
  484. proc writeVersion() =
  485. stdout.write(Version & "\n")
  486. stdout.flushFile()
  487. quit(0)
  488. proc checkOptions(subset: TOptions, a, b: string) =
  489. if subset <= options:
  490. quit("cannot specify both '$#' and '$#'" % [a, b])
  491. when defined(posix):
  492. useWriteStyled = terminal.isatty(stdout)
  493. # that should be before option processing to allow override of useWriteStyled
  494. for kind, key, val in getopt():
  495. case kind
  496. of cmdArgument:
  497. if options.contains(optStdin):
  498. filenames.add(key)
  499. elif pattern.len == 0:
  500. pattern = key
  501. elif options.contains(optReplace) and replacement.len == 0:
  502. replacement = key
  503. else:
  504. filenames.add(key)
  505. of cmdLongOption, cmdShortOption:
  506. case normalize(key)
  507. of "find", "f": incl(options, optFind)
  508. of "replace", "!": incl(options, optReplace)
  509. of "peg":
  510. excl(options, optRegex)
  511. incl(options, optPeg)
  512. of "re":
  513. incl(options, optRegex)
  514. excl(options, optPeg)
  515. of "rex", "x":
  516. incl(options, optRex)
  517. incl(options, optRegex)
  518. excl(options, optPeg)
  519. of "recursive", "r": incl(options, optRecursive)
  520. of "follow": incl(options, optFollow)
  521. of "confirm": incl(options, optConfirm)
  522. of "stdin": incl(options, optStdin)
  523. of "word", "w": incl(options, optWord)
  524. of "ignorecase", "i": incl(options, optIgnoreCase)
  525. of "ignorestyle", "y": incl(options, optIgnoreStyle)
  526. of "ext": extensions.add val.split('|')
  527. of "noext": skipExtensions.add val.split('|')
  528. of "excludedir", "exclude-dir": excludeDir.add rex(val)
  529. of "includefile", "include-file": includeFile.add rex(val)
  530. of "excludefile", "exclude-file": excludeFile.add rex(val)
  531. of "nocolor": useWriteStyled = false
  532. of "color":
  533. case val
  534. of "auto": discard
  535. of "never", "false": useWriteStyled = false
  536. of "", "always", "true": useWriteStyled = true
  537. else: reportError("invalid value '" & val & "' for --color")
  538. of "colortheme":
  539. colortheme = normalize(val)
  540. if colortheme notin ["simple", "bnw", "ack", "gnu"]:
  541. reportError("unknown colortheme '" & val & "'")
  542. of "beforecontext", "before-context", "b":
  543. try:
  544. linesBefore = parseInt(val)
  545. except ValueError:
  546. reportError("option " & key & " requires an integer but '" &
  547. val & "' was given")
  548. of "aftercontext", "after-context", "a":
  549. try:
  550. linesAfter = parseInt(val)
  551. except ValueError:
  552. reportError("option " & key & " requires an integer but '" &
  553. val & "' was given")
  554. of "context", "c":
  555. try:
  556. linesContext = parseInt(val)
  557. except ValueError:
  558. reportError("option --context requires an integer but '" &
  559. val & "' was given")
  560. of "newline", "l": newLine = true
  561. of "oneline": oneline = true
  562. of "group", "g": oneline = false
  563. of "verbose": incl(options, optVerbose)
  564. of "filenames": incl(options, optFilenames)
  565. of "help", "h": writeHelp()
  566. of "version", "v": writeVersion()
  567. else: reportError("unrecognized option '" & key & "'")
  568. of cmdEnd: assert(false) # cannot happen
  569. checkOptions({optFind, optReplace}, "find", "replace")
  570. checkOptions({optPeg, optRegex}, "peg", "re")
  571. checkOptions({optIgnoreCase, optIgnoreStyle}, "ignore_case", "ignore_style")
  572. checkOptions({optFilenames, optReplace}, "filenames", "replace")
  573. linesBefore = max(linesBefore, linesContext)
  574. linesAfter = max(linesAfter, linesContext)
  575. if optStdin in options:
  576. pattern = ask("pattern [ENTER to exit]: ")
  577. if pattern.len == 0: quit(0)
  578. if optReplace in options:
  579. replacement = ask("replacement [supports $1, $# notations]: ")
  580. if pattern.len == 0:
  581. reportError("empty pattern was given")
  582. else:
  583. var counter = 0
  584. var errors = 0
  585. if filenames.len == 0:
  586. filenames.add(os.getCurrentDir())
  587. if optRegex notin options:
  588. if optWord in options:
  589. pattern = r"(^ / !\letter)(" & pattern & r") !\letter"
  590. if optIgnoreStyle in options:
  591. pattern = "\\y " & pattern
  592. elif optIgnoreCase in options:
  593. pattern = "\\i " & pattern
  594. let pegp = peg(pattern)
  595. for f in items(filenames):
  596. walker(pegp, f, counter, errors)
  597. else:
  598. var reflags = {reStudy}
  599. if optIgnoreStyle in options:
  600. pattern = styleInsensitive(pattern)
  601. if optWord in options:
  602. # see https://github.com/nim-lang/Nim/issues/13528#issuecomment-592786443
  603. pattern = r"(^|\W)(:?" & pattern & r")($|\W)"
  604. if {optIgnoreCase, optIgnoreStyle} * options != {}:
  605. reflags.incl reIgnoreCase
  606. let rep = if optRex in options: rex(pattern, reflags)
  607. else: re(pattern, reflags)
  608. for f in items(filenames):
  609. walker(rep, f, counter, errors)
  610. if errors != 0:
  611. printError $errors & " errors"
  612. stdout.write($counter & " matches\n")
  613. if errors != 0:
  614. quit(1)