argument_parser.nim 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. ## Command line parsing module for Nim.
  2. ##
  3. ## `Nim <http://nim-lang.org>`_ provides the `parseopt module
  4. ## <http://nim-lang.org/parseopt.html>`_ to parse options from the
  5. ## commandline. This module tries to provide functionality to prevent you from
  6. ## writing commandline parsing and let you concentrate on providing the best
  7. ## possible experience for your users.
  8. ##
  9. ## Source code for this module can be found at
  10. ## https://github.com/gradha/argument_parser.
  11. import os, strutils, tables, math, parseutils, sequtils, sets, algorithm,
  12. unicode
  13. const
  14. VERSION_STR* = "0.1.2" ## Module version as a string.
  15. VERSION_INT* = (major: 0, minor: 1, maintenance: 2) ## \
  16. ## Module version as an integer tuple.
  17. ##
  18. ## Major versions changes mean a break in API backwards compatibility, either
  19. ## through removal of symbols or modification of their purpose.
  20. ##
  21. ## Minor version changes can add procs (and maybe default parameters). Minor
  22. ## odd versions are development/git/unstable versions. Minor even versions
  23. ## are public stable releases.
  24. ##
  25. ## Maintenance version changes mean bugfixes or non API changes.
  26. # - Types
  27. type
  28. Tparam_kind* = enum ## Different types of results for parameter parsing.
  29. PK_EMPTY, PK_INT, PK_FLOAT, PK_STRING, PK_BOOL,
  30. PK_BIGGEST_INT, PK_BIGGEST_FLOAT, PK_HELP
  31. Tparameter_callback* =
  32. proc (parameter: string; value: var Tparsed_parameter): string ## \
  33. ## Prototype of parameter callbacks
  34. ##
  35. ## A parameter callback is just a custom proc you provide which is invoked
  36. ## after a parameter is parsed passing the basic type validation. The
  37. ## `parameter` parameter is the string which triggered the option. The
  38. ## `value` parameter contains the string passed by the user already parsed
  39. ## into the basic type you specified for it.
  40. ##
  41. ## The callback proc has modification access to the Tparsed_parameter
  42. ## `value` parameter that will be put into Tcommandline_results: you can
  43. ## read it and also modify it, maybe changing its type. In fact, if you
  44. ## need special parsing, most likely you will end up specifying PK_STRING
  45. ## in the parameter input specification so that the parse() proc doesn't
  46. ## *mangle* the string before you can process it yourself.
  47. ##
  48. ## If the callback decides to abort the validation of the parameter, it has
  49. ## to put into result a non zero length string with a message for the user
  50. ## explaining why the validation failed, and maybe offer a hint as to what
  51. ## can be done to pass validation.
  52. Tparameter_specification* = object ## \
  53. ## Holds the expectations of a parameter.
  54. ##
  55. ## You create these objects and feed them to the parse() proc, which then
  56. ## uses them to detect parameters and turn them into something uself.
  57. names*: seq[string] ## List of possible parameters to catch for this.
  58. consumes*: Tparam_kind ## Expected type of the parameter (empty for none)
  59. custom_validator*: Tparameter_callback ## Optional custom callback
  60. ## to run after type conversion.
  61. help_text*: string ## Help for this group of parameters.
  62. Tparsed_parameter* = object ## \
  63. ## Contains the parsed value from the user.
  64. ##
  65. ## This implements an object variant through the kind field. You can 'case'
  66. ## this field to write a generic proc to deal with parsed parameters, but
  67. ## nothing prevents you from accessing directly the type of field you want
  68. ## if you expect only one kind.
  69. case kind*: Tparam_kind
  70. of PK_EMPTY: discard
  71. of PK_INT: int_val*: int
  72. of PK_BIGGEST_INT: big_int_val*: BiggestInt
  73. of PK_FLOAT: float_val*: float
  74. of PK_BIGGEST_FLOAT: big_float_val*: BiggestFloat
  75. of PK_STRING: str_val*: string
  76. of PK_BOOL: bool_val*: bool
  77. of PK_HELP: discard
  78. Tcommandline_results* = object of RootObj ## \
  79. ## Contains the results of the parsing.
  80. ##
  81. ## Usually this is the result of the parse() call, but you can inherit from
  82. ## it to add your own fields for convenience.
  83. ##
  84. ## Note that you always have to access the ``options`` ordered table with
  85. ## the first variant of a parameter name. For instance, if you have an
  86. ## option specified like ``@["-s", "--silent"]`` and the user types
  87. ## ``--silent`` at the commandline, you have to use
  88. ## ``options.hasKey("-s")`` to test for it. This standarizes access through
  89. ## the first name variant for all options to avoid you repeating the test
  90. ## with different keys.
  91. positional_parameters*: seq[Tparsed_parameter]
  92. options*: OrderedTable[string, Tparsed_parameter]
  93. # - Tparam_kind procs
  94. proc `$`*(value: Tparam_kind): string =
  95. ## Stringifies the type, used to generate help texts.
  96. case value:
  97. of PK_EMPTY: result = ""
  98. of PK_INT: result = "INT"
  99. of PK_BIGGEST_INT: result = "BIG_INT"
  100. of PK_FLOAT: result = "FLOAT"
  101. of PK_BIGGEST_FLOAT: result = "BIG_FLOAG"
  102. of PK_STRING: result = "STRING"
  103. of PK_BOOL: result = "BOOL"
  104. of PK_HELP: result = ""
  105. # - Tparameter_specification procs
  106. proc init*(param: var Tparameter_specification, consumes = PK_EMPTY,
  107. custom_validator: Tparameter_callback = nil, help_text = "",
  108. names: varargs[string]) =
  109. ## Initialization helper with default parameters.
  110. ##
  111. ## You can decide to miss some if you like the defaults, reducing code. You
  112. ## can also use new_parameter_specification() for single assignment
  113. ## variables.
  114. param.names = @names
  115. param.consumes = consumes
  116. param.custom_validator = custom_validator
  117. param.help_text = help_text
  118. proc new_parameter_specification*(consumes = PK_EMPTY,
  119. custom_validator: Tparameter_callback = nil, help_text = "",
  120. names: varargs[string]): Tparameter_specification =
  121. ## Initialization helper for single assignment variables.
  122. result.init(consumes, custom_validator, help_text, names)
  123. # - Tparsed_parameter procs
  124. proc `$`*(data: Tparsed_parameter): string =
  125. ## Stringifies the value, mostly for debug purposes.
  126. ##
  127. ## The proc will display the value followed by non string type in brackets.
  128. ## The non string types would be PK_INT (i), PK_BIGGEST_INT (I), PK_FLOAT
  129. ## (f), PK_BIGGEST_FLOAT (F), PK_BOOL (b). The string type would be enclosed
  130. ## inside quotes. PK_EMPTY produces the word `nil`, and PK_HELP produces the
  131. ## world `help`.
  132. case data.kind:
  133. of PK_EMPTY: result = "nil"
  134. of PK_INT: result = "$1(i)" % $data.int_val
  135. of PK_BIGGEST_INT: result = "$1(I)" % $data.big_int_val
  136. of PK_FLOAT: result = "$1(f)" % $data.float_val
  137. of PK_BIGGEST_FLOAT: result = "$1(F)" % $data.big_float_val
  138. of PK_STRING: result = "\"" & $data.str_val & "\""
  139. of PK_BOOL: result = "$1(b)" % $data.bool_val
  140. of PK_HELP: result = "help"
  141. template new_parsed_parameter*(tkind: Tparam_kind, expr): Tparsed_parameter =
  142. ## Handy compile time template to build Tparsed_parameter object variants.
  143. ##
  144. ## The problem with object variants is that you first have to initialise them
  145. ## to a kind, then assign values to the correct variable, and it is a little
  146. ## bit annoying.
  147. ##
  148. ## Through this template you specify as the first parameter the kind of the
  149. ## Tparsed_parameter you want to build, and directly the value it will be
  150. ## initialised with. The template figures out at compile time what field to
  151. ## assign the variable to, and thus you reduce code clutter and may use this
  152. ## to initialise single assignments variables in `let` blocks. Example:
  153. ## ```nim
  154. ## let
  155. ## parsed_param1 = new_parsed_parameter(PK_FLOAT, 3.41)
  156. ## parsed_param2 = new_parsed_parameter(PK_BIGGEST_INT, 2358123 * 23123)
  157. ## # The following line doesn't compile due to
  158. ## # type mismatch: got <string> but expected 'int'
  159. ## #parsed_param3 = new_parsed_parameter(PK_INT, "231")
  160. ## ```
  161. var result {.gensym.}: Tparsed_parameter
  162. result.kind = tkind
  163. when tkind == PK_EMPTY: discard
  164. elif tkind == PK_INT: result.int_val = expr
  165. elif tkind == PK_BIGGEST_INT: result.big_int_val = expr
  166. elif tkind == PK_FLOAT: result.float_val = expr
  167. elif tkind == PK_BIGGEST_FLOAT: result.big_float_val = expr
  168. elif tkind == PK_STRING: result.str_val = expr
  169. elif tkind == PK_BOOL: result.bool_val = expr
  170. elif tkind == PK_HELP: discard
  171. else: {.error: "unknown kind".}
  172. result
  173. # - Tcommandline_results procs
  174. proc init*(param: var Tcommandline_results;
  175. positional_parameters: seq[Tparsed_parameter] = @[];
  176. options: OrderedTable[string, Tparsed_parameter] =
  177. initOrderedTable[string, Tparsed_parameter](4)) =
  178. ## Initialization helper with default parameters.
  179. param.positional_parameters = positional_parameters
  180. param.options = options
  181. proc `$`*(data: Tcommandline_results): string =
  182. ## Stringifies a Tcommandline_results structure for debug output
  183. var dict: seq[string] = @[]
  184. for key, value in data.options:
  185. dict.add("$1: $2" % [escape(key), $value])
  186. result = "Tcommandline_result{positional_parameters:[$1], options:{$2}}" % [
  187. join(map(data.positional_parameters, `$`), ", "), join(dict, ", ")]
  188. # - Parse code
  189. template raise_or_quit(exception, message: untyped) =
  190. ## Avoids repeating if check based on the default quit_on_failure variable.
  191. ##
  192. ## As a special case, if message has a zero length the call to quit won't
  193. ## generate any messages or errors (used by the mechanism to echo help to the
  194. ## user).
  195. if quit_on_failure:
  196. if len(message) > 0:
  197. quit(message)
  198. else:
  199. quit()
  200. else:
  201. raise newException(exception, message)
  202. template run_custom_proc(parsed_parameter: Tparsed_parameter,
  203. custom_validator: Tparameter_callback,
  204. parameter: string) =
  205. ## Runs the custom validator if it is not nil.
  206. ##
  207. ## Pass in the string of the parameter triggering the call. If the
  208. if not custom_validator.isNil:
  209. try:
  210. let message = custom_validator(parameter, parsed_parameter)
  211. if message.len > 0:
  212. raise_or_quit(ValueError, ("Failed to validate value for " &
  213. "parameter $1:\n$2" % [escape(parameter), message]))
  214. except:
  215. raise_or_quit(ValueError, ("Couldn't run custom proc for " &
  216. "parameter $1:\n$2" % [escape(parameter),
  217. getCurrentExceptionMsg()]))
  218. proc parse_parameter(quit_on_failure: bool, param, value: string,
  219. param_kind: Tparam_kind): Tparsed_parameter =
  220. ## Tries to parse a text according to the specified type.
  221. ##
  222. ## Pass the parameter string which requires a value and the text the user
  223. ## passed in for it. It will be parsed according to the param_kind. This proc
  224. ## will raise (ValueError, EOverflow) if something can't be parsed.
  225. result.kind = param_kind
  226. case param_kind:
  227. of PK_INT:
  228. try: result.int_val = value.parseInt
  229. except OverflowDefect:
  230. raise_or_quit(OverflowDefect, ("parameter $1 requires an " &
  231. "integer, but $2 is too large to fit into one") % [param,
  232. escape(value)])
  233. except ValueError:
  234. raise_or_quit(ValueError, ("parameter $1 requires an " &
  235. "integer, but $2 can't be parsed into one") % [param, escape(value)])
  236. of PK_STRING:
  237. result.str_val = value
  238. of PK_FLOAT:
  239. try: result.float_val = value.parseFloat
  240. except ValueError:
  241. raise_or_quit(ValueError, ("parameter $1 requires a " &
  242. "float, but $2 can't be parsed into one") % [param, escape(value)])
  243. of PK_BOOL:
  244. try: result.bool_val = value.parseBool
  245. except ValueError:
  246. raise_or_quit(ValueError, ("parameter $1 requires a " &
  247. "boolean, but $2 can't be parsed into one. Valid values are: " &
  248. "y, yes, true, 1, on, n, no, false, 0, off") % [param, escape(value)])
  249. of PK_BIGGEST_INT:
  250. try:
  251. let parsed_len = parseBiggestInt(value, result.big_int_val)
  252. if value.len != parsed_len or parsed_len < 1:
  253. raise_or_quit(ValueError, ("parameter $1 requires an " &
  254. "integer, but $2 can't be parsed completely into one") % [
  255. param, escape(value)])
  256. except ValueError:
  257. raise_or_quit(ValueError, ("parameter $1 requires an " &
  258. "integer, but $2 can't be parsed into one") % [param, escape(value)])
  259. of PK_BIGGEST_FLOAT:
  260. try:
  261. let parsed_len = parseBiggestFloat(value, result.big_float_val)
  262. if value.len != parsed_len or parsed_len < 1:
  263. raise_or_quit(ValueError, ("parameter $1 requires a " &
  264. "float, but $2 can't be parsed completely into one") % [
  265. param, escape(value)])
  266. except ValueError:
  267. raise_or_quit(ValueError, ("parameter $1 requires a " &
  268. "float, but $2 can't be parsed into one") % [param, escape(value)])
  269. of PK_EMPTY:
  270. discard
  271. of PK_HELP:
  272. discard
  273. template build_specification_lookup():
  274. OrderedTable[string, ptr Tparameter_specification] =
  275. ## Returns the table used to keep pointers to all of the specifications.
  276. var result {.gensym.}: OrderedTable[string, ptr Tparameter_specification]
  277. result = initOrderedTable[string, ptr Tparameter_specification](expected.len)
  278. for i in 0..expected.len-1:
  279. for param_to_detect in expected[i].names:
  280. if result.hasKey(param_to_detect):
  281. raise_or_quit(KeyError,
  282. "Parameter $1 repeated in input specification" % param_to_detect)
  283. else:
  284. result[param_to_detect] = addr(expected[i])
  285. result
  286. proc echo_help*(expected: seq[Tparameter_specification] = @[],
  287. type_of_positional_parameters = PK_STRING,
  288. bad_prefixes = @["-", "--"], end_of_options = "--")
  289. proc parse*(expected: seq[Tparameter_specification] = @[],
  290. type_of_positional_parameters = PK_STRING, args: seq[string] = @[],
  291. bad_prefixes = @["-", "--"], end_of_options = "--",
  292. quit_on_failure = true): Tcommandline_results =
  293. ## Parses parameters and returns results.
  294. ##
  295. ## The expected array should contain a list of the parameters you want to
  296. ## detect, which can capture additional values. Uncaptured parameters are
  297. ## considered positional parameters for which you can specify a type with
  298. ## type_of_positional_parameters.
  299. ##
  300. ## Before accepting a positional parameter, the list of bad_prefixes is
  301. ## compared against it. If the positional parameter starts with any of them,
  302. ## an error is displayed to the user due to ambiguity. The user can overcome
  303. ## the ambiguity by typing the special string specified by end_of_options.
  304. ## Note that values captured by parameters are not checked against bad
  305. ## prefixes, otherwise it would be a problem to specify the dash as synonim
  306. ## for standard input for many programs.
  307. ##
  308. ## The args sequence should be the list of parameters passed to your program
  309. ## without the program binary (usually OSes provide the path to the binary as
  310. ## the zeroth parameter). If args is empty, the list will be retrieved from the
  311. ## OS.
  312. ##
  313. ## If there is any kind of error and quit_on_failure is true, the quit proc
  314. ## will be called with a user error message. If quit_on_failure is false
  315. ## errors will raise exceptions (usually ValueError or EOverflow) instead
  316. ## for you to catch and handle.
  317. assert type_of_positional_parameters != PK_EMPTY and
  318. type_of_positional_parameters != PK_HELP
  319. for bad_prefix in bad_prefixes:
  320. assert bad_prefix.len > 0, "Can't pass in a bad prefix of zero length"
  321. var
  322. expected = expected
  323. adding_options = true
  324. result.init()
  325. # Prepare the input parameter list, maybe get it from the OS if not available.
  326. var args = args
  327. if args.len == 0:
  328. let total_params = paramCount()
  329. #echo "Got no explicit args, retrieving from OS. Count: ", total_params
  330. newSeq(args, total_params)
  331. for i in 0..total_params - 1:
  332. #echo ($i)
  333. args[i] = paramStr(i + 1)
  334. # Generate lookup table for each type of parameter based on strings.
  335. var lookup = build_specification_lookup()
  336. # Loop through the input arguments detecting their type and doing stuff.
  337. var i = 0
  338. while i < args.len:
  339. let arg = args[i]
  340. block adding_positional_parameter:
  341. if arg.len > 0 and adding_options:
  342. if arg == end_of_options:
  343. # Looks like we found the end_of_options marker, disable options.
  344. adding_options = false
  345. break adding_positional_parameter
  346. elif lookup.hasKey(arg):
  347. var parsed: Tparsed_parameter
  348. let param = lookup[arg]
  349. # Insert check here for help, which aborts parsing.
  350. if param.consumes == PK_HELP:
  351. echo_help(expected, type_of_positional_parameters,
  352. bad_prefixes, end_of_options)
  353. raise_or_quit(KeyError, "")
  354. if param.consumes != PK_EMPTY:
  355. if i + 1 < args.len:
  356. parsed = parse_parameter(quit_on_failure,
  357. arg, args[i + 1], param.consumes)
  358. run_custom_proc(parsed, param.custom_validator, arg)
  359. i += 1
  360. else:
  361. raise_or_quit(ValueError, ("parameter $1 requires a " &
  362. "value, but none was provided") % [arg])
  363. result.options[param.names[0]] = parsed
  364. break adding_positional_parameter
  365. else:
  366. for bad_prefix in bad_prefixes:
  367. if arg.startsWith(bad_prefix):
  368. raise_or_quit(ValueError, ("Found ambiguos parameter '$1' " &
  369. "starting with '$2', put '$3' as the previous parameter " &
  370. "if you want to force it as positional parameter.") % [arg,
  371. bad_prefix, end_of_options])
  372. # Unprocessed, add the parameter to the list of positional parameters.
  373. result.positional_parameters.add(parse_parameter(quit_on_failure,
  374. $(1 + i), arg, type_of_positional_parameters))
  375. i += 1
  376. proc toString(runes: seq[Rune]): string =
  377. result = ""
  378. for rune in runes: result.add(rune.toUTF8)
  379. proc ascii_cmp(a, b: string): int =
  380. ## Comparison ignoring non ascii characters, for better switch sorting.
  381. let a = filterIt(toSeq(runes(a)), it.isAlpha())
  382. # Can't use filterIt twice, github bug #351.
  383. let b = filter(toSeq(runes(b)), proc(x: Rune): bool = x.isAlpha())
  384. return system.cmp(toString(a), toString(b))
  385. proc build_help*(expected: seq[Tparameter_specification] = @[],
  386. type_of_positional_parameters = PK_STRING,
  387. bad_prefixes = @["-", "--"], end_of_options = "--"): seq[string] =
  388. ## Builds basic help text and returns it as a sequence of strings.
  389. ##
  390. ## Note that this proc doesn't do as much sanity checks as the normal parse()
  391. ## proc, though it's unlikely you will be using one without the other, so if
  392. ## you had a parameter specification problem you would find out soon.
  393. result = @["Usage parameters: "]
  394. # Generate lookup table for each type of parameter based on strings.
  395. let quit_on_failure = false
  396. var
  397. expected = expected
  398. lookup = build_specification_lookup()
  399. keys = toSeq(lookup.keys())
  400. # First generate the joined version of input parameters in a list.
  401. var
  402. seen = initHashSet[string]()
  403. prefixes: seq[string] = @[]
  404. helps: seq[string] = @[]
  405. for key in keys:
  406. if seen.contains(key):
  407. continue
  408. # Add the joined string to the list.
  409. let param = lookup[key][]
  410. var param_names = param.names
  411. sort(param_names, ascii_cmp)
  412. var prefix = join(param_names, ", ")
  413. # Don't forget about the type, if the parameter consumes values
  414. if param.consumes != PK_EMPTY and param.consumes != PK_HELP:
  415. prefix &= " " & $param.consumes
  416. prefixes.add(prefix)
  417. helps.add(param.help_text)
  418. # Ignore future elements.
  419. for name in param.names: seen.incl(name)
  420. # Calculate the biggest width and try to use that
  421. let width = prefixes.map(proc (x: string): int = 3 + len(x)).max
  422. for line in zip(prefixes, helps):
  423. result.add(line[0] & spaces(width - line[0].len) & line[1])
  424. proc echo_help*(expected: seq[Tparameter_specification] = @[],
  425. type_of_positional_parameters = PK_STRING,
  426. bad_prefixes = @["-", "--"], end_of_options = "--") =
  427. ## Prints out help on the terminal.
  428. ##
  429. ## This is just a wrapper around build_help. Note that calling this proc
  430. ## won't exit your program, you should call quit() yourself.
  431. for line in build_help(expected,
  432. type_of_positional_parameters, bad_prefixes, end_of_options):
  433. echo line
  434. when true:
  435. # Simply tests code embedded in docs.
  436. let
  437. parsed_param1 = new_parsed_parameter(PK_FLOAT, 3.41)
  438. parsed_param2 = new_parsed_parameter(PK_BIGGEST_INT, 2358123 * 23123)
  439. #parsed_param3 = new_parsed_parameter(PK_INT, "231")