nimgrep.nim 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435
  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, osproc, tables, algorithm, times
  11. const
  12. Version = "2.0.0"
  13. Usage = "nimgrep - Nim Grep Searching and Replacement Utility Version " &
  14. Version & """
  15. (c) 2012-2020 Andreas Rumpf
  16. """ & slurp "../doc/nimgrep_cmdline.txt"
  17. # Limitations / ideas / TODO:
  18. # * No unicode support with --cols
  19. # * Consider making --onlyAscii default, since dumping binary data has
  20. # stability and security repercussions
  21. # * Mode - reads entire buffer by whole from stdin, which is bad for streaming.
  22. # To implement line-by-line reading after adding option to turn off
  23. # multiline matches
  24. # * Add some form of file pre-processing, e.g. feed binary files to utility
  25. # `strings` and then do the search inside these strings
  26. # * Add --showCol option to also show column (of match), not just line; it
  27. # makes it easier when jump to line+col in an editor or on terminal
  28. # Search results for a file are modelled by these levels:
  29. # FileResult -> Block -> Output/Chunk -> SubLine
  30. #
  31. # 1. SubLine is an entire line or its part.
  32. #
  33. # 2. Chunk, which is a sequence of SubLine, represents a match and its
  34. # surrounding context.
  35. # Output is a Chunk or one of auxiliary results like an openError.
  36. #
  37. # 3. Block, which is a sequence of Chunks, is not present as a separate type.
  38. # It will just be separated from another Block by newline when there is
  39. # more than 3 lines in it.
  40. # Here is an example of a Block where only 1 match is found and
  41. # 1 line before and 1 line after of context are required:
  42. #
  43. # ...a_line_before...................................... <<<SubLine(Chunk 1)
  44. #
  45. # .......pre....... ....new_match.... .......post......
  46. # ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
  47. # SubLine (Chunk 1) SubLine (Chunk 1) SubLine (Chunk 2)
  48. #
  49. # ...a_line_after....................................... <<<SubLine(Chunk 2)
  50. #
  51. # 4. FileResult is printed as a sequence of Blocks.
  52. # However FileResult is represented as seq[Output] in the program.
  53. type
  54. TOption = enum
  55. optFind, optReplace, optPeg, optRegex, optRecursive, optConfirm, optStdin,
  56. optWord, optIgnoreCase, optIgnoreStyle, optVerbose, optFilenames,
  57. optRex, optFollow, optCount, optLimitChars, optPipe
  58. TOptions = set[TOption]
  59. TConfirmEnum = enum
  60. ceAbort, ceYes, ceAll, ceNo, ceNone
  61. Bin = enum
  62. biOn, biOnly, biOff
  63. Pattern = Regex | Peg
  64. MatchInfo = tuple[first: int, last: int;
  65. lineBeg: int, lineEnd: int, match: string]
  66. outputKind = enum
  67. openError, rejected, justCount,
  68. blockFirstMatch, blockNextMatch, blockEnd, fileContents, outputFileName
  69. Output = object
  70. case kind: outputKind
  71. of openError: msg: string # file/directory not found
  72. of rejected: reason: string # when the file contents do not pass
  73. of justCount: matches: int # the only output for option --count
  74. of blockFirstMatch, blockNextMatch: # the normal case: match itself
  75. pre: string
  76. match: MatchInfo
  77. of blockEnd: # block ending right after prev. match
  78. blockEnding: string
  79. firstLine: int
  80. # == last lineN of last match
  81. of fileContents: # yielded for --replace only
  82. buffer: string
  83. of outputFileName: # yielded for --filenames when no
  84. name: string # PATTERN was provided
  85. Trequest = (int, string)
  86. FileResult = seq[Output]
  87. Tresult = tuple[finished: bool, fileNo: int,
  88. filename: string, fileResult: FileResult]
  89. WalkOpt = tuple # used for walking directories/producing paths
  90. extensions: seq[string]
  91. notExtensions: seq[string]
  92. filename: seq[string]
  93. notFilename: seq[string]
  94. dirPath: seq[string]
  95. notDirPath: seq[string]
  96. dirname : seq[string]
  97. notDirname : seq[string]
  98. WalkOptComp[Pat] = tuple # a compiled version of the previous
  99. filename: seq[Pat]
  100. notFilename: seq[Pat]
  101. dirname : seq[Pat]
  102. notDirname : seq[Pat]
  103. dirPath: seq[Pat]
  104. notDirPath: seq[Pat]
  105. SearchOpt = tuple # used for searching inside a file
  106. patternSet: bool # To distinguish uninitialized/empty 'pattern'
  107. pattern: string # Main PATTERN
  108. inFile: seq[string] # --inFile, --inf
  109. notInFile: seq[string] # --notinFile, --ninf
  110. inContext: seq[string] # --inContext, --inc
  111. notInContext: seq[string] # --notinContext, --ninc
  112. checkBin: Bin # --bin, --text
  113. SearchOptComp[Pat] = tuple # a compiled version of the previous
  114. pattern: Pat
  115. inFile: seq[Pat]
  116. notInFile: seq[Pat]
  117. inContext: seq[Pat]
  118. notInContext: seq[Pat]
  119. SinglePattern[PAT] = tuple # compile single pattern for replacef
  120. pattern: PAT
  121. Column = tuple # current column info for the cropping (--limit) feature
  122. terminal: int # column in terminal emulator
  123. file: int # column in file (for correct Tab processing)
  124. overflowMatches: int
  125. var
  126. paths: seq[string] = @[]
  127. replacement = ""
  128. replacementSet = false
  129. # to distinguish between uninitialized 'replacement' and empty one
  130. options: TOptions = {optRegex}
  131. walkOpt {.threadvar.}: WalkOpt
  132. searchOpt {.threadvar.}: SearchOpt
  133. sortTime = false
  134. sortTimeOrder = SortOrder.Ascending
  135. useWriteStyled = true
  136. oneline = true # turned off by --group
  137. expandTabs = true # Tabs are expanded in oneline mode
  138. linesBefore = 0
  139. linesAfter = 0
  140. linesContext = 0
  141. newLine = false
  142. gVar = (matches: 0, errors: 0, reallyReplace: true)
  143. # gVar - variables that can change during search/replace
  144. nWorkers = 0 # run in single thread by default
  145. searchRequestsChan: Channel[Trequest]
  146. resultsChan: Channel[Tresult]
  147. colorTheme: string = "simple"
  148. limitCharUsr = high(int) # don't limit line width by default
  149. termWidth = 80
  150. optOnlyAscii = false
  151. searchOpt.checkBin = biOn
  152. proc ask(msg: string): string =
  153. stdout.write(msg)
  154. stdout.flushFile()
  155. result = stdin.readLine()
  156. proc confirm: TConfirmEnum =
  157. while true:
  158. case normalize(ask(" [a]bort; [y]es, a[l]l, [n]o, non[e]: "))
  159. of "a", "abort": return ceAbort
  160. of "y", "yes": return ceYes
  161. of "l", "all": return ceAll
  162. of "n", "no": return ceNo
  163. of "e", "none": return ceNone
  164. else: discard
  165. func countLineBreaks(s: string, first, last: int): int =
  166. # count line breaks (unlike strutils.countLines starts count from 0)
  167. var i = first
  168. while i <= last:
  169. if s[i] == '\c':
  170. inc result
  171. if i < last and s[i+1] == '\l': inc(i)
  172. elif s[i] == '\l':
  173. inc result
  174. inc i
  175. func beforePattern(s: string, pos: int, nLines = 1): int =
  176. var linesLeft = nLines
  177. result = min(pos, s.len-1)
  178. while true:
  179. while result >= 0 and s[result] notin {'\c', '\l'}: dec(result)
  180. if result == -1: break
  181. if s[result] == '\l':
  182. dec(linesLeft)
  183. if linesLeft == 0: break
  184. dec(result)
  185. if result >= 0 and s[result] == '\c': dec(result)
  186. else: # '\c'
  187. dec(linesLeft)
  188. if linesLeft == 0: break
  189. dec(result)
  190. inc(result)
  191. proc afterPattern(s: string, pos: int, nLines = 1): int =
  192. result = max(0, pos)
  193. var linesScanned = 0
  194. while true:
  195. while result < s.len and s[result] notin {'\c', '\l'}: inc(result)
  196. inc(linesScanned)
  197. if linesScanned == nLines: break
  198. if result < s.len:
  199. if s[result] == '\l':
  200. inc(result)
  201. elif s[result] == '\c':
  202. inc(result)
  203. if result < s.len and s[result] == '\l': inc(result)
  204. else: break
  205. dec(result)
  206. template whenColors(body: untyped) =
  207. if useWriteStyled:
  208. body
  209. else:
  210. stdout.write(s)
  211. proc printFile(s: string) =
  212. whenColors:
  213. case colorTheme
  214. of "simple": stdout.write(s)
  215. of "bnw": stdout.styledWrite(styleUnderscore, s)
  216. of "ack": stdout.styledWrite(fgGreen, s)
  217. of "gnu": stdout.styledWrite(fgMagenta, s)
  218. proc printBlockFile(s: string) =
  219. whenColors:
  220. case colorTheme
  221. of "simple": stdout.styledWrite(styleBright, s)
  222. of "bnw": stdout.styledWrite(styleUnderscore, s)
  223. of "ack": stdout.styledWrite(styleUnderscore, fgGreen, s)
  224. of "gnu": stdout.styledWrite(styleUnderscore, fgMagenta, s)
  225. proc printBold(s: string) =
  226. whenColors:
  227. stdout.styledWrite(styleBright, s)
  228. proc printSpecial(s: string) =
  229. whenColors:
  230. case colorTheme
  231. of "simple", "bnw":
  232. stdout.styledWrite(if s == " ": styleReverse else: styleBright, s)
  233. of "ack", "gnu": stdout.styledWrite(styleReverse, fgBlue, bgDefault, s)
  234. proc printError(s: string) =
  235. whenColors:
  236. case colorTheme
  237. of "simple", "bnw": stdout.styledWriteLine(styleBright, s)
  238. of "ack", "gnu": stdout.styledWriteLine(styleReverse, fgRed, bgDefault, s)
  239. stdout.flushFile()
  240. proc printLineN(s: string, isMatch: bool) =
  241. whenColors:
  242. case colorTheme
  243. of "simple": stdout.write(s)
  244. of "bnw":
  245. if isMatch: stdout.styledWrite(styleBright, s)
  246. else: stdout.styledWrite(s)
  247. of "ack":
  248. if isMatch: stdout.styledWrite(fgYellow, s)
  249. else: stdout.styledWrite(fgGreen, s)
  250. of "gnu":
  251. if isMatch: stdout.styledWrite(fgGreen, s)
  252. else: stdout.styledWrite(fgCyan, s)
  253. proc printBlockLineN(s: string) =
  254. whenColors:
  255. case colorTheme
  256. of "simple": stdout.styledWrite(styleBright, s)
  257. of "bnw": stdout.styledWrite(styleUnderscore, styleBright, s)
  258. of "ack": stdout.styledWrite(styleUnderscore, fgYellow, s)
  259. of "gnu": stdout.styledWrite(styleUnderscore, fgGreen, s)
  260. proc writeColored(s: string) =
  261. whenColors:
  262. case colorTheme
  263. of "simple": terminal.writeStyled(s, {styleUnderscore, styleBright})
  264. of "bnw": stdout.styledWrite(styleReverse, s)
  265. # Try styleReverse & bgDefault as a work-around against nasty feature
  266. # "Background color erase" (sticky background after line wraps):
  267. of "ack": stdout.styledWrite(styleReverse, fgYellow, bgDefault, s)
  268. of "gnu": stdout.styledWrite(fgRed, s)
  269. proc printContents(s: string, isMatch: bool) =
  270. if isMatch:
  271. writeColored(s)
  272. else:
  273. stdout.write(s)
  274. proc writeArrow(s: string) =
  275. whenColors:
  276. stdout.styledWrite(styleReverse, s)
  277. const alignment = 6 # selected so that file contents start at 8, i.e.
  278. # Tabs expand correctly without additional care
  279. proc blockHeader(filename: string, line: int|string, replMode=false) =
  280. if replMode:
  281. writeArrow(" ->\n")
  282. elif newLine and optFilenames notin options and optPipe notin options:
  283. if oneline:
  284. printBlockFile(filename)
  285. printBlockLineN(":" & $line & ":")
  286. else:
  287. printBlockLineN($line.`$`.align(alignment) & ":")
  288. stdout.write("\n")
  289. proc newLn(curCol: var Column) =
  290. stdout.write("\n")
  291. curCol.file = 0
  292. curCol.terminal = 0
  293. # We reserve 10+3 chars on the right in --cols mode (optLimitChars).
  294. # If the current match touches this right margin, subLine before it will
  295. # be cropped (even if space is enough for subLine after the match — we
  296. # currently don't have a way to know it since we get it afterwards).
  297. const matchPaddingFromRight = 10
  298. const ellipsis = "..."
  299. proc lineHeader(filename: string, line: int|string, isMatch: bool,
  300. curCol: var Column) =
  301. let lineSym =
  302. if isMatch: $line & ":"
  303. else: $line & " "
  304. if not newLine and optFilenames notin options and optPipe notin options:
  305. if oneline:
  306. printFile(filename)
  307. printLineN(":" & lineSym, isMatch)
  308. curcol.terminal += filename.len + 1 + lineSym.len
  309. else:
  310. printLineN(lineSym.align(alignment+1), isMatch)
  311. curcol.terminal += lineSym.align(alignment+1).len
  312. stdout.write(" "); curCol.terminal += 1
  313. curCol.terminal = curCol.terminal mod termWidth
  314. if optLimitChars in options and
  315. curCol.terminal > limitCharUsr - matchPaddingFromRight - ellipsis.len:
  316. newLn(curCol)
  317. proc reserveChars(mi: MatchInfo): int =
  318. if optLimitChars in options:
  319. let patternChars = afterPattern(mi.match, 0) + 1
  320. result = patternChars + ellipsis.len + matchPaddingFromRight
  321. else:
  322. result = 0
  323. # Our substitutions of non-printable symbol to ASCII character are similar to
  324. # those of programm 'less'.
  325. const lowestAscii = 0x20 # lowest ASCII Latin printable symbol (@)
  326. const largestAscii = 0x7e
  327. const by2ascii = 2 # number of ASCII chars to represent chars < lowestAscii
  328. const by3ascii = 3 # number of ASCII chars to represent chars > largestAscii
  329. proc printExpanded(s: string, curCol: var Column, isMatch: bool,
  330. limitChar: int) =
  331. # Print taking into account tabs and optOnlyAscii (and also optLimitChar:
  332. # the proc called from printCropped but we need to check column < limitChar
  333. # also here, since exact cut points are known only after tab expansion).
  334. # With optOnlyAscii non-ascii chars are highlighted even in matches.
  335. #
  336. # use buffer because:
  337. # 1) we need to print non-ascii character inside matches while keeping the
  338. # amount of color escape sequences minimal.
  339. # 2) there is a report that fwrite buffering is slow on MacOS
  340. # https://github.com/nim-lang/Nim/pull/15612#discussion_r510538326
  341. const bufSize = 8192 # typical for fwrite too
  342. var buffer: string
  343. const normal = 0
  344. const special = 1
  345. var lastAdded = normal
  346. template dumpBuf() =
  347. if lastAdded == normal:
  348. printContents(buffer, isMatch)
  349. else:
  350. printSpecial(buffer)
  351. template addBuf(i: int, s: char|string, size: int) =
  352. if lastAdded != i or buffer.len + size > bufSize:
  353. dumpBuf()
  354. buffer.setlen(0)
  355. buffer.add s
  356. lastAdded = i
  357. for c in s:
  358. let charsAllowed = limitChar - curCol.terminal
  359. if charsAllowed <= 0:
  360. break
  361. if lowestAscii <= int(c) and int(c) <= largestAscii: # ASCII latin
  362. addBuf(normal, c, 1)
  363. curCol.file += 1; curCol.terminal += 1
  364. elif (not optOnlyAscii) and c != '\t': # the same, print raw
  365. addBuf(normal, c, 1)
  366. curCol.file += 1; curCol.terminal += 1
  367. elif c == '\t':
  368. let spaces = 8 - (curCol.file mod 8)
  369. let spacesAllowed = min(spaces, charsAllowed)
  370. curCol.file += spaces
  371. curCol.terminal += spacesAllowed
  372. if expandTabs:
  373. if optOnlyAscii: # print a nice box for tab
  374. addBuf(special, " ", 1)
  375. addBuf(normal, " ".repeat(spacesAllowed-1), spacesAllowed-1)
  376. else:
  377. addBuf(normal, " ".repeat(spacesAllowed), spacesAllowed)
  378. else:
  379. addBuf(normal, '\t', 1)
  380. else: # substitute characters that are not ACSII Latin
  381. if int(c) < lowestAscii:
  382. let substitute = char(int(c) + 0x40) # use common "control codes"
  383. addBuf(special, "^" & substitute, by2ascii)
  384. curCol.terminal += by2ascii
  385. else: # int(c) > largestAscii
  386. curCol.terminal += by3ascii
  387. let substitute = '\'' & c.BiggestUInt.toHex(2)
  388. addBuf(special, substitute, by3ascii)
  389. curCol.file += 1
  390. if buffer.len > 0:
  391. dumpBuf()
  392. template nextCharacter(c: char, file: var int, term: var int) =
  393. if lowestAscii <= int(c) and int(c) <= largestAscii: # ASCII latin
  394. file += 1
  395. term += 1
  396. elif (not optOnlyAscii) and c != '\t': # the same, print raw
  397. file += 1
  398. term += 1
  399. elif c == '\t':
  400. term += 8 - (file mod 8)
  401. file += 8 - (file mod 8)
  402. elif int(c) < lowestAscii:
  403. file += 1
  404. term += by2ascii
  405. else: # int(c) > largestAscii:
  406. file += 1
  407. term += by3ascii
  408. proc calcTermLen(s: string, firstCol: int, chars: int, fromLeft: bool): int =
  409. # calculate additional length added by Tabs expansion and substitutions
  410. var col = firstCol
  411. var first, last: int
  412. if fromLeft:
  413. first = max(0, s.len - chars)
  414. last = s.len - 1
  415. else:
  416. first = 0
  417. last = min(s.len - 1, chars - 1)
  418. for c in s[first .. last]:
  419. nextCharacter(c, col, result)
  420. proc printCropped(s: string, curCol: var Column, fromLeft: bool,
  421. limitChar: int, isMatch = false) =
  422. # print line `s`, may be cropped if option --cols was set
  423. const eL = ellipsis.len
  424. if optLimitChars notin options:
  425. if not expandTabs and not optOnlyAscii: # for speed mostly
  426. printContents(s, isMatch)
  427. else:
  428. printExpanded(s, curCol, isMatch, limitChar)
  429. else: # limit columns, expand Tabs is also forced
  430. var charsAllowed = limitChar - curCol.terminal
  431. if fromLeft and charsAllowed < eL:
  432. charsAllowed = eL
  433. if (not fromLeft) and charsAllowed <= 0:
  434. # already overflown and ellipsis shold be in place
  435. return
  436. let fullLenWithin = calcTermLen(s, curCol.file, charsAllowed, fromLeft)
  437. # additional length from Tabs and special symbols
  438. let addLen = fullLenWithin - min(s.len, charsAllowed)
  439. # determine that the string is guaranteed to fit within `charsAllowed`
  440. let fits =
  441. if s.len > charsAllowed:
  442. false
  443. else:
  444. if isMatch: fullLenWithin <= charsAllowed - eL
  445. else: fullLenWithin <= charsAllowed
  446. if fits:
  447. printExpanded(s, curCol, isMatch, limitChar = high(int))
  448. else:
  449. if fromLeft:
  450. printBold ellipsis
  451. curCol.terminal += eL
  452. # find position `pos` where the right side of line will fit charsAllowed
  453. var col = 0
  454. var term = 0
  455. var pos = min(s.len, max(0, s.len - (charsAllowed - eL)))
  456. while pos <= s.len - 1:
  457. let c = s[pos]
  458. nextCharacter(c, col, term)
  459. if term >= addLen:
  460. break
  461. inc pos
  462. curCol.file = pos
  463. # TODO don't expand tabs when cropped from the left - difficult, meaningless
  464. printExpanded(s[pos .. s.len - 1], curCol, isMatch,
  465. limitChar = high(int))
  466. else:
  467. let last = max(-1, min(s.len - 1, charsAllowed - eL - 1))
  468. printExpanded(s[0 .. last], curCol, isMatch, limitChar-eL)
  469. let numDots = limitChar - curCol.terminal
  470. printBold ".".repeat(numDots)
  471. curCol.terminal = limitChar
  472. proc printMatch(fileName: string, mi: MatchInfo, curCol: var Column) =
  473. let sLines = mi.match.splitLines()
  474. for i, l in sLines:
  475. if i > 0:
  476. lineHeader(filename, mi.lineBeg + i, isMatch = true, curCol)
  477. let charsAllowed = limitCharUsr - curCol.terminal
  478. if charsAllowed > 0:
  479. printCropped(l, curCol, fromLeft = false, limitCharUsr, isMatch = true)
  480. else:
  481. curCol.overflowMatches += 1
  482. if i < sLines.len - 1:
  483. newLn(curCol)
  484. proc getSubLinesBefore(buf: string, curMi: MatchInfo): string =
  485. let first = beforePattern(buf, curMi.first-1, linesBefore+1)
  486. result = substr(buf, first, curMi.first-1)
  487. proc printSubLinesBefore(filename: string, beforeMatch: string, lineBeg: int,
  488. curCol: var Column, reserveChars: int,
  489. replMode=false) =
  490. # start block: print 'linesBefore' lines before current match `curMi`
  491. let sLines = splitLines(beforeMatch)
  492. let startLine = lineBeg - sLines.len + 1
  493. blockHeader(filename, lineBeg, replMode=replMode)
  494. for i, l in sLines:
  495. let isLastLine = i == sLines.len - 1
  496. lineHeader(filename, startLine + i, isMatch = isLastLine, curCol)
  497. let limit = if isLastLine: limitCharUsr - reserveChars else: limitCharUsr
  498. l.printCropped(curCol, fromLeft = isLastLine, limitChar = limit)
  499. if not isLastLine:
  500. newLn(curCol)
  501. proc getSubLinesAfter(buf: string, mi: MatchInfo): string =
  502. let last = afterPattern(buf, mi.last+1, 1+linesAfter)
  503. let skipByte = # workaround posix: suppress extra line at the end of file
  504. if (last == buf.len-1 and buf.len >= 2 and
  505. buf[^1] == '\l' and buf[^2] != '\c'): 1
  506. else: 0
  507. result = substr(buf, mi.last+1, last - skipByte)
  508. proc printOverflow(filename: string, line: int, curCol: var Column) =
  509. if curCol.overflowMatches > 0:
  510. lineHeader(filename, line, isMatch = true, curCol)
  511. printBold("(" & $curCol.overflowMatches & " matches skipped)")
  512. newLn(curCol)
  513. curCol.overflowMatches = 0
  514. proc printSubLinesAfter(filename: string, afterMatch: string, matchLineEnd: int,
  515. curCol: var Column) =
  516. # finish block: print 'linesAfter' lines after match `mi`
  517. let sLines = splitLines(afterMatch)
  518. if sLines.len == 0: # EOF
  519. newLn(curCol)
  520. else:
  521. sLines[0].printCropped(curCol, fromLeft = false, limitCharUsr)
  522. # complete the line after the match itself
  523. newLn(curCol)
  524. printOverflow(filename, matchLineEnd, curCol)
  525. for i in 1 ..< sLines.len:
  526. lineHeader(filename, matchLineEnd + i, isMatch = false, curCol)
  527. sLines[i].printCropped(curCol, fromLeft = false, limitCharUsr)
  528. newLn(curCol)
  529. proc getSubLinesBetween(buf: string, prevMi: MatchInfo,
  530. curMi: MatchInfo): string =
  531. buf.substr(prevMi.last+1, curMi.first-1)
  532. proc printBetweenMatches(filename: string, betweenMatches: string,
  533. lastLineBeg: int,
  534. curCol: var Column, reserveChars: int) =
  535. # continue block: print between `prevMi` and `curMi`
  536. let sLines = betweenMatches.splitLines()
  537. sLines[0].printCropped(curCol, fromLeft = false, limitCharUsr)
  538. # finish the line of previous Match
  539. if sLines.len > 1:
  540. newLn(curCol)
  541. printOverflow(filename, lastLineBeg - sLines.len + 1, curCol)
  542. for i in 1 ..< sLines.len:
  543. let isLastLine = i == sLines.len - 1
  544. lineHeader(filename, lastLineBeg - sLines.len + i + 1,
  545. isMatch = isLastLine, curCol)
  546. let limit = if isLastLine: limitCharUsr - reserveChars else: limitCharUsr
  547. sLines[i].printCropped(curCol, fromLeft = isLastLine, limitChar = limit)
  548. if not isLastLine:
  549. newLn(curCol)
  550. proc printReplacement(fileName: string, buf: string, mi: MatchInfo,
  551. repl: string, showRepl: bool, curPos: int,
  552. newBuf: string, curLine: int) =
  553. var curCol: Column
  554. printSubLinesBefore(fileName, getSubLinesBefore(buf, mi), mi.lineBeg,
  555. curCol, reserveChars(mi))
  556. printMatch(fileName, mi, curCol)
  557. printSubLinesAfter(fileName, getSubLinesAfter(buf, mi), mi.lineEnd, curCol)
  558. stdout.flushFile()
  559. if showRepl:
  560. let miForNewBuf: MatchInfo =
  561. (first: newBuf.len, last: newBuf.len,
  562. lineBeg: curLine, lineEnd: curLine, match: "")
  563. printSubLinesBefore(fileName, getSubLinesBefore(newBuf, miForNewBuf),
  564. miForNewBuf.lineBeg, curCol, reserveChars(miForNewBuf),
  565. replMode=true)
  566. let replLines = countLineBreaks(repl, 0, repl.len-1)
  567. let miFixLines: MatchInfo =
  568. (first: mi.first, last: mi.last,
  569. lineBeg: curLine, lineEnd: curLine + replLines, match: repl)
  570. printMatch(fileName, miFixLines, curCol)
  571. printSubLinesAfter(fileName, getSubLinesAfter(buf, miFixLines),
  572. miFixLines.lineEnd, curCol)
  573. if linesAfter + linesBefore >= 2 and not newLine: stdout.write("\n")
  574. stdout.flushFile()
  575. proc replace1match(filename: string, buf: string, mi: MatchInfo, i: int,
  576. r: string; newBuf: var string, curLine: var int): bool =
  577. newBuf.add(buf.substr(i, mi.first-1))
  578. inc(curLine, countLineBreaks(buf, i, mi.first-1))
  579. if optConfirm in options:
  580. printReplacement(filename, buf, mi, r, showRepl=true, i, newBuf, curLine)
  581. case confirm()
  582. of ceAbort: quit(0)
  583. of ceYes: gVar.reallyReplace = true
  584. of ceAll:
  585. gVar.reallyReplace = true
  586. options.excl(optConfirm)
  587. of ceNo:
  588. gVar.reallyReplace = false
  589. of ceNone:
  590. gVar.reallyReplace = false
  591. options.excl(optConfirm)
  592. elif optPipe notin options:
  593. printReplacement(filename, buf, mi, r, showRepl=gVar.reallyReplace, i,
  594. newBuf, curLine)
  595. if gVar.reallyReplace:
  596. result = true
  597. newBuf.add(r)
  598. inc(curLine, countLineBreaks(r, 0, r.len-1))
  599. else:
  600. newBuf.add(mi.match)
  601. inc(curLine, countLineBreaks(mi.match, 0, mi.match.len-1))
  602. template updateCounters(output: Output) =
  603. case output.kind
  604. of blockFirstMatch, blockNextMatch: inc(gVar.matches)
  605. of justCount: inc(gVar.matches, output.matches)
  606. of openError: inc(gVar.errors)
  607. of rejected, blockEnd, fileContents, outputFileName: discard
  608. proc printInfo(filename:string, output: Output) =
  609. case output.kind
  610. of openError:
  611. printError("cannot open path '" & filename & "': " & output.msg)
  612. of rejected:
  613. if optVerbose in options:
  614. echo "(rejected: ", output.reason, ")"
  615. of justCount:
  616. echo " (" & $output.matches & " matches)"
  617. of blockFirstMatch, blockNextMatch, blockEnd, fileContents, outputFileName:
  618. discard
  619. proc printOutput(filename: string, output: Output, curCol: var Column) =
  620. case output.kind
  621. of openError, rejected, justCount: printInfo(filename, output)
  622. of fileContents: discard # impossible
  623. of outputFileName:
  624. printCropped(output.name, curCol, fromLeft=false, limitCharUsr)
  625. newLn(curCol)
  626. of blockFirstMatch:
  627. printSubLinesBefore(filename, output.pre, output.match.lineBeg,
  628. curCol, reserveChars(output.match))
  629. printMatch(filename, output.match, curCol)
  630. of blockNextMatch:
  631. printBetweenMatches(filename, output.pre, output.match.lineBeg,
  632. curCol, reserveChars(output.match))
  633. printMatch(filename, output.match, curCol)
  634. of blockEnd:
  635. printSubLinesAfter(filename, output.blockEnding, output.firstLine, curCol)
  636. if linesAfter + linesBefore >= 2 and not newLine and
  637. optFilenames notin options: stdout.write("\n")
  638. iterator searchFile(pattern: Pattern; buffer: string): Output =
  639. var prevMi, curMi: MatchInfo
  640. prevMi.lineEnd = 1
  641. var i = 0
  642. var matches: array[0..re.MaxSubpatterns-1, string]
  643. for j in 0..high(matches): matches[j] = ""
  644. while true:
  645. let t = findBounds(buffer, pattern, matches, i)
  646. if t.first < 0 or t.last < t.first:
  647. if prevMi.lineBeg != 0: # finalize last match
  648. yield Output(kind: blockEnd,
  649. blockEnding: getSubLinesAfter(buffer, prevMi),
  650. firstLine: prevMi.lineEnd)
  651. break
  652. let lineBeg = prevMi.lineEnd + countLineBreaks(buffer, i, t.first-1)
  653. curMi = (first: t.first,
  654. last: t.last,
  655. lineBeg: lineBeg,
  656. lineEnd: lineBeg + countLineBreaks(buffer, t.first, t.last),
  657. match: buffer.substr(t.first, t.last))
  658. if prevMi.lineBeg == 0: # no prev. match, so no prev. block to finalize
  659. let pre = getSubLinesBefore(buffer, curMi)
  660. prevMi = curMi
  661. yield Output(kind: blockFirstMatch, pre: pre, match: move(curMi))
  662. else:
  663. let nLinesBetween = curMi.lineBeg - prevMi.lineEnd
  664. if nLinesBetween <= linesAfter + linesBefore + 1: # print as 1 block
  665. let pre = getSubLinesBetween(buffer, prevMi, curMi)
  666. prevMi = curMi
  667. yield Output(kind: blockNextMatch, pre: pre, match: move(curMi))
  668. else: # finalize previous block and then print next block
  669. let after = getSubLinesAfter(buffer, prevMi)
  670. yield Output(kind: blockEnd, blockEnding: after,
  671. firstLine: prevMi.lineEnd)
  672. let pre = getSubLinesBefore(buffer, curMi)
  673. prevMi = curMi
  674. yield Output(kind: blockFirstMatch,
  675. pre: pre,
  676. match: move(curMi))
  677. i = t.last+1
  678. when typeof(pattern) is Regex:
  679. if buffer.len > MaxReBufSize:
  680. yield Output(kind: openError, msg: "PCRE size limit is " & $MaxReBufSize)
  681. func detectBin(buffer: string): bool =
  682. for i in 0 ..< min(1024, buffer.len):
  683. if buffer[i] == '\0':
  684. return true
  685. proc compilePeg(initPattern: string): Peg =
  686. var pattern = initPattern
  687. if optWord in options:
  688. pattern = r"(^ / !\letter)(" & pattern & r") !\letter"
  689. if optIgnoreStyle in options:
  690. pattern = "\\y " & pattern
  691. elif optIgnoreCase in options:
  692. pattern = "\\i " & pattern
  693. result = peg(pattern)
  694. proc styleInsensitive(s: string): string =
  695. template addx =
  696. result.add(s[i])
  697. inc(i)
  698. result = ""
  699. var i = 0
  700. var brackets = 0
  701. while i < s.len:
  702. case s[i]
  703. of 'A'..'Z', 'a'..'z', '0'..'9':
  704. addx()
  705. if brackets == 0: result.add("_?")
  706. of '_':
  707. addx()
  708. result.add('?')
  709. of '[':
  710. addx()
  711. inc(brackets)
  712. of ']':
  713. addx()
  714. if brackets > 0: dec(brackets)
  715. of '?':
  716. addx()
  717. if s[i] == '<':
  718. addx()
  719. while s[i] != '>' and s[i] != '\0': addx()
  720. of '\\':
  721. addx()
  722. if s[i] in strutils.Digits:
  723. while s[i] in strutils.Digits: addx()
  724. else:
  725. addx()
  726. else: addx()
  727. proc compileRegex(initPattern: string): Regex =
  728. var pattern = initPattern
  729. var reflags = {reStudy}
  730. if optIgnoreStyle in options:
  731. pattern = styleInsensitive(pattern)
  732. if optWord in options:
  733. # see https://github.com/nim-lang/Nim/issues/13528#issuecomment-592786443
  734. pattern = r"(^|\W)(:?" & pattern & r")($|\W)"
  735. if {optIgnoreCase, optIgnoreStyle} * options != {}:
  736. reflags.incl reIgnoreCase
  737. result = if optRex in options: rex(pattern, reflags)
  738. else: re(pattern, reflags)
  739. template declareCompiledPatterns(compiledStruct: untyped,
  740. StructType: untyped,
  741. body: untyped) =
  742. {.hint[XDeclaredButNotUsed]: off.}
  743. if optRegex notin options:
  744. var compiledStruct: StructType[Peg]
  745. template compile1Pattern(p: string, pat: Peg) =
  746. if p!="": pat = p.compilePeg()
  747. proc compileArray(initPattern: seq[string]): seq[Peg] =
  748. for pat in initPattern:
  749. result.add pat.compilePeg()
  750. body
  751. else:
  752. var compiledStruct: StructType[Regex]
  753. template compile1Pattern(p: string, pat: Regex) =
  754. if p!="": pat = p.compileRegex()
  755. proc compileArray(initPattern: seq[string]): seq[Regex] =
  756. for pat in initPattern:
  757. result.add pat.compileRegex()
  758. body
  759. {.hint[XDeclaredButNotUsed]: on.}
  760. template ensureIncluded(includePat: seq[Pattern], str: string,
  761. body: untyped) =
  762. if includePat.len != 0:
  763. var matched = false
  764. for pat in includePat:
  765. if str.contains(pat):
  766. matched = true
  767. break
  768. if not matched:
  769. body
  770. template ensureExcluded(excludePat: seq[Pattern], str: string,
  771. body: untyped) =
  772. {.warning[UnreachableCode]: off.}
  773. for pat in excludePat:
  774. if str.contains(pat, 0):
  775. body
  776. break
  777. {.warning[UnreachableCode]: on.}
  778. func checkContext(context: string, searchOptC: SearchOptComp[Pattern]): bool =
  779. ensureIncluded searchOptC.inContext, context:
  780. return false
  781. ensureExcluded searchOptC.notInContext, context:
  782. return false
  783. result = true
  784. iterator processFile(searchOptC: SearchOptComp[Pattern], filename: string,
  785. yieldContents=false): Output =
  786. var buffer: string
  787. var error = false
  788. if optFilenames in options:
  789. buffer = filename
  790. elif optPipe in options:
  791. buffer = stdin.readAll()
  792. else:
  793. try:
  794. buffer = system.readFile(filename)
  795. except IOError as e:
  796. yield Output(kind: openError, msg: "readFile failed")
  797. error = true
  798. if not error:
  799. var reject = false
  800. var reason: string
  801. if searchOpt.checkBin in {biOff, biOnly}:
  802. let isBin = detectBin(buffer)
  803. if isBin and searchOpt.checkBin == biOff:
  804. reject = true
  805. reason = "binary file"
  806. if (not isBin) and searchOpt.checkBin == biOnly:
  807. reject = true
  808. reason = "text file"
  809. if not reject:
  810. ensureIncluded searchOptC.inFile, buffer:
  811. reject = true
  812. reason = "doesn't contain a requested match"
  813. if not reject:
  814. ensureExcluded searchOptC.notInFile, buffer:
  815. reject = true
  816. reason = "contains a forbidden match"
  817. if reject:
  818. yield Output(kind: rejected, reason: move(reason))
  819. elif optFilenames in options and searchOpt.pattern == "":
  820. yield Output(kind: outputFileName, name: move(buffer))
  821. else:
  822. var found = false
  823. var cnt = 0
  824. let skipCheckContext = (searchOpt.notInContext.len == 0 and
  825. searchOpt.inContext.len == 0)
  826. if skipCheckContext:
  827. for output in searchFile(searchOptC.pattern, buffer):
  828. found = true
  829. if optCount notin options:
  830. yield output
  831. else:
  832. if output.kind in {blockFirstMatch, blockNextMatch}:
  833. inc(cnt)
  834. else:
  835. var context: string
  836. var outputAccumulator: seq[Output]
  837. for outp in searchFile(searchOptC.pattern, buffer):
  838. if outp.kind in {blockFirstMatch, blockNextMatch}:
  839. outputAccumulator.add outp
  840. context.add outp.pre
  841. context.add outp.match.match
  842. elif outp.kind == blockEnd:
  843. outputAccumulator.add outp
  844. context.add outp.blockEnding
  845. # context has been formed, now check it:
  846. if checkContext(context, searchOptC):
  847. found = true
  848. for output in outputAccumulator:
  849. if optCount notin options:
  850. yield output
  851. else:
  852. if output.kind in {blockFirstMatch, blockNextMatch}:
  853. inc(cnt)
  854. context = ""
  855. outputAccumulator.setLen 0
  856. # end `if skipCheckContext`.
  857. if optCount in options and cnt > 0:
  858. yield Output(kind: justCount, matches: cnt)
  859. if yieldContents and found and optCount notin options:
  860. yield Output(kind: fileContents, buffer: move(buffer))
  861. proc hasRightPath(path: string, walkOptC: WalkOptComp[Pattern]): bool =
  862. if not (
  863. walkOpt.extensions.len > 0 or walkOpt.notExtensions.len > 0 or
  864. walkOpt.filename.len > 0 or walkOpt.notFilename.len > 0 or
  865. walkOpt.notDirPath.len > 0 or walkOpt.dirPath.len > 0):
  866. return true
  867. let filename = path.lastPathPart
  868. let ex = filename.splitFile.ext.substr(1) # skip leading '.'
  869. if walkOpt.extensions.len != 0:
  870. var matched = false
  871. for x in walkOpt.extensions:
  872. if os.cmpPaths(x, ex) == 0:
  873. matched = true
  874. break
  875. if not matched: return false
  876. for x in walkOpt.notExtensions:
  877. if os.cmpPaths(x, ex) == 0: return false
  878. ensureIncluded walkOptC.filename, filename:
  879. return false
  880. ensureExcluded walkOptC.notFilename, filename:
  881. return false
  882. let parent = path.parentDir
  883. ensureExcluded walkOptC.notDirPath, parent:
  884. return false
  885. ensureIncluded walkOptC.dirPath, parent:
  886. return false
  887. result = true
  888. proc isRightDirectory(path: string, walkOptC: WalkOptComp[Pattern]): bool =
  889. ## --dirname can be only checked when the final path is known
  890. ## so this proc is suitable for files only.
  891. if walkOptC.dirname.len > 0:
  892. var badDirname = false
  893. var (nextParent, dirname) = splitPath(path)
  894. # check that --dirname matches for one of directories in parent path:
  895. while dirname != "":
  896. badDirname = false
  897. ensureIncluded walkOptC.dirname, dirname:
  898. badDirname = true
  899. if not badDirname:
  900. break
  901. (nextParent, dirname) = splitPath(nextParent)
  902. if badDirname: # badDirname was set to true for all the dirs
  903. return false
  904. result = true
  905. proc descendToDirectory(path: string, walkOptC: WalkOptComp[Pattern]): bool =
  906. ## --notdirname can be checked for directories immediately for optimization to
  907. ## prevent descending into undesired directories.
  908. if walkOptC.notDirname.len > 0:
  909. let dirname = path.lastPathPart
  910. ensureExcluded walkOptC.notDirname, dirname:
  911. return false
  912. result = true
  913. iterator walkDirBasic(dir: string, walkOptC: WalkOptComp[Pattern]): string
  914. {.closure.} =
  915. var dirStack = @[dir] # stack of directories
  916. var timeFiles = newSeq[(times.Time, string)]()
  917. while dirStack.len > 0:
  918. let d = dirStack.pop()
  919. let rightDirForFiles = d.isRightDirectory(walkOptC)
  920. var files = newSeq[string]()
  921. var dirs = newSeq[string]()
  922. for kind, path in walkDir(d, skipSpecial = true):
  923. case kind
  924. of pcFile:
  925. if path.hasRightPath(walkOptC) and rightDirForFiles:
  926. files.add(path)
  927. of pcLinkToFile:
  928. if optFollow in options and path.hasRightPath(walkOptC) and
  929. rightDirForFiles:
  930. files.add(path)
  931. of pcDir:
  932. if optRecursive in options and path.descendToDirectory(walkOptC):
  933. dirs.add path
  934. of pcLinkToDir:
  935. if optFollow in options and optRecursive in options and
  936. path.descendToDirectory(walkOptC):
  937. dirs.add path
  938. if sortTime: # sort by time - collect files before yielding
  939. for file in files:
  940. var time: Time
  941. try:
  942. time = getLastModificationTime(file) # can fail for broken symlink
  943. except:
  944. discard
  945. timeFiles.add((time, file))
  946. else: # alphanumeric sort, yield immediately after sorting
  947. files.sort()
  948. for file in files:
  949. yield file
  950. dirs.sort(order = SortOrder.Descending)
  951. for dir in dirs:
  952. dirStack.add(dir)
  953. if sortTime:
  954. timeFiles.sort(sortTimeOrder)
  955. for (_, file) in timeFiles:
  956. yield file
  957. iterator walkRec(paths: seq[string]): tuple[error: string, filename: string]
  958. {.closure.} =
  959. declareCompiledPatterns(walkOptC, WalkOptComp):
  960. walkOptC.notFilename.add walkOpt.notFilename.compileArray()
  961. walkOptC.filename.add walkOpt.filename.compileArray()
  962. walkOptC.dirname.add walkOpt.dirname.compileArray()
  963. walkOptC.notDirname.add walkOpt.notDirname.compileArray()
  964. walkOptC.dirPath.add walkOpt.dirPath.compileArray()
  965. walkOptC.notDirPath.add walkOpt.notDirPath.compileArray()
  966. for path in paths:
  967. if dirExists(path):
  968. for p in walkDirBasic(path, walkOptC):
  969. yield ("", p)
  970. else:
  971. yield (
  972. if fileExists(path): ("", path)
  973. else: ("Error: no such file or directory: ", path))
  974. proc replaceMatches(pattern: Pattern; filename: string, buffer: string,
  975. fileResult: FileResult) =
  976. var newBuf = newStringOfCap(buffer.len)
  977. var changed = false
  978. var lineRepl = 1
  979. var i = 0
  980. for output in fileResult:
  981. if output.kind in {blockFirstMatch, blockNextMatch}:
  982. let curMi = output.match
  983. let r = replacef(curMi.match, pattern, replacement)
  984. if replace1match(filename, buffer, curMi, i, r, newBuf, lineRepl):
  985. changed = true
  986. i = curMi.last + 1
  987. if changed and optPipe notin options:
  988. newBuf.add(substr(buffer, i)) # finalize new buffer after last match
  989. var f: File
  990. if open(f, filename, fmWrite):
  991. f.write(newBuf)
  992. f.close()
  993. else:
  994. printError "cannot open file for overwriting: " & filename
  995. inc(gVar.errors)
  996. elif optPipe in options: # always print new buffer to stdout in pipe mode
  997. newBuf.add(substr(buffer, i)) # finalize new buffer after last match
  998. stdout.write(newBuf)
  999. template processFileResult(pattern: Pattern; filename: string,
  1000. fileResult: untyped) =
  1001. var filenameShown = false
  1002. template showFilename =
  1003. if not filenameShown:
  1004. printBlockFile(filename)
  1005. stdout.write("\n")
  1006. stdout.flushFile()
  1007. filenameShown = true
  1008. if optVerbose in options:
  1009. showFilename
  1010. if optReplace notin options:
  1011. var curCol: Column
  1012. var toFlush: bool
  1013. for output in fileResult:
  1014. updateCounters(output)
  1015. toFlush = true
  1016. if output.kind notin {rejected, openError, justCount} and not oneline:
  1017. showFilename
  1018. if output.kind == justCount and oneline:
  1019. printFile(filename & ":")
  1020. printOutput(filename, output, curCol)
  1021. if nWorkers == 0 and output.kind in {blockFirstMatch, blockNextMatch}:
  1022. stdout.flushFile() # flush immediately in single thread mode
  1023. if toFlush: stdout.flushFile()
  1024. else:
  1025. var buffer = ""
  1026. var matches: FileResult
  1027. for output in fileResult:
  1028. updateCounters(output)
  1029. case output.kind
  1030. of rejected, openError, justCount, outputFileName:
  1031. printInfo(filename, output)
  1032. of blockFirstMatch, blockNextMatch, blockEnd:
  1033. matches.add(output)
  1034. of fileContents: buffer = output.buffer
  1035. if matches.len > 0:
  1036. replaceMatches(pattern, filename, buffer, matches)
  1037. proc run1Thread() =
  1038. declareCompiledPatterns(searchOptC, SearchOptComp):
  1039. compile1Pattern(searchOpt.pattern, searchOptC.pattern)
  1040. searchOptC.inFile.add searchOpt.inFile.compileArray()
  1041. searchOptC.notInFile.add searchOpt.notInFile.compileArray()
  1042. searchOptC.inContext.add searchOpt.inContext.compileArray()
  1043. searchOptC.notInContext.add searchOpt.notInContext.compileArray()
  1044. if optPipe in options:
  1045. processFileResult(searchOptC.pattern, "-",
  1046. processFile(searchOptC, "-",
  1047. yieldContents=optReplace in options))
  1048. for entry in walkRec(paths):
  1049. if entry.error != "":
  1050. inc(gVar.errors)
  1051. printError (entry.error & entry.filename)
  1052. continue
  1053. processFileResult(searchOptC.pattern, entry.filename,
  1054. processFile(searchOptC, entry.filename,
  1055. yieldContents=optReplace in options))
  1056. # Multi-threaded version: all printing is being done in the Main thread.
  1057. # Totally nWorkers+1 additional threads are created (workers + pathProducer).
  1058. # An example of case nWorkers=2:
  1059. #
  1060. # ------------------ initial paths -------------------
  1061. # | Main thread |----------------->| pathProducer |
  1062. # ------------------ -------------------
  1063. # ^ | |
  1064. # resultsChan | walking errors, | | searchRequestsChan
  1065. # | number of files | -----+-----
  1066. # ----+--------------------------- | |
  1067. # | | (when walking finished) |a path |a path to file
  1068. # | | | |
  1069. # | | V V
  1070. # | | ------------ ------------
  1071. # | | | worker 1 | | worker 2 |
  1072. # | | ------------ ------------
  1073. # | | matches in the file | |
  1074. # | -------------------------------- |
  1075. # | matches in the file |
  1076. # ----------------------------------------------
  1077. #
  1078. # The matches from each file are passed at once as FileResult type.
  1079. proc worker(initSearchOpt: SearchOpt) {.thread.} =
  1080. searchOpt = initSearchOpt # init thread-local var
  1081. declareCompiledPatterns(searchOptC, SearchOptComp):
  1082. compile1Pattern(searchOpt.pattern, searchOptC.pattern)
  1083. searchOptC.inFile.add searchOpt.inFile.compileArray()
  1084. searchOptC.notInFile.add searchOpt.notInFile.compileArray()
  1085. searchOptC.inContext.add searchOpt.inContext.compileArray()
  1086. searchOptC.notInContext.add searchOpt.notInContext.compileArray()
  1087. while true:
  1088. let (fileNo, filename) = searchRequestsChan.recv()
  1089. var fileResult: FileResult
  1090. for output in processFile(searchOptC, filename,
  1091. yieldContents=(optReplace in options)):
  1092. fileResult.add(output)
  1093. resultsChan.send((false, fileNo, filename, move(fileResult)))
  1094. proc pathProducer(arg: (seq[string], WalkOpt)) {.thread.} =
  1095. let paths = arg[0]
  1096. walkOpt = arg[1] # init thread-local copy of opt
  1097. var
  1098. nextFileN = 0
  1099. for entry in walkRec(paths):
  1100. if entry.error == "":
  1101. searchRequestsChan.send((nextFileN, entry.filename))
  1102. else:
  1103. resultsChan.send((false, nextFileN, entry.filename,
  1104. @[Output(kind: openError, msg: entry.error)]))
  1105. nextFileN += 1
  1106. resultsChan.send((true, nextFileN, "", @[])) # pass total number of files
  1107. proc runMultiThread() =
  1108. var
  1109. workers = newSeq[Thread[SearchOpt]](nWorkers)
  1110. storage = newTable[int, (string, FileResult) ]()
  1111. # file number -> tuple[filename, fileResult - accumulated data structure]
  1112. firstUnprocessedFile = 0 # for always processing files in the same order
  1113. open(searchRequestsChan)
  1114. open(resultsChan)
  1115. for n in 0 ..< nWorkers:
  1116. createThread(workers[n], worker, searchOpt)
  1117. var producerThread: Thread[(seq[string], WalkOpt)]
  1118. createThread(producerThread, pathProducer, (paths, walkOpt))
  1119. declareCompiledPatterns(pat, SinglePattern):
  1120. compile1Pattern(searchOpt.pattern, pat.pattern)
  1121. template add1fileResult(fileNo: int, fname: string, fResult: FileResult) =
  1122. storage[fileNo] = (fname, fResult)
  1123. while storage.haskey(firstUnprocessedFile):
  1124. let fileResult = storage[firstUnprocessedFile][1]
  1125. let filename = storage[firstUnprocessedFile][0]
  1126. processFileResult(pat.pattern, filename, fileResult)
  1127. storage.del(firstUnprocessedFile)
  1128. firstUnprocessedFile += 1
  1129. var totalFiles = -1 # will be known when pathProducer finishes
  1130. while totalFiles == -1 or firstUnprocessedFile < totalFiles:
  1131. let msg = resultsChan.recv()
  1132. if msg.finished:
  1133. totalFiles = msg.fileNo
  1134. else:
  1135. add1fileResult(msg.fileNo, msg.filename, msg.fileResult)
  1136. proc reportError(msg: string) =
  1137. printError "Error: " & msg
  1138. quit "Run nimgrep --help for the list of options"
  1139. proc writeHelp() =
  1140. stdout.write(Usage)
  1141. stdout.flushFile()
  1142. quit(0)
  1143. proc writeVersion() =
  1144. stdout.write(Version & "\n")
  1145. stdout.flushFile()
  1146. quit(0)
  1147. proc checkOptions(subset: TOptions, a, b: string) =
  1148. if subset <= options:
  1149. quit("cannot specify both '$#' and '$#'" % [a, b])
  1150. proc parseNonNegative(str: string, key: string): int =
  1151. try:
  1152. result = parseInt(str)
  1153. except ValueError:
  1154. reportError("Option " & key & " requires an integer but '" &
  1155. str & "' was given")
  1156. if result < 0:
  1157. reportError("A positive integer is expected for option " & key)
  1158. when defined(posix):
  1159. useWriteStyled = terminal.isatty(stdout)
  1160. # that should be before option processing to allow override of useWriteStyled
  1161. for kind, key, val in getopt():
  1162. case kind
  1163. of cmdArgument:
  1164. if options.contains(optStdin):
  1165. paths.add(key)
  1166. elif not searchOpt.patternSet:
  1167. searchOpt.pattern = key
  1168. searchOpt.patternSet = true
  1169. elif options.contains(optReplace) and not replacementSet:
  1170. replacement = key
  1171. replacementSet = true
  1172. else:
  1173. paths.add(key)
  1174. of cmdLongOption, cmdShortOption:
  1175. proc addNotEmpty(s: var seq[string], name: string) =
  1176. if name == "":
  1177. reportError("empty string given for option --" & key &
  1178. " (did you forget `:`?)")
  1179. s.add name
  1180. case normalize(key)
  1181. of "find", "f": incl(options, optFind)
  1182. of "replace", "!": incl(options, optReplace)
  1183. of "peg":
  1184. excl(options, optRegex)
  1185. incl(options, optPeg)
  1186. of "re":
  1187. incl(options, optRegex)
  1188. excl(options, optPeg)
  1189. of "rex", "x":
  1190. incl(options, optRex)
  1191. incl(options, optRegex)
  1192. excl(options, optPeg)
  1193. of "recursive", "r": incl(options, optRecursive)
  1194. of "follow": incl(options, optFollow)
  1195. of "confirm": incl(options, optConfirm)
  1196. of "stdin": incl(options, optStdin)
  1197. of "word", "w": incl(options, optWord)
  1198. of "ignorecase", "ignore-case", "i": incl(options, optIgnoreCase)
  1199. of "ignorestyle", "ignore-style", "y": incl(options, optIgnoreStyle)
  1200. of "threads", "j":
  1201. if val == "":
  1202. nWorkers = countProcessors()
  1203. else:
  1204. nWorkers = parseNonNegative(val, key)
  1205. of "extensions", "ex", "ext": walkOpt.extensions.add val.split('|')
  1206. of "nextensions", "notextensions", "nex", "notex",
  1207. "noext", "no-ext": # 2 deprecated options
  1208. walkOpt.notExtensions.add val.split('|')
  1209. of "dirname", "di":
  1210. walkOpt.dirname.addNotEmpty val
  1211. of "ndirname", "notdirname", "ndi", "notdi",
  1212. "excludedir", "exclude-dir", "ed": # 3 deprecated options
  1213. walkOpt.notDirname.addNotEmpty val
  1214. of "dirpath", "dirp",
  1215. "includedir", "include-dir", "id": # 3 deprecated options
  1216. walkOpt.dirPath.addNotEmpty val
  1217. of "ndirpath", "notdirpath", "ndirp", "notdirp":
  1218. walkOpt.notDirPath.addNotEmpty val
  1219. of "filename", "fi",
  1220. "includefile", "include-file", "if": # 3 deprecated options
  1221. walkOpt.filename.addNotEmpty val
  1222. of "nfilename", "nfi", "notfilename", "notfi",
  1223. "excludefile", "exclude-file", "ef": # 3 deprecated options
  1224. walkOpt.notFilename.addNotEmpty val
  1225. of "infile", "inf",
  1226. "matchfile", "match", "mf": # 3 deprecated options
  1227. searchOpt.inFile.addNotEmpty val
  1228. of "ninfile", "notinfile", "ninf", "notinf",
  1229. "nomatchfile", "nomatch", "nf": # 3 options are deprecated
  1230. searchOpt.notInFile.addNotEmpty val
  1231. of "incontext", "inc":
  1232. searchOpt.inContext.addNotEmpty val
  1233. of "nincontext", "notincontext", "ninc", "notinc":
  1234. searchOpt.notInContext.addNotEmpty val
  1235. of "bin":
  1236. case val
  1237. of "on": searchOpt.checkBin = biOn
  1238. of "off": searchOpt.checkBin = biOff
  1239. of "only": searchOpt.checkBin = biOnly
  1240. else: reportError("unknown value for --bin")
  1241. of "text", "t": searchOpt.checkBin = biOff
  1242. of "count": incl(options, optCount)
  1243. of "sorttime", "sort-time", "s":
  1244. case normalize(val)
  1245. of "off": sortTime = false
  1246. of "", "on", "asc", "ascending":
  1247. sortTime = true
  1248. sortTimeOrder = SortOrder.Ascending
  1249. of "desc", "descending":
  1250. sortTime = true
  1251. sortTimeOrder = SortOrder.Descending
  1252. else: reportError("invalid value '" & val & "' for --sortTime")
  1253. of "nocolor", "no-color": useWriteStyled = false
  1254. of "color":
  1255. case val
  1256. of "auto": discard
  1257. of "off", "never", "false": useWriteStyled = false
  1258. of "", "on", "always", "true": useWriteStyled = true
  1259. else: reportError("invalid value '" & val & "' for --color")
  1260. of "colortheme", "color-theme":
  1261. colortheme = normalize(val)
  1262. if colortheme notin ["simple", "bnw", "ack", "gnu"]:
  1263. reportError("unknown colortheme '" & val & "'")
  1264. of "beforecontext", "before-context", "b":
  1265. linesBefore = parseNonNegative(val, key)
  1266. of "aftercontext", "after-context", "a":
  1267. linesAfter = parseNonNegative(val, key)
  1268. of "context", "c":
  1269. linesContext = parseNonNegative(val, key)
  1270. of "newline", "l":
  1271. newLine = true
  1272. # Tabs are aligned automatically for --group, --newLine, --filenames
  1273. expandTabs = false
  1274. of "group", "g":
  1275. oneline = false
  1276. expandTabs = false
  1277. of "cols", "%":
  1278. incl(options, optLimitChars)
  1279. termWidth = terminalWidth()
  1280. if val == "auto" or key == "%":
  1281. limitCharUsr = termWidth
  1282. when defined(windows): # Windows cmd & powershell add an empty line
  1283. limitCharUsr -= 1 # when printing '\n' right after the last column
  1284. elif val == "":
  1285. limitCharUsr = 80
  1286. else:
  1287. limitCharUsr = parseNonNegative(val, key)
  1288. of "onlyascii", "only-ascii", "@":
  1289. if val == "" or val == "on" or key == "@":
  1290. optOnlyAscii = true
  1291. elif val == "off":
  1292. optOnlyAscii = false
  1293. else:
  1294. printError("unknown value for --onlyAscii option")
  1295. of "verbose": incl(options, optVerbose)
  1296. of "filenames":
  1297. incl(options, optFilenames)
  1298. expandTabs = false
  1299. of "help", "h": writeHelp()
  1300. of "version", "v": writeVersion()
  1301. of "": incl(options, optPipe)
  1302. else: reportError("unrecognized option '" & key & "'")
  1303. of cmdEnd: assert(false) # cannot happen
  1304. checkOptions({optFind, optReplace}, "find", "replace")
  1305. checkOptions({optCount, optReplace}, "count", "replace")
  1306. checkOptions({optPeg, optRegex}, "peg", "re")
  1307. checkOptions({optIgnoreCase, optIgnoreStyle}, "ignore_case", "ignore_style")
  1308. checkOptions({optFilenames, optReplace}, "filenames", "replace")
  1309. checkOptions({optPipe, optStdin}, "-", "stdin")
  1310. checkOptions({optPipe, optFilenames}, "-", "filenames")
  1311. checkOptions({optPipe, optConfirm}, "-", "confirm")
  1312. checkOptions({optPipe, optRecursive}, "-", "recursive")
  1313. linesBefore = max(linesBefore, linesContext)
  1314. linesAfter = max(linesAfter, linesContext)
  1315. if optPipe in options and paths.len != 0:
  1316. reportError("both - and paths are specified")
  1317. if optStdin in options:
  1318. searchOpt.pattern = ask("pattern [ENTER to exit]: ")
  1319. if searchOpt.pattern.len == 0: quit(0)
  1320. if optReplace in options:
  1321. replacement = ask("replacement [supports $1, $# notations]: ")
  1322. if optReplace in options and not replacementSet:
  1323. reportError("provide REPLACEMENT as second argument (use \"\" for empty one)")
  1324. if optReplace in options and paths.len == 0 and optPipe notin options:
  1325. reportError("provide paths for replacement explicitly (use . for current directory)")
  1326. if searchOpt.pattern == "" and optFilenames notin options:
  1327. reportError("empty pattern was given")
  1328. else:
  1329. if paths.len == 0 and optPipe notin options:
  1330. paths.add(".")
  1331. if optPipe in options or nWorkers == 0:
  1332. run1Thread()
  1333. else:
  1334. runMultiThread()
  1335. if gVar.errors != 0:
  1336. printError $gVar.errors & " errors"
  1337. if searchOpt.pattern != "":
  1338. # PATTERN allowed to be empty if --filenames is given
  1339. printBold($gVar.matches & " matches")
  1340. stdout.write("\n")
  1341. if gVar.errors != 0:
  1342. quit(1)