I just wanted to say this language is fantastic. I just finished a -- to me -- medium-sized project for work yesterday that took about a week (including modifying & wrapping a C library to replace httpclient for static compiling with SSL/TLS, but that's another story...) and I had decided to use the argparse module. Having never used it before, I didn't realize there are so many bugs in argparse that I needed duplicate code, have an extra page of if statements, and several loops to "fix" the parameters before argparse even saw it to keep it from crashing. My point is sometimes code has issues and isn't a perfect fit for ever use case - no fault of the argparse author - it's awesome it exists.
Speaking of perfect fit for my use case, today I decided to replace argparse with good old parseopt from the standard library. parseopt is a little low level, but with less than 20 LOC across 2 simple templates, I had a lean CLI framework that did exactly what I wanted. Using this new code ended up reducing my overall LOC by about 65! Not to mention, the arguments parsing became way easier to read.
Thank you Nim, Nim community; I'm having fun.
For the curious, here are my templates. Perhaps not best practices are seen here, but it's working for what I need so far, and the code is much more maintainable. (In hindsight, docopt would have also worked and saved even more code at the trade-off of dependencies, and been a bit restrictive on the help text. Next time!)
# register_command(word) creates two variables: word_args:seq[string] and word_enabled:bool
# that are automatically set and filled for you
template register_command(command:untyped, nargs:Natural = 0, aliases:openArray = new_seq[string]()) {.dirty.} =
when not defined(`command`):
let `command` :string = ""
var
`command _ args` {.used.} = new_seq[string]()
`command _ enabled` = false
`command _ args _ len` {.used.} = nargs
`all _ command _ alias _ list`:seq[string] # [command, alias1, alias2, ...]
if aliases.len > 0:
`all _ command _ alias _ list` = concat(@[`command`.astToStr],to_seq(aliases))
else:
`all _ command _ alias _ list` = @[`command`.astToStr]
template capture_command(p:OptParser, command:untyped) {.dirty.} =
if p.key in `all _ command _ alias _ list`:
`command _ enabled` = true
for capture_command_i in 1 .. `command _ args _ len`:
p.next()
if p.kind == cmdEnd: break # assume we're using parseopt while loop convention
`command _ args`.add p.key
continue
## register all cli arguments
when defined(windows):
register_command(vs)
register_command(force,aliases=["f"])
register_command(init)
register_command(cxx,nargs=1,aliases=["c"])
register_command(debug,aliases=["g"])
register_command(generate,nargs=1,aliases=["gen"])
register_command(new,nargs=2)
register_command(copy,nargs=3,aliases=["cp"])
register_command(download,nargs=1,aliases=["dl"])
register_command(help,aliases=["h"])
# meanwhile, in main()...
proc main() =
# parse the command line options
var p = initOptParser(command_line_params())
while true:
p.next()
case p.kind:
of cmdEnd: break
of cmdShortOption, cmdLongOption:
p.capture_command force
p.capture_command cxx
p.capture_command help
write_error("Error: ",&"Unknown command or option '{p.key}'")
quit(1)
of cmdArgument:
p.capture_command new
p.capture_command copy
p.capture_command download
p.capture_command generate
p.capture_command init
when defined(windows):
p.capture_command vs
write_error("Error: ",&"Unknown command or option '{p.key}'")
quit(1)
if help_enabled:
echo help_text
quit(0)
# etc.
Confutils is another package that makes the creation of git-style CLI tools particularly easy: