nre.nim 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. #
  2. # Nim's Runtime Library
  3. # (c) Copyright 2015 Nim Contributors
  4. #
  5. # See the file "copying.txt", included in this
  6. # distribution, for details about the copyright.
  7. #
  8. when defined(js):
  9. {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".}
  10. ## What is NRE?
  11. ## ============
  12. ##
  13. ## A regular expression library for Nim using PCRE to do the hard work.
  14. ##
  15. ## For documentation on how to write patterns, there exists `the official PCRE
  16. ## pattern documentation
  17. ## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_. You can also
  18. ## search the internet for a wide variety of third-party documentation and
  19. ## tools.
  20. ##
  21. ## .. warning:: If you love `sequtils.toSeq` we have bad news for you. This
  22. ## library doesn't work with it due to documented compiler limitations. As
  23. ## a workaround, use this:
  24. runnableExamples:
  25. # either `import std/nre except toSeq` or fully qualify `sequtils.toSeq`:
  26. import std/sequtils
  27. iterator iota(n: int): int =
  28. for i in 0..<n: yield i
  29. assert sequtils.toSeq(iota(3)) == @[0, 1, 2]
  30. ## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre)
  31. ## and [regex](https://github.com/nitely/nim-regex).
  32. ## Licencing
  33. ## ---------
  34. ##
  35. ## PCRE has `some additional terms`_ that you must agree to in order to use
  36. ## this module.
  37. ##
  38. ## .. _`some additional terms`: https://pcre.sourceforge.net/license.txt
  39. runnableExamples:
  40. import std/sugar
  41. let vowels = re"[aeoui]"
  42. let bounds = collect:
  43. for match in "moiga".findIter(vowels): match.matchBounds
  44. assert bounds == @[1 .. 1, 2 .. 2, 4 .. 4]
  45. from std/sequtils import toSeq
  46. let s = sequtils.toSeq("moiga".findIter(vowels))
  47. # fully qualified to avoid confusion with nre.toSeq
  48. assert s.len == 3
  49. let firstVowel = "foo".find(vowels)
  50. let hasVowel = firstVowel.isSome()
  51. assert hasVowel
  52. let matchBounds = firstVowel.get().captureBounds[-1]
  53. assert matchBounds.a == 1
  54. # as with module `re`, unless specified otherwise, `start` parameter in each
  55. # proc indicates where the scan starts, but outputs are relative to the start
  56. # of the input string, not to `start`:
  57. assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab"
  58. assert find("uxabc", re"ab", start = 3).isNone
  59. from std/pcre import nil
  60. import nre/private/util
  61. import std/tables
  62. from std/strutils import `%`
  63. import std/options
  64. from std/unicode import runeLenAt
  65. when defined(nimPreviewSlimSystem):
  66. import std/assertions
  67. export options
  68. type
  69. RegexDesc* = object
  70. pattern*: string
  71. pcreObj: ptr pcre.Pcre ## not nil
  72. pcreExtra: ptr pcre.ExtraData ## nil
  73. captureNameToId: Table[string, int]
  74. Regex* = ref RegexDesc
  75. ## Represents the pattern that things are matched against, constructed with
  76. ## `re(string)`. Examples: `re"foo"`, `re(r"(*ANYCRLF)(?x)foo #
  77. ## comment".`
  78. ##
  79. ## `pattern: string`
  80. ## : the string that was used to create the pattern. For details on how
  81. ## to write a pattern, please see `the official PCRE pattern
  82. ## documentation.
  83. ## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_
  84. ##
  85. ## `captureCount: int`
  86. ## : the number of captures that the pattern has.
  87. ##
  88. ## `captureNameId: Table[string, int]`
  89. ## : a table from the capture names to their numeric id.
  90. ##
  91. ##
  92. ## Options
  93. ## .......
  94. ##
  95. ## The following options may appear anywhere in the pattern, and they affect
  96. ## the rest of it.
  97. ##
  98. ## - `(?i)` - case insensitive
  99. ## - `(?m)` - multi-line: `^` and `$` match the beginning and end of
  100. ## lines, not of the subject string
  101. ## - `(?s)` - `.` also matches newline (*dotall*)
  102. ## - `(?U)` - expressions are not greedy by default. `?` can be added
  103. ## to a qualifier to make it greedy
  104. ## - `(?x)` - whitespace and comments (`#`) are ignored (*extended*)
  105. ## - `(?X)` - character escapes without special meaning (`\w` vs.
  106. ## `\a`) are errors (*extra*)
  107. ##
  108. ## One or a combination of these options may appear only at the beginning
  109. ## of the pattern:
  110. ##
  111. ## - `(*UTF8)` - treat both the pattern and subject as UTF-8
  112. ## - `(*UCP)` - Unicode character properties; `\w` matches `я`
  113. ## - `(*U)` - a combination of the two options above
  114. ## - `(*FIRSTLINE*)` - fails if there is not a match on the first line
  115. ## - `(*NO_AUTO_CAPTURE)` - turn off auto-capture for groups;
  116. ## `(?<name>...)` can be used to capture
  117. ## - `(*CR)` - newlines are separated by `\r`
  118. ## - `(*LF)` - newlines are separated by `\n` (UNIX default)
  119. ## - `(*CRLF)` - newlines are separated by `\r\n` (Windows default)
  120. ## - `(*ANYCRLF)` - newlines are separated by any of the above
  121. ## - `(*ANY)` - newlines are separated by any of the above and Unicode
  122. ## newlines:
  123. ##
  124. ## single characters VT (vertical tab, U+000B), FF (form feed, U+000C),
  125. ## NEL (next line, U+0085), LS (line separator, U+2028), and PS
  126. ## (paragraph separator, U+2029). For the 8-bit library, the last two
  127. ## are recognized only in UTF-8 mode.
  128. ## — man pcre
  129. ##
  130. ## - `(*JAVASCRIPT_COMPAT)` - JavaScript compatibility
  131. ## - `(*NO_STUDY)` - turn off studying; study is enabled by default
  132. ##
  133. ## For more details on the leading option groups, see the `Option
  134. ## Setting <https://man7.org/linux/man-pages/man3/pcresyntax.3.html#OPTION_SETTING>`_
  135. ## and the `Newline
  136. ## Convention <https://man7.org/linux/man-pages/man3/pcresyntax.3.html#NEWLINE_CONVENTION>`_
  137. ## sections of the `PCRE syntax
  138. ## manual <https://man7.org/linux/man-pages/man3/pcresyntax.3.html>`_.
  139. ##
  140. ## Some of these options are not part of PCRE and are converted by nre
  141. ## into PCRE flags. These include `NEVER_UTF`, `ANCHORED`,
  142. ## `DOLLAR_ENDONLY`, `FIRSTLINE`, `NO_AUTO_CAPTURE`,
  143. ## `JAVASCRIPT_COMPAT`, `U`, `NO_STUDY`. In other PCRE wrappers, you
  144. ## will need to pass these as separate flags to PCRE.
  145. RegexMatch* = object
  146. ## Usually seen as Option[RegexMatch], it represents the result of an
  147. ## execution. On failure, it is none, on success, it is some.
  148. ##
  149. ## `pattern: Regex`
  150. ## : the pattern that is being matched
  151. ##
  152. ## `str: string`
  153. ## : the string that was matched against
  154. ##
  155. ## `captures[]: string`
  156. ## : the string value of whatever was captured at that id. If the value
  157. ## is invalid, then behavior is undefined. If the id is `-1`, then
  158. ## the whole match is returned. If the given capture was not matched,
  159. ## `nil` is returned. See examples for `match`.
  160. ##
  161. ## `captureBounds[]: HSlice[int, int]`
  162. ## : gets the bounds of the given capture according to the same rules as
  163. ## the above. If the capture is not filled, then `None` is returned.
  164. ## The bounds are both inclusive. See examples for `match`.
  165. ##
  166. ## `match: string`
  167. ## : the full text of the match.
  168. ##
  169. ## `matchBounds: HSlice[int, int]`
  170. ## : the bounds of the match, as in `captureBounds[]`
  171. ##
  172. ## `(captureBounds|captures).toTable`
  173. ## : returns a table with each named capture as a key.
  174. ##
  175. ## `(captureBounds|captures).toSeq`
  176. ## : returns all the captures by their number.
  177. ##
  178. ## `$: string`
  179. ## : same as `match`
  180. pattern*: Regex ## The regex doing the matching.
  181. ## Not nil.
  182. str*: string ## The string that was matched against.
  183. pcreMatchBounds: seq[HSlice[cint, cint]] ## First item is the bounds of the match
  184. ## Other items are the captures
  185. ## `a` is inclusive start, `b` is exclusive end
  186. Captures* = distinct RegexMatch
  187. CaptureBounds* = distinct RegexMatch
  188. RegexError* = ref object of CatchableError
  189. RegexInternalError* = ref object of RegexError
  190. ## Internal error in the module, this probably means that there is a bug
  191. InvalidUnicodeError* = ref object of RegexError
  192. ## Thrown when matching fails due to invalid unicode in strings
  193. pos*: int ## the location of the invalid unicode in bytes
  194. SyntaxError* = ref object of RegexError
  195. ## Thrown when there is a syntax error in the
  196. ## regular expression string passed in
  197. pos*: int ## the location of the syntax error in bytes
  198. pattern*: string ## the pattern that caused the problem
  199. StudyError* = ref object of RegexError
  200. ## Thrown when studying the regular expression fails
  201. ## for whatever reason. The message contains the error
  202. ## code.
  203. when defined(gcDestructors):
  204. when defined(nimAllowNonVarDestructor) and defined(nimPreviewNonVarDestructor):
  205. proc `=destroy`(pattern: RegexDesc) =
  206. `=destroy`(pattern.pattern)
  207. pcre.free_substring(cast[cstring](pattern.pcreObj))
  208. if pattern.pcreExtra != nil:
  209. pcre.free_study(pattern.pcreExtra)
  210. `=destroy`(pattern.captureNameToId)
  211. else:
  212. proc `=destroy`(pattern: var RegexDesc) =
  213. `=destroy`(pattern.pattern)
  214. pcre.free_substring(cast[cstring](pattern.pcreObj))
  215. if pattern.pcreExtra != nil:
  216. pcre.free_study(pattern.pcreExtra)
  217. `=destroy`(pattern.captureNameToId)
  218. else:
  219. proc destroyRegex(pattern: Regex) =
  220. `=destroy`(pattern.pattern)
  221. pcre.free_substring(cast[cstring](pattern.pcreObj))
  222. if pattern.pcreExtra != nil:
  223. pcre.free_study(pattern.pcreExtra)
  224. `=destroy`(pattern.captureNameToId)
  225. proc getinfo[T](pattern: Regex, opt: cint): T =
  226. result = default(T)
  227. let retcode = pcre.fullinfo(pattern.pcreObj, pattern.pcreExtra, opt, addr result)
  228. if retcode < 0:
  229. # XXX Error message that doesn't expose implementation details
  230. raise newException(FieldDefect, "Invalid getinfo for $1, errno $2" % [$opt, $retcode])
  231. proc getNameToNumberTable(pattern: Regex): Table[string, int] =
  232. let entryCount = getinfo[cint](pattern, pcre.INFO_NAMECOUNT)
  233. let entrySize = getinfo[cint](pattern, pcre.INFO_NAMEENTRYSIZE)
  234. let table = cast[ptr UncheckedArray[uint8]](
  235. getinfo[int](pattern, pcre.INFO_NAMETABLE))
  236. result = initTable[string, int]()
  237. for i in 0 ..< entryCount:
  238. let pos = i * entrySize
  239. let num = (int(table[pos]) shl 8) or int(table[pos + 1]) - 1
  240. var name = ""
  241. var idx = 2
  242. while table[pos + idx] != 0:
  243. name.add(char(table[pos + idx]))
  244. idx += 1
  245. result[name] = num
  246. proc initRegex(pattern: string, flags: int, study = true): Regex =
  247. when defined(gcDestructors):
  248. result = Regex()
  249. else:
  250. new(result, destroyRegex)
  251. result.pattern = pattern
  252. var errorMsg: cstring = ""
  253. var errOffset: cint = 0
  254. result.pcreObj = pcre.compile(cstring(pattern),
  255. # better hope int is at least 4 bytes..
  256. cint(flags), addr errorMsg,
  257. addr errOffset, nil)
  258. if result.pcreObj == nil:
  259. # failed to compile
  260. raise SyntaxError(msg: $errorMsg, pos: errOffset, pattern: pattern)
  261. if study:
  262. var options: cint = 0
  263. var hasJit: cint = cint(0)
  264. if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0:
  265. if hasJit == 1'i32:
  266. options = pcre.STUDY_JIT_COMPILE
  267. result.pcreExtra = pcre.study(result.pcreObj, options, addr errorMsg)
  268. if errorMsg != nil:
  269. raise StudyError(msg: $errorMsg)
  270. result.captureNameToId = result.getNameToNumberTable()
  271. proc captureCount*(pattern: Regex): int =
  272. return getinfo[cint](pattern, pcre.INFO_CAPTURECOUNT)
  273. proc captureNameId*(pattern: Regex): Table[string, int] =
  274. return pattern.captureNameToId
  275. proc matchesCrLf(pattern: Regex): bool =
  276. let flags = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS))
  277. let newlineFlags = flags and (pcre.NEWLINE_CRLF or
  278. pcre.NEWLINE_ANY or
  279. pcre.NEWLINE_ANYCRLF)
  280. if newlineFlags > 0u32:
  281. return true
  282. # get flags from build config
  283. var confFlags: cint = cint(0)
  284. if pcre.config(pcre.CONFIG_NEWLINE, addr confFlags) != 0:
  285. assert(false, "CONFIG_NEWLINE apparently got screwed up")
  286. case confFlags
  287. of 13: return false
  288. of 10: return false
  289. of (13 shl 8) or 10: return true
  290. of -2: return true
  291. of -1: return true
  292. else: return false
  293. func captureBounds*(pattern: RegexMatch): CaptureBounds = return CaptureBounds(pattern)
  294. func captures*(pattern: RegexMatch): Captures = return Captures(pattern)
  295. func contains*(pattern: CaptureBounds, i: int): bool =
  296. let pattern = RegexMatch(pattern)
  297. pattern.pcreMatchBounds[i + 1].a != -1
  298. func contains*(pattern: Captures, i: int): bool =
  299. i in CaptureBounds(pattern)
  300. func `[]`*(pattern: CaptureBounds, i: int): HSlice[int, int] =
  301. let pattern = RegexMatch(pattern)
  302. if not (i in pattern.captureBounds):
  303. raise newException(IndexDefect, "Group '" & $i & "' was not captured")
  304. let bounds = pattern.pcreMatchBounds[i + 1]
  305. int(bounds.a)..int(bounds.b-1)
  306. func `[]`*(pattern: Captures, i: int): string =
  307. let pattern = RegexMatch(pattern)
  308. let bounds = pattern.captureBounds[i]
  309. pattern.str.substr(bounds.a, bounds.b)
  310. func match*(pattern: RegexMatch): string =
  311. return pattern.captures[-1]
  312. func matchBounds*(pattern: RegexMatch): HSlice[int, int] =
  313. return pattern.captureBounds[-1]
  314. func contains*(pattern: CaptureBounds, name: string): bool =
  315. let pattern = RegexMatch(pattern)
  316. let nameToId = pattern.pattern.captureNameToId
  317. if not (name in nameToId):
  318. return false
  319. nameToId[name] in pattern.captureBounds
  320. func contains*(pattern: Captures, name: string): bool =
  321. name in CaptureBounds(pattern)
  322. func checkNamedCaptured(pattern: RegexMatch, name: string) =
  323. if not (name in pattern.captureBounds):
  324. raise newException(KeyError, "Group '" & name & "' was not captured")
  325. func `[]`*(pattern: CaptureBounds, name: string): HSlice[int, int] =
  326. let pattern = RegexMatch(pattern)
  327. checkNamedCaptured(pattern, name)
  328. {.noSideEffect.}:
  329. result = pattern.captureBounds[pattern.pattern.captureNameToId[name]]
  330. func `[]`*(pattern: Captures, name: string): string =
  331. let pattern = RegexMatch(pattern)
  332. checkNamedCaptured(pattern, name)
  333. {.noSideEffect.}:
  334. result = pattern.captures[pattern.pattern.captureNameToId[name]]
  335. template toTableImpl() {.dirty.} =
  336. for key in RegexMatch(pattern).pattern.captureNameId.keys:
  337. if key in pattern:
  338. result[key] = pattern[key]
  339. func toTable*(pattern: Captures): Table[string, string] =
  340. result = initTable[string, string]()
  341. toTableImpl()
  342. func toTable*(pattern: CaptureBounds): Table[string, HSlice[int, int]] =
  343. result = initTable[string, HSlice[int, int]]()
  344. toTableImpl()
  345. template itemsImpl() {.dirty.} =
  346. for i in 0 ..< RegexMatch(pattern).pattern.captureCount:
  347. # done in this roundabout way to avoid multiple yields (potential code
  348. # bloat)
  349. let nextYieldVal = if i in pattern:
  350. some(pattern[i])
  351. else:
  352. default
  353. yield nextYieldVal
  354. iterator items*(pattern: CaptureBounds,
  355. default = none(HSlice[int, int])): Option[HSlice[int, int]] =
  356. itemsImpl()
  357. iterator items*(pattern: Captures,
  358. default: Option[string] = none(string)): Option[string] =
  359. itemsImpl()
  360. proc toSeq*(pattern: CaptureBounds,
  361. default = none(HSlice[int, int])): seq[Option[HSlice[int, int]]] =
  362. result = @[]
  363. for it in pattern.items(default): result.add it
  364. proc toSeq*(pattern: Captures,
  365. default: Option[string] = none(string)): seq[Option[string]] =
  366. result = @[]
  367. for it in pattern.items(default): result.add it
  368. proc `$`*(pattern: RegexMatch): string =
  369. return pattern.captures[-1]
  370. proc `==`*(a, b: Regex): bool =
  371. if not a.isNil and not b.isNil:
  372. return a.pattern == b.pattern and
  373. a.pcreObj == b.pcreObj and
  374. a.pcreExtra == b.pcreExtra
  375. else:
  376. return system.`==`(a, b)
  377. proc `==`*(a, b: RegexMatch): bool =
  378. return a.pattern == b.pattern and
  379. a.str == b.str
  380. const PcreOptions = {
  381. "NEVER_UTF": pcre.NEVER_UTF,
  382. "ANCHORED": pcre.ANCHORED,
  383. "DOLLAR_ENDONLY": pcre.DOLLAR_ENDONLY,
  384. "FIRSTLINE": pcre.FIRSTLINE,
  385. "NO_AUTO_CAPTURE": pcre.NO_AUTO_CAPTURE,
  386. "JAVASCRIPT_COMPAT": pcre.JAVASCRIPT_COMPAT,
  387. "U": pcre.UTF8 or pcre.UCP
  388. }.toTable
  389. # Options that are supported inside regular expressions themselves
  390. const SkipOptions = [
  391. "LIMIT_MATCH=", "LIMIT_RECURSION=", "NO_AUTO_POSSESS", "NO_START_OPT",
  392. "UTF8", "UTF16", "UTF32", "UTF", "UCP",
  393. "CR", "LF", "CRLF", "ANYCRLF", "ANY", "BSR_ANYCRLF", "BSR_UNICODE"
  394. ]
  395. proc extractOptions(pattern: string): tuple[pattern: string, flags: int, study: bool] =
  396. result = ("", 0, true)
  397. var optionStart = 0
  398. var equals = false
  399. for i, c in pattern:
  400. if optionStart == i:
  401. if c != '(':
  402. break
  403. optionStart = i
  404. elif optionStart == i-1:
  405. if c != '*':
  406. break
  407. elif c == ')':
  408. let name = pattern[optionStart+2 .. i-1]
  409. if equals or name in SkipOptions:
  410. result.pattern.add pattern[optionStart .. i]
  411. elif PcreOptions.hasKey name:
  412. result.flags = result.flags or PcreOptions[name]
  413. elif name == "NO_STUDY":
  414. result.study = false
  415. else:
  416. break
  417. optionStart = i+1
  418. equals = false
  419. elif not equals:
  420. if c == '=':
  421. equals = true
  422. if pattern[optionStart+2 .. i] notin SkipOptions:
  423. break
  424. elif c notin {'A'..'Z', '0'..'9', '_'}:
  425. break
  426. result.pattern.add pattern[optionStart .. pattern.high]
  427. proc re*(pattern: string): Regex =
  428. let (pattern, flags, study) = extractOptions(pattern)
  429. initRegex(pattern, flags, study)
  430. proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Option[RegexMatch] =
  431. var myResult = RegexMatch(pattern: pattern, str: str)
  432. # See PCRE man pages.
  433. # 2x capture count to make room for start-end pairs
  434. # 1x capture count as slack space for PCRE
  435. let vecsize = (pattern.captureCount() + 1) * 3
  436. # div 2 because each element is 2 cints long
  437. # plus 1 because we need the ceiling, not the floor
  438. myResult.pcreMatchBounds = newSeq[HSlice[cint, cint]]((vecsize + 1) div 2)
  439. myResult.pcreMatchBounds.setLen(vecsize div 3)
  440. let strlen = if endpos == int.high: str.len else: endpos+1
  441. doAssert(strlen <= str.len) # don't want buffer overflows
  442. let execRet = pcre.exec(pattern.pcreObj,
  443. pattern.pcreExtra,
  444. cstring(str),
  445. cint(strlen),
  446. cint(start),
  447. cint(flags),
  448. cast[ptr cint](addr myResult.pcreMatchBounds[0]),
  449. cint(vecsize))
  450. if execRet >= 0:
  451. return some(myResult)
  452. case execRet:
  453. of pcre.ERROR_NOMATCH:
  454. return none(RegexMatch)
  455. of pcre.ERROR_NULL:
  456. raise newException(AccessViolationDefect, "Expected non-null parameters")
  457. of pcre.ERROR_BADOPTION:
  458. raise RegexInternalError(msg: "Unknown pattern flag. Either a bug or " &
  459. "outdated PCRE.")
  460. of pcre.ERROR_BADUTF8, pcre.ERROR_SHORTUTF8, pcre.ERROR_BADUTF8_OFFSET:
  461. raise InvalidUnicodeError(msg: "Invalid unicode byte sequence",
  462. pos: myResult.pcreMatchBounds[0].a)
  463. else:
  464. raise RegexInternalError(msg: "Unknown internal error: " & $execRet)
  465. proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] =
  466. ## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the
  467. ## string.
  468. runnableExamples:
  469. assert "foo".match(re"f").isSome
  470. assert "foo".match(re"o").isNone
  471. assert "abc".match(re"(\w)").get.captures[0] == "a"
  472. assert "abc".match(re"(?<letter>\w)").get.captures["letter"] == "a"
  473. assert "abc".match(re"(\w)\w").get.captures[-1] == "ab"
  474. assert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0
  475. assert 0 in "abc".match(re"(\w)").get.captureBounds
  476. assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1
  477. assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2
  478. return str.matchImpl(pattern, start, endpos, pcre.ANCHORED)
  479. iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): RegexMatch =
  480. ## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every
  481. ## non-overlapping match:
  482. runnableExamples:
  483. import std/sugar
  484. assert collect(for a in "2222".findIter(re"22"): a.match) == @["22", "22"]
  485. # not @["22", "22", "22"]
  486. ## Arguments are the same as `find(...)<#find,string,Regex,int>`_
  487. ##
  488. ## Variants:
  489. ##
  490. ## - `proc findAll(...)` returns a `seq[string]`
  491. # see pcredemo for explanation => https://www.pcre.org/original/doc/html/pcredemo.html
  492. let matchesCrLf = pattern.matchesCrLf()
  493. let unicode = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS) and
  494. pcre.UTF8) > 0u32
  495. let strlen = if endpos == int.high: str.len else: endpos+1
  496. var offset = start
  497. var match: Option[RegexMatch] = default(Option[RegexMatch])
  498. var neverMatched = true
  499. while true:
  500. var flags = 0
  501. if match.isSome and
  502. match.get.matchBounds.a > match.get.matchBounds.b:
  503. # 0-len match
  504. flags = pcre.NOTEMPTY_ATSTART
  505. match = str.matchImpl(pattern, offset, endpos, flags)
  506. if match.isNone:
  507. # either the end of the input or the string
  508. # cannot be split here - we also need to bail
  509. # if we've never matched and we've already tried to...
  510. if flags == 0 or offset >= strlen or neverMatched: # All matches found
  511. break
  512. if matchesCrLf and offset < (str.len - 1) and
  513. str[offset] == '\r' and str[offset + 1] == '\L':
  514. # if PCRE treats CrLf as newline, skip both at the same time
  515. offset += 2
  516. elif unicode:
  517. # XXX what about invalid unicode?
  518. offset += str.runeLenAt(offset)
  519. assert(offset <= strlen)
  520. else:
  521. offset += 1
  522. else:
  523. neverMatched = false
  524. offset = match.get.matchBounds.b + 1
  525. yield match.get
  526. proc find*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] =
  527. ## Finds the given pattern in the string between the end and start
  528. ## positions.
  529. ##
  530. ## `start`
  531. ## : The start point at which to start matching. `|abc` is `0`;
  532. ## `a|bc` is `1`
  533. ##
  534. ## `endpos`
  535. ## : The maximum index for a match; `int.high` means the end of the
  536. ## string, otherwise it’s an inclusive upper bound.
  537. return str.matchImpl(pattern, start, endpos, 0)
  538. proc findAll*(str: string, pattern: Regex, start = 0, endpos = int.high): seq[string] =
  539. result = @[]
  540. for match in str.findIter(pattern, start, endpos):
  541. result.add(match.match)
  542. proc contains*(str: string, pattern: Regex, start = 0, endpos = int.high): bool =
  543. ## Determine if the string contains the given pattern between the end and
  544. ## start positions:
  545. ## This function is equivalent to `isSome(str.find(pattern, start, endpos))`.
  546. runnableExamples:
  547. assert "abc".contains(re"bc")
  548. assert not "abc".contains(re"cd")
  549. assert not "abc".contains(re"a", start = 1)
  550. return isSome(str.find(pattern, start, endpos))
  551. proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string] =
  552. ## Splits the string with the given regex. This works according to the
  553. ## rules that Perl and Javascript use.
  554. ##
  555. ## `start` behaves the same as in `find(...)<#find,string,Regex,int>`_.
  556. ##
  557. runnableExamples:
  558. # - If the match is zero-width, then the string is still split:
  559. assert "123".split(re"") == @["1", "2", "3"]
  560. # - If the pattern has a capture in it, it is added after the string
  561. # split:
  562. assert "12".split(re"(\d)") == @["", "1", "", "2", ""]
  563. # - If `maxsplit != -1`, then the string will only be split
  564. # `maxsplit - 1` times. This means that there will be `maxsplit`
  565. # strings in the output seq.
  566. assert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"]
  567. result = @[]
  568. var lastIdx = start
  569. var splits = 0
  570. var bounds = 0 .. -1
  571. var never_ran = true
  572. for match in str.findIter(pattern, start = start):
  573. never_ran = false
  574. # bounds are inclusive:
  575. #
  576. # 0123456
  577. # ^^^
  578. # (1, 3)
  579. bounds = match.matchBounds
  580. # "12".split("") would be @["", "1", "2"], but
  581. # if we skip an empty first match, it's the correct
  582. # @["1", "2"]
  583. if bounds.a <= bounds.b or bounds.a > start:
  584. result.add(str.substr(lastIdx, bounds.a - 1))
  585. splits += 1
  586. lastIdx = bounds.b + 1
  587. for cap in match.captures:
  588. # if there are captures, include them in the result
  589. if cap.isSome:
  590. result.add(cap.get)
  591. if splits == maxSplit - 1:
  592. break
  593. # "12".split("\b") would be @["1", "2", ""], but
  594. # if we skip an empty last match, it's the correct
  595. # @["1", "2"]
  596. # If matches were never found, then the input string is the result
  597. if bounds.a <= bounds.b or bounds.b < str.high or never_ran:
  598. # last match: Each match takes the previous substring,
  599. # but "1 2".split(/ /) needs to return @["1", "2"].
  600. # This handles "2"
  601. result.add(str.substr(bounds.b + 1, str.high))
  602. template replaceImpl(str: string, pattern: Regex,
  603. replacement: untyped) {.dirty.} =
  604. # XXX seems very similar to split, maybe I can reduce code duplication
  605. # somehow?
  606. result = ""
  607. var lastIdx = 0
  608. for match {.inject.} in str.findIter(pattern):
  609. let bounds = match.matchBounds
  610. result.add(str.substr(lastIdx, bounds.a - 1))
  611. let nextVal = replacement
  612. result.add(nextVal)
  613. lastIdx = bounds.b + 1
  614. result.add(str.substr(lastIdx, str.len - 1))
  615. return result
  616. proc replace*(str: string, pattern: Regex,
  617. subproc: proc (match: RegexMatch): string): string =
  618. ## Replaces each match of Regex in the string with `subproc`, which should
  619. ## never be or return `nil`.
  620. ##
  621. ## If `subproc` is a `proc (RegexMatch): string`, then it is executed with
  622. ## each match and the return value is the replacement value.
  623. ##
  624. ## If `subproc` is a `proc (string): string`, then it is executed with the
  625. ## full text of the match and the return value is the replacement value.
  626. ##
  627. ## If `subproc` is a string, the syntax is as follows:
  628. ##
  629. ## - `$$` - literal `$`
  630. ## - `$123` - capture number `123`
  631. ## - `$foo` - named capture `foo`
  632. ## - `${foo}` - same as above
  633. ## - `$1$#` - first and second captures
  634. ## - `$#` - first capture
  635. ## - `$0` - full match
  636. ##
  637. ## If a given capture is missing, `IndexDefect` thrown for un-named captures
  638. ## and `KeyError` for named captures.
  639. replaceImpl(str, pattern, subproc(match))
  640. proc replace*(str: string, pattern: Regex,
  641. subproc: proc (match: string): string): string =
  642. replaceImpl(str, pattern, subproc(match.match))
  643. proc replace*(str: string, pattern: Regex, sub: string): string =
  644. # - 1 because the string numbers are 0-indexed
  645. replaceImpl(str, pattern,
  646. formatStr(sub, match.captures[name], match.captures[id - 1]))
  647. proc escapeRe*(str: string): string {.gcsafe.} =
  648. ## Escapes the string so it doesn't match any special characters.
  649. ## Incompatible with the Extra flag (`X`).
  650. ##
  651. ## Escaped char: `\ + * ? [ ^ ] $ ( ) { } = ! < > | : -`
  652. runnableExamples:
  653. assert escapeRe("fly+wind") == "fly\\+wind"
  654. assert escapeRe("!") == "\\!"
  655. assert escapeRe("nim*") == "nim\\*"
  656. #([\\+*?[^\]$(){}=!<>|:-])
  657. const SpecialCharMatcher = {'\\', '+', '*', '?', '[', '^', ']', '$', '(',
  658. ')', '{', '}', '=', '!', '<', '>', '|', ':',
  659. '-'}
  660. result = ""
  661. for c in items(str):
  662. case c
  663. of SpecialCharMatcher:
  664. result.add("\\")
  665. result.add(c)
  666. else:
  667. result.add(c)