The Backuity CLIST is a scala-only (2.11+) library for quickly building beautiful type-safe modular and reusable mutable CLIs.
- You said beautiful
- Why mutable?
- Let's start - Single Command CLI
- Attributes: Argument vs Arguments vs Options
- Parsing
- Exit code vs Exception
- Version, Help and Usage
- Multiple Commands
- Licence & Contribution
An image is worth a thousand words, here is a taste of what you'd get for free:
We think that CLIs do not require an immutable approach. Immutability often comes at the expense of simplicity. If you are looking for an immutable CLI library you should take a look at projects like https://github.com/scopt/scopt.
First let's configure our SBT build
libraryDependencies ++=Seq(
"org.backuity.clist"%%"clist-core"%"3.5.1",
"org.backuity.clist"%%"clist-macros"%"3.5.1"%"provided")Then define a command:
importorg.backuity.clist._// or if you do not like wildcard imports:// import org.backuity.clist.{Command, opt, args}classCatextendsCommand(description ="concatenate files and print on the standard output") {
// `opt`, `arg` and `args` are scala macros that will extract the name of the member// to use it as the option/arguments name.// Here for instance the member `showAll` will be turned into the option `--show-all`varshowAll= opt[Boolean](abbrev ="A", description ="equivalent to -vET")
// an abbreviated form can be added, so that this option can be triggered both by `--number-nonblank` or `-b`varnumberNonblank= opt[Boolean](abbrev ="b", description ="number nonempty output lines, overrides -n")
// default values can be providedvarmaxLines= opt[Int](default =123)
varfiles= args[Seq[File]](description ="files to concat")
}And use it to parse args:
defmain(args: Array[String]) {
Cli.parse(args).withCommand(newCat) { case cat =>// the new Cat instance will be mutated to receive the command-line arguments
println(cat.files)
}
}Alternatively for simple commands like this one you can simply extend CliMain thus reducing the boiler plate even further:
importjava.io.Fileimportorg.backuity.clist._objectCatDemoextendsCliMain[Unit](
name ="cat",
description ="concatenate files and print on the standard output") {
varshowAll= opt[Boolean](abbrev ="A", description ="equivalent to -vET")
varnumberNonblank= opt[Boolean](abbrev ="b", description ="number nonempty output lines, overrides -n")
varmaxLines= opt[Int](default =123)
varfiles= args[Seq[File]](description ="files to concat")
defrun:Unit= {
println("files = "+ files)
println("showAll = "+ showAll)
println("maxLines = "+ maxLines)
}
}A Command can have 3 kinds of attributes:
opt: an option is always optional and is provided either by--option-name=value, or with an abbreviated form such as-x. Declaration order does not matter.arg: an argument receives an un-named value as in the commandcat <file>. It might be optional. Argument declaration order matters.args: the equivalent of a var-args. At most one can be specified and it must be declared last.
An option (being optional) must have a default value (as we want to avoid null for obvious reasons).
That default value is automatically provided for Boolean and Option (respectively false and None).
Note that a boolean option can be true by default, in that case providing it will make it false:
varprettyPrint= opt[Boolean](default =true, name ="no-pretty-print")Then on the command line: cmd --no-pretty-print will make prettyPrint false.
Currently only boolean are supported.
An argument can turned optional by setting the required attribute to false:
vartarget= arg[Option[String]](required =false)Very much like options, optional arguments must provide a default value for types other than Boolean and Option.
An argument can be provided through the command line in the same fashion as options.
vartarget= arg[String]()
varverbose= opt[Boolean]()Then on the command line: cmd --verbose --target=stuff
Note that when doing so its order become irrelevant (the argument can be provided after options/arguments that were declared after him).
The parsing is done through the Read and ReadMultiple type-classes. User-specific instances can be provided by simply
adding them to the implicit scope.
Read (used by opt and arg) parses a String into a type T,
whereas ReadMultiple (used by args) takes a list of string to produce a type U.
The following types are supported out-of-the-box:
- String
- Int, Long, Double
- BigInt, BigDecimal
- java.util.Calendar (in the
yyyy-MM-ddformat) - java.io.File
- java.net.URI
- Tuple2
- java enums
Note that on the command line there is a distinction between
cat file1 file2 "file with space"
and
cat file1 file2 file with space
varmaxDelay= opt[Long](default =3000L)
varmaxError= opt[Double](default =3.24)Or if you need to customize that parsing:
objectPersonextendsCommand {
varname= arg[Name]()
}
caseclassName(firstName: String, lastName: String)
importorg.backuity.clist.util.ReadimplicitvalnameRead=Read.reads[Name] { str =>valArray(first,last) = str.split("\\.")
Name(first,last)
}By default, upon failure, the Cli will exit with code 1. This behavior can be customized:
Cli.parse(args).throwExceptionOnError(): throws an exception instead of exitingCli.parse(args).exitCode(12): exits with a specific code
You can provide a version number for your program through version("1.0.0"). This will add a version option,
whose name can be customized with version("1.0.0", "custom-name").
By default a help command is added, which displays the command usage. This can be removed with noHelp().
The usage is printed for each parsing error but this can be disabled with noUsageOnError().
Finally the usage can be customized through withUsage(newCustomUsage).
To build a multi-command CLI simply provide the parser with more than one command:
objectRunextendsCommandobjectShowextendsCommandvalres=Cli.parse(args).withCommands(Run, Show)
// res will be an Option[Command]It makes sense now to define a name for our program:
Cli.parse(args).withProgramName("my-cli").withCommands(Run, Show)It is entirely possible (and encouraged) to factorize options into traits and compose Commands with them:
traitCommon { this:Command=>varverbose= opt[Boolean](abbrev ="v")
}
objectRunextendsCommandwithCommonobjectShowextendsCommandwithCommonvalres=Cli.parse(args).withCommands(Run, Show)
// res is also now inferred as an `Option[Common]`You can also seal your command hierarchy to allow exhaustive pattern matching checks:
sealedtraitCommon { this:Command=>// same as above
}
Cli.parse(args).withCommands(Run, Show) match {
caseSome(Run) =>caseSome(Show) =>caseNone=>
}Depending on your taste, you might want to define the behavior of your commands within them:
sealedtraitCommon { this:Command=>varverbose= opt[Boolean](abbrev ="v")
defrun():Unit
}
objectRunextendsCommandwithCommon {
defrun():Unit= {
println("Running...")
}
}
objectShowextendsCommandwithCommon {
defrun():Unit= {
println("Showing...")
}
}
Cli.parse(args).withCommands(Run, Show).foreach(_.run())The code is published under the Apache 2.0 licence.
You're welcome to fork and submit PRs, and if you like the project you can up-vote the related StackOverflow answer.

