parseopt.nim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module provides the standard Nim command line parser.
  10. ## It supports one convenience iterator over all command line options and some
  11. ## lower-level features.
  12. ##
  13. ## Supported Syntax
  14. ## ================
  15. ##
  16. ## The following syntax is supported when arguments for the `shortNoVal` and
  17. ## `longNoVal` parameters, which are
  18. ## `described later<#nimshortnoval-and-nimlongnoval>`_, are not provided:
  19. ##
  20. ## 1. Short options: `-abcd`, `-e:5`, `-e=5`
  21. ## 2. Long options: `--foo:bar`, `--foo=bar`, `--foo`
  22. ## 3. Arguments: everything that does not start with a `-`
  23. ##
  24. ## These three kinds of tokens are enumerated in the
  25. ## `CmdLineKind enum<#CmdLineKind>`_.
  26. ##
  27. ## When option values begin with ':' or '=', they need to be doubled up (as in
  28. ## `--delim::`) or alternated (as in `--delim=:`).
  29. ##
  30. ## The `--` option, commonly used to denote that every token that follows is
  31. ## an argument, is interpreted as a long option, and its name is the empty
  32. ## string.
  33. ##
  34. ## Parsing
  35. ## =======
  36. ##
  37. ## Use an `OptParser<#OptParser>`_ to parse command line options. It can be
  38. ## created with `initOptParser<#initOptParser,string,set[char],seq[string]>`_,
  39. ## and `next<#next,OptParser>`_ advances the parser by one token.
  40. ##
  41. ## For each token, the parser's `kind`, `key`, and `val` fields give
  42. ## information about that token. If the token is a long or short option, `key`
  43. ## is the option's name, and `val` is either the option's value, if provided,
  44. ## or the empty string. For arguments, the `key` field contains the argument
  45. ## itself, and `val` is unused. To check if the end of the command line has
  46. ## been reached, check if `kind` is equal to `cmdEnd`.
  47. ##
  48. ## Here is an example:
  49. ##
  50. ## ```Nim
  51. ## import std/parseopt
  52. ##
  53. ## var p = initOptParser("-ab -e:5 --foo --bar=20 file.txt")
  54. ## while true:
  55. ## p.next()
  56. ## case p.kind
  57. ## of cmdEnd: break
  58. ## of cmdShortOption, cmdLongOption:
  59. ## if p.val == "":
  60. ## echo "Option: ", p.key
  61. ## else:
  62. ## echo "Option and value: ", p.key, ", ", p.val
  63. ## of cmdArgument:
  64. ## echo "Argument: ", p.key
  65. ##
  66. ## # Output:
  67. ## # Option: a
  68. ## # Option: b
  69. ## # Option and value: e, 5
  70. ## # Option: foo
  71. ## # Option and value: bar, 20
  72. ## # Argument: file.txt
  73. ## ```
  74. ##
  75. ## The `getopt iterator<#getopt.i,OptParser>`_, which is provided for
  76. ## convenience, can be used to iterate through all command line options as well.
  77. ##
  78. ## To set a default value for a variable assigned through `getopt` and accept arguments from the cmd line.
  79. ## Assign the default value to a variable before parsing.
  80. ## Then set the variable to the new value while parsing.
  81. ##
  82. ## Here is an example:
  83. ##
  84. ## ```Nim
  85. ## import std/parseopt
  86. ##
  87. ## var varName: string = "defaultValue"
  88. ##
  89. ## for kind, key, val in getopt():
  90. ## case kind
  91. ## of cmdArgument:
  92. ## discard
  93. ## of cmdLongOption, cmdShortOption:
  94. ## case key:
  95. ## of "varName": # --varName:<value> in the console when executing
  96. ## varName = val # do input sanitization in production systems
  97. ## of cmdEnd:
  98. ## discard
  99. ## ```
  100. ##
  101. ## `shortNoVal` and `longNoVal`
  102. ## ============================
  103. ##
  104. ## The optional `shortNoVal` and `longNoVal` parameters present in
  105. ## `initOptParser<#initOptParser,string,set[char],seq[string]>`_ are for
  106. ## specifying which short and long options do not accept values.
  107. ##
  108. ## When `shortNoVal` is non-empty, users are not required to separate short
  109. ## options and their values with a ':' or '=' since the parser knows which
  110. ## options accept values and which ones do not. This behavior also applies for
  111. ## long options if `longNoVal` is non-empty. For short options, `-j4`
  112. ## becomes supported syntax, and for long options, `--foo bar` becomes
  113. ## supported. This is in addition to the `previously mentioned
  114. ## syntax<#supported-syntax>`_. Users can still separate options and their
  115. ## values with ':' or '=', but that becomes optional.
  116. ##
  117. ## As more options which do not accept values are added to your program,
  118. ## remember to amend `shortNoVal` and `longNoVal` accordingly.
  119. ##
  120. ## The following example illustrates the difference between having an empty
  121. ## `shortNoVal` and `longNoVal`, which is the default, and providing
  122. ## arguments for those two parameters:
  123. ##
  124. ## ```Nim
  125. ## import std/parseopt
  126. ##
  127. ## proc printToken(kind: CmdLineKind, key: string, val: string) =
  128. ## case kind
  129. ## of cmdEnd: doAssert(false) # Doesn't happen with getopt()
  130. ## of cmdShortOption, cmdLongOption:
  131. ## if val == "":
  132. ## echo "Option: ", key
  133. ## else:
  134. ## echo "Option and value: ", key, ", ", val
  135. ## of cmdArgument:
  136. ## echo "Argument: ", key
  137. ##
  138. ## let cmdLine = "-j4 --first bar"
  139. ##
  140. ## var emptyNoVal = initOptParser(cmdLine)
  141. ## for kind, key, val in emptyNoVal.getopt():
  142. ## printToken(kind, key, val)
  143. ##
  144. ## # Output:
  145. ## # Option: j
  146. ## # Option: 4
  147. ## # Option: first
  148. ## # Argument: bar
  149. ##
  150. ## var withNoVal = initOptParser(cmdLine, shortNoVal = {'c'},
  151. ## longNoVal = @["second"])
  152. ## for kind, key, val in withNoVal.getopt():
  153. ## printToken(kind, key, val)
  154. ##
  155. ## # Output:
  156. ## # Option and value: j, 4
  157. ## # Option and value: first, bar
  158. ## ```
  159. ##
  160. ## See also
  161. ## ========
  162. ##
  163. ## * `os module<os.html>`_ for lower-level command line parsing procs
  164. ## * `parseutils module<parseutils.html>`_ for helpers that parse tokens,
  165. ## numbers, identifiers, etc.
  166. ## * `strutils module<strutils.html>`_ for common string handling operations
  167. ## * `json module<json.html>`_ for a JSON parser
  168. ## * `parsecfg module<parsecfg.html>`_ for a configuration file parser
  169. ## * `parsecsv module<parsecsv.html>`_ for a simple CSV (comma separated value)
  170. ## parser
  171. ## * `parsexml module<parsexml.html>`_ for a XML / HTML parser
  172. ## * `other parsers<lib.html#pure-libraries-parsers>`_ for more parsers
  173. {.push debugger: off.}
  174. include "system/inclrtl"
  175. import std/strutils
  176. import std/os
  177. type
  178. CmdLineKind* = enum ## The detected command line token.
  179. cmdEnd, ## End of command line reached
  180. cmdArgument, ## An argument such as a filename
  181. cmdLongOption, ## A long option such as --option
  182. cmdShortOption ## A short option such as -c
  183. OptParser* = object of RootObj ## \
  184. ## Implementation of the command line parser.
  185. ##
  186. ## To initialize it, use the
  187. ## `initOptParser proc<#initOptParser,string,set[char],seq[string]>`_.
  188. pos: int
  189. inShortState: bool
  190. allowWhitespaceAfterColon: bool
  191. shortNoVal: set[char]
  192. longNoVal: seq[string]
  193. cmds: seq[string]
  194. idx: int
  195. kind*: CmdLineKind ## The detected command line token
  196. key*, val*: string ## Key and value pair; the key is the option
  197. ## or the argument, and the value is not "" if
  198. ## the option was given a value
  199. proc parseWord(s: string, i: int, w: var string,
  200. delim: set[char] = {'\t', ' '}): int =
  201. result = i
  202. if result < s.len and s[result] == '\"':
  203. inc(result)
  204. while result < s.len:
  205. if s[result] == '"':
  206. inc result
  207. break
  208. add(w, s[result])
  209. inc(result)
  210. else:
  211. while result < s.len and s[result] notin delim:
  212. add(w, s[result])
  213. inc(result)
  214. proc initOptParser*(cmdline: seq[string], shortNoVal: set[char] = {},
  215. longNoVal: seq[string] = @[];
  216. allowWhitespaceAfterColon = true): OptParser =
  217. ## Initializes the command line parser.
  218. ##
  219. ## If `cmdline.len == 0`, the real command line as provided by the
  220. ## `os` module is retrieved instead if it is available. If the
  221. ## command line is not available, a `ValueError` will be raised.
  222. ## Behavior of the other parameters remains the same as in
  223. ## `initOptParser(string, ...)
  224. ## <#initOptParser,string,set[char],seq[string]>`_.
  225. ##
  226. ## See also:
  227. ## * `getopt iterator<#getopt.i,seq[string],set[char],seq[string]>`_
  228. runnableExamples:
  229. var p = initOptParser()
  230. p = initOptParser(@["--left", "--debug:3", "-l", "-r:2"])
  231. p = initOptParser(@["--left", "--debug:3", "-l", "-r:2"],
  232. shortNoVal = {'l'}, longNoVal = @["left"])
  233. result.pos = 0
  234. result.idx = 0
  235. result.inShortState = false
  236. result.shortNoVal = shortNoVal
  237. result.longNoVal = longNoVal
  238. result.allowWhitespaceAfterColon = allowWhitespaceAfterColon
  239. if cmdline.len != 0:
  240. result.cmds = newSeq[string](cmdline.len)
  241. for i in 0..<cmdline.len:
  242. result.cmds[i] = cmdline[i]
  243. else:
  244. when declared(paramCount):
  245. when defined(nimscript):
  246. var ctr = 0
  247. var firstNimsFound = false
  248. for i in countup(0, paramCount()):
  249. if firstNimsFound:
  250. result.cmds[ctr] = paramStr(i)
  251. inc ctr, 1
  252. if paramStr(i).endsWith(".nims") and not firstNimsFound:
  253. firstNimsFound = true
  254. result.cmds = newSeq[string](paramCount()-i)
  255. else:
  256. result.cmds = newSeq[string](paramCount())
  257. for i in countup(1, paramCount()):
  258. result.cmds[i-1] = paramStr(i)
  259. else:
  260. # we cannot provide this for NimRtl creation on Posix, because we can't
  261. # access the command line arguments then!
  262. raiseAssert "empty command line given but" &
  263. " real command line is not accessible"
  264. result.kind = cmdEnd
  265. result.key = ""
  266. result.val = ""
  267. proc initOptParser*(cmdline = "", shortNoVal: set[char] = {},
  268. longNoVal: seq[string] = @[];
  269. allowWhitespaceAfterColon = true): OptParser =
  270. ## Initializes the command line parser.
  271. ##
  272. ## If `cmdline == ""`, the real command line as provided by the
  273. ## `os` module is retrieved instead if it is available. If the
  274. ## command line is not available, a `ValueError` will be raised.
  275. ##
  276. ## `shortNoVal` and `longNoVal` are used to specify which options
  277. ## do not take values. See the `documentation about these
  278. ## parameters<#nimshortnoval-and-nimlongnoval>`_ for more information on
  279. ## how this affects parsing.
  280. ##
  281. ## This does not provide a way of passing default values to arguments.
  282. ##
  283. ## See also:
  284. ## * `getopt iterator<#getopt.i,OptParser>`_
  285. runnableExamples:
  286. var p = initOptParser()
  287. p = initOptParser("--left --debug:3 -l -r:2")
  288. p = initOptParser("--left --debug:3 -l -r:2",
  289. shortNoVal = {'l'}, longNoVal = @["left"])
  290. initOptParser(parseCmdLine(cmdline), shortNoVal, longNoVal, allowWhitespaceAfterColon)
  291. proc handleShortOption(p: var OptParser; cmd: string) =
  292. var i = p.pos
  293. p.kind = cmdShortOption
  294. if i < cmd.len:
  295. add(p.key, cmd[i])
  296. inc(i)
  297. p.inShortState = true
  298. while i < cmd.len and cmd[i] in {'\t', ' '}:
  299. inc(i)
  300. p.inShortState = false
  301. if i < cmd.len and (cmd[i] in {':', '='} or
  302. card(p.shortNoVal) > 0 and p.key[0] notin p.shortNoVal):
  303. if i < cmd.len and cmd[i] in {':', '='}:
  304. inc(i)
  305. p.inShortState = false
  306. while i < cmd.len and cmd[i] in {'\t', ' '}: inc(i)
  307. p.val = substr(cmd, i)
  308. p.pos = 0
  309. inc p.idx
  310. else:
  311. p.pos = i
  312. if i >= cmd.len:
  313. p.inShortState = false
  314. p.pos = 0
  315. inc p.idx
  316. proc next*(p: var OptParser) {.rtl, extern: "npo$1".} =
  317. ## Parses the next token.
  318. ##
  319. ## `p.kind` describes what kind of token has been parsed. `p.key` and
  320. ## `p.val` are set accordingly.
  321. runnableExamples:
  322. var p = initOptParser("--left -r:2 file.txt")
  323. p.next()
  324. doAssert p.kind == cmdLongOption and p.key == "left"
  325. p.next()
  326. doAssert p.kind == cmdShortOption and p.key == "r" and p.val == "2"
  327. p.next()
  328. doAssert p.kind == cmdArgument and p.key == "file.txt"
  329. p.next()
  330. doAssert p.kind == cmdEnd
  331. if p.idx >= p.cmds.len:
  332. p.kind = cmdEnd
  333. return
  334. var i = p.pos
  335. while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i)
  336. p.pos = i
  337. setLen(p.key, 0)
  338. setLen(p.val, 0)
  339. if p.inShortState:
  340. p.inShortState = false
  341. if i >= p.cmds[p.idx].len:
  342. inc(p.idx)
  343. p.pos = 0
  344. if p.idx >= p.cmds.len:
  345. p.kind = cmdEnd
  346. return
  347. else:
  348. handleShortOption(p, p.cmds[p.idx])
  349. return
  350. if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-':
  351. inc(i)
  352. if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-':
  353. p.kind = cmdLongOption
  354. inc(i)
  355. i = parseWord(p.cmds[p.idx], i, p.key, {' ', '\t', ':', '='})
  356. while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i)
  357. if i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {':', '='}:
  358. inc(i)
  359. while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i)
  360. # if we're at the end, use the next command line option:
  361. if i >= p.cmds[p.idx].len and p.idx < p.cmds.len and
  362. p.allowWhitespaceAfterColon:
  363. inc p.idx
  364. i = 0
  365. if p.idx < p.cmds.len:
  366. p.val = p.cmds[p.idx].substr(i)
  367. elif len(p.longNoVal) > 0 and p.key notin p.longNoVal and p.idx+1 < p.cmds.len:
  368. p.val = p.cmds[p.idx+1]
  369. inc p.idx
  370. else:
  371. p.val = ""
  372. inc p.idx
  373. p.pos = 0
  374. else:
  375. p.pos = i
  376. handleShortOption(p, p.cmds[p.idx])
  377. else:
  378. p.kind = cmdArgument
  379. p.key = p.cmds[p.idx]
  380. inc p.idx
  381. p.pos = 0
  382. when declared(quoteShellCommand):
  383. proc cmdLineRest*(p: OptParser): string {.rtl, extern: "npo$1".} =
  384. ## Retrieves the rest of the command line that has not been parsed yet.
  385. ##
  386. ## See also:
  387. ## * `remainingArgs proc<#remainingArgs,OptParser>`_
  388. ##
  389. ## **Examples:**
  390. ## ```Nim
  391. ## var p = initOptParser("--left -r:2 -- foo.txt bar.txt")
  392. ## while true:
  393. ## p.next()
  394. ## if p.kind == cmdLongOption and p.key == "": # Look for "--"
  395. ## break
  396. ## doAssert p.cmdLineRest == "foo.txt bar.txt"
  397. ## ```
  398. result = p.cmds[p.idx .. ^1].quoteShellCommand
  399. proc remainingArgs*(p: OptParser): seq[string] {.rtl, extern: "npo$1".} =
  400. ## Retrieves a sequence of the arguments that have not been parsed yet.
  401. ##
  402. ## See also:
  403. ## * `cmdLineRest proc<#cmdLineRest,OptParser>`_
  404. ##
  405. ## **Examples:**
  406. ## ```Nim
  407. ## var p = initOptParser("--left -r:2 -- foo.txt bar.txt")
  408. ## while true:
  409. ## p.next()
  410. ## if p.kind == cmdLongOption and p.key == "": # Look for "--"
  411. ## break
  412. ## doAssert p.remainingArgs == @["foo.txt", "bar.txt"]
  413. ## ```
  414. result = @[]
  415. for i in p.idx..<p.cmds.len: result.add p.cmds[i]
  416. iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key,
  417. val: string] =
  418. ## Convenience iterator for iterating over the given
  419. ## `OptParser<#OptParser>`_.
  420. ##
  421. ## There is no need to check for `cmdEnd` while iterating. If using `getopt`
  422. ## with case switching, checking for `cmdEnd` is required.
  423. ##
  424. ## See also:
  425. ## * `initOptParser proc<#initOptParser,string,set[char],seq[string]>`_
  426. ##
  427. ## **Examples:**
  428. ##
  429. ## ```Nim
  430. ## # these are placeholders, of course
  431. ## proc writeHelp() = discard
  432. ## proc writeVersion() = discard
  433. ##
  434. ## var filename: string
  435. ## var p = initOptParser("--left --debug:3 -l -r:2")
  436. ##
  437. ## for kind, key, val in p.getopt():
  438. ## case kind
  439. ## of cmdArgument:
  440. ## filename = key
  441. ## of cmdLongOption, cmdShortOption:
  442. ## case key
  443. ## of "help", "h": writeHelp()
  444. ## of "version", "v": writeVersion()
  445. ## of cmdEnd: assert(false) # cannot happen
  446. ## if filename == "":
  447. ## # no filename has been given, so we show the help
  448. ## writeHelp()
  449. ## ```
  450. p.pos = 0
  451. p.idx = 0
  452. while true:
  453. next(p)
  454. if p.kind == cmdEnd: break
  455. yield (p.kind, p.key, p.val)
  456. iterator getopt*(cmdline: seq[string] = @[],
  457. shortNoVal: set[char] = {}, longNoVal: seq[string] = @[]):
  458. tuple[kind: CmdLineKind, key, val: string] =
  459. ## Convenience iterator for iterating over command line arguments.
  460. ##
  461. ## This creates a new `OptParser<#OptParser>`_. If no command line
  462. ## arguments are provided, the real command line as provided by the
  463. ## `os` module is retrieved instead.
  464. ##
  465. ## `shortNoVal` and `longNoVal` are used to specify which options
  466. ## do not take values. See the `documentation about these
  467. ## parameters<#nimshortnoval-and-nimlongnoval>`_ for more information on
  468. ## how this affects parsing.
  469. ##
  470. ## There is no need to check for `cmdEnd` while iterating. If using `getopt`
  471. ## with case switching, checking for `cmdEnd` is required.
  472. ##
  473. ## See also:
  474. ## * `initOptParser proc<#initOptParser,seq[string],set[char],seq[string]>`_
  475. ##
  476. ## **Examples:**
  477. ##
  478. ## ```Nim
  479. ## # these are placeholders, of course
  480. ## proc writeHelp() = discard
  481. ## proc writeVersion() = discard
  482. ##
  483. ## var filename: string
  484. ## let params = @["--left", "--debug:3", "-l", "-r:2"]
  485. ##
  486. ## for kind, key, val in getopt(params):
  487. ## case kind
  488. ## of cmdArgument:
  489. ## filename = key
  490. ## of cmdLongOption, cmdShortOption:
  491. ## case key
  492. ## of "help", "h": writeHelp()
  493. ## of "version", "v": writeVersion()
  494. ## of cmdEnd: assert(false) # cannot happen
  495. ## if filename == "":
  496. ## # no filename has been written, so we show the help
  497. ## writeHelp()
  498. ## ```
  499. var p = initOptParser(cmdline, shortNoVal = shortNoVal,
  500. longNoVal = longNoVal)
  501. while true:
  502. next(p)
  503. if p.kind == cmdEnd: break
  504. yield (p.kind, p.key, p.val)
  505. {.pop.}