nimgrep.nim 47 KB

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