loki: line oriented (k)ommand interpreter
Loki is a small library for writing line-oriented command interpreters (or cli programs) in Nim, that is inspired by Python's cmd lib.
import loki, strutils, options
from sequtils import zip
loki(myHandler, input):
do_greet name:
## Get a nice greeting!
if isSome(name):
echo("Hello ", name.get, "!")
else:
echo("Hello there!")
do_add num1, num2:
if isSome(num1) and isSome(num2):
echo("Result is ", parseInt(num1.get) + parseInt(num2.get))
else:
echo("Provide two numbers to add them")
do_EOF:
write(stdout, "Bye!\n")
return true
default:
write(stdout, "*** Unknown syntax: ", input.text , " ***\n")
let myCmd = newLoki(
handler=myHandler,
intro="Welcome to my CLI!\n",
)
myCmd.cmdLoop
Compile with something like:
nim c --threads:on cmd.nim
And an example run:
Loki uses the powerful macro system in Nim. Macros in Nim are functions that execute at compile-time and can transform a syntax tree into a different one.
The loki
macro block in the example above would expand into something like this:
proc do_greet(line: Line; name: Option[string] = none(string)): bool =
## Get a nice greeting!
if isSome(name):
echo(["Hello ", get(name), "!"])
else:
echo(["Hello!"])
proc do_add(line: Line; num1: Option[string] = none(string);
num2: Option[string] = none(string)): bool =
if isSome(num1) and isSome(num2):
echo(["Result is ", parseInt(get(num1)) + parseInt(get(num2))])
else:
echo(["Provide two numbers to add them"])
proc do_EOF(line: Line): bool =
write(stdout, "Bye!\n")
return true
proc default(line: Line): bool =
write(stdout, ["*** Unknown syntax: ", line.text, " ***\n"])
proc help(input: Line): bool =
var undocced: seq[string] = @["add"]
var docced: seq[string] = @["greet"]
var docs = @["d1", "d2"]
if isSome(input.args):
var cmdarg = pick(input.args, 0)
if isSome(cmdarg):
var cmd = get(cmdarg)
if contains(undocced, cmd):
write(stdout, "*** No help on for this")
else:
for pair in items(zip(docced, docs)):
let (docced_cmd, doc) = pair
if cmd == docced_cmd:
write(stdout, doc)
break
return
write(stdout, "\nDocumented commands (type help <topic>):\n")
write(stdout, "========================================\n")
write(stdout, join(docced, " \t "))
write(stdout, "\n\nUndocumented commands:\n")
write(stdout, "======================\n")
write(stdout, join(undocced, " \t "))
write(stdout, "\n")
proc cmdHandler(line: Line): bool =
case line.command
of "greet":
if isSome(line.args):
return do_greet(line, pick(line.args, 0))
else:
return do_greet(line, none(string))
of "add":
if isSome(line.args):
return do_add(line, pick(line.args, 0), pick(line.args, 1))
else:
return do_add(line, none(string), none(string))
of "EOF":
if isSome(line.args):
return do_EOF(line)
else:
return do_EOF(line)
of "help":
return help(line)
else:
return default(line)
Tip: The expanded code above (of the loki
macro) can be printed using
expandMacros
which can be helpful when debugging your code to see what's going on.
- Automatically generate TOC and help for handler commands
- Fix for handling extra args (thanks @hugosenari)
- Run tests on GH actions
- Minor docs
- Initial release