This project is under improvement. It needs your help to push it to maturity.
Built against scala versions: 2.11.11, 2.12.3
- Argument arg - general phrase of all below.
- Command cmd - a predefined phrase that must be fully matched on the command-line to tell the app what to do.
- Parameter param - argument without a name on the command-line, which directly matches the value. This is equivalent to "argument" in many other libraries.
- Option opt - (optional) argument that has a name with hyphens precede:
-f
,--source-file
- Properties prop(s) - an argument with a flag as its name,
and values of key/value pairs:
-Dkey=value
- PriorArg prior - an argument with alias that has priority to be matched.
-help
,--help
If you ask google "scala command line arguments parser github", google gives you a whole page of answers. Some libraries said: "Life is too short to parse command-line arguments". I think it might be "Life is too short to swing from one command-line argument parser to another". I tried many of these parsers, some are complicated with hard-to-read README, some kind of lack features that fit into some cases, some do not generate usage info...
Scmd brings these features all-in-one:
Fetures | example |
---|---|
Boolean option folding | -xyz |
Option Value folding | -fValue |
Option Value evaluation | -f=Value |
Trailing option | cp -r SRC DST equivalent to cp SRC DST -r |
Mutual exclusion | --start ❘ --stop |
Mutual dependency | [-a -b] or [-a [-b]] |
Validation | cp SRC DST SRC must exist/etc. |
Typed argument | cp SRC DST SRC is File or Path |
Argument optionality | SRC [DST] |
Variable argument | SRC... DST |
Properties | -Dkey=value , -Dkey:value |
Sub commands and argument hierarchy | openstack nova list service |
Optional commands | command [<command1> <command2>] |
Routing | no manually writing if .. else or match case to route command |
Contextual help | preciser help info |
Typo correction | git comit triggers: did you mean: 'commit' |
Usage info generation | see pictures below |
- Simplicity - Clients would be able to use it with little effort. More complex functionality should be possible and addable.
- Versatility - It has to be powerful enough to tackle those tricky things.
- Beauty - It should provide fluent coding style; It should be able to generate formatted usage console info.
- Strictness - As a library, it should be well structured, documented and self-encapsulated.
First, define the arguments in def-class:
import java.io.File
import Scmd._
@ScmdDef
private class CpDef(args: Seq[String]) extends ScmdDefStub[CpDef]{ //app name 'cp' is inferred from CpDef
val SRC = paramDefVariable[File]().mandatory
val DEST = paramDef[File]().mandatory
val recursive = optDef[Boolean](abbr = "R")
}
This definition matches cmd-line argument combinations below:
$cp file1 file2 ... dest //backtracking: dest always matched.
$cp file1 dest -R
$cp -recursive file1 dest //option can be put at anywhere.
...
Then use them:
object CpApp extends App{
val conf = (new CpDef(args)).parse
import scmdValueConverter._
conf.SRC.value // Seq[File]
conf.DEST.value // File
conf.recursive.value // Boolean
}
- Project setup
- Define arguments
- Build up argument structure
- Validation
- Use parsed values
- Routing
- Customization
- Misc
Scmd depends on scalameta at compile time.
val macroAnnotationSettings = Seq(
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full),
scalacOptions += "-Xplugin-require:macroparadise",
scalacOptions in(Compile, console) ~= (_ filterNot (_ contains "paradise")),
libraryDependencies += "org.scalameta" %% "scalameta" % "1.8.0" % Provided
)
val yourProject = project
.settings(macroAnnotationSettings)
.settings(
libraryDependencies ++= Seq(
"com.github.cuzfrog" %% "scmd" % "0.1.2"
)
)
For simplicity, Scmd is not divided into multiple projects.
If one seriously demands smaller runtime jar size, please fire an issue or manually exclude
package com.github.cuzfrog.scmd.macros
.
- name is from val's name.
val nova = cmdDef(description = "nova command entry") //the command's name is nova
val Nova = cmdDef() //another cmd (cmd name is case-sensitive)
val remotePort = optDef[Int]() //matches `--remote-port` and `--remotePort`
//val RemotePort = optDef[Int]() //won't compile, name conflicts are checked at compile time.
The description
cannot be omitted.
- param/opt/props are typed:
val port = optDef[Int](abbr = "p", description = "manually specify tcp port to use") //type is Int
See Supported types. Put custom evidence into def-class to support more types.
- mandatory argument:
val srcFile = paramDef[Path](description = "file to send.").mandatory
- argument with default value:
val name = optDef[String]().withDefault("default name")
Mandatory argument cannot have default value.
Boolean args have default value of false
, which could be set to true
manually.
Boolean args cannot(should not) be mandatory.
- variable argument(repetition):
val SRC = paramDefVariable[File]().mandatory
val num = optDefVariable[Long](abbr = "N")
-N=1 --num 2 --num=3 -N4 -N 5
is legal, num
will have the value of Seq(1,2,3,4,5)
- properties(argument with flag):
val properties = propDef[Int](flag = "D",description = "this is a property arg.")
-Dkey1=1 -Dkey2=3
gives properties
the value Seq("key1"->1,"key2"->3)
Props are global argument, that means they can be matched from anywhere on the command-line arguments.
- prior argument:
val help = priorDef(alias = Seq("-help", "--help")) //fully matches `-help` and `--help`
Prior args are scoped to cmd:
git --help
prints the help info for git
, git tag --help
prints for tag
Prior args are picked immediately.
cp --help SRC DST
prints the help info, SRC
and DST
are ignored.
Argument definition is of the shape of a tree, params/opts are scoped to their cmds.
- Inferred from declaration.
The definition order matters in
@ScmdDef
annotated Class.
val sharedParam = paramDef[String](description = "this should be shared by cmds below.")
val topLevelOpt = optDef[Int]()
val nova = cmdDef(description = "nova command entry")
val param1 = paramDef[File]()
val opt1 = optDef[Int]()
val neutron = cmdDef(description = "neutron command entry")
val opt2 = optDef[Int]()
This will build the structure:
openstack
+-topLevelOpt
+-nova
+-sharedParam
+-param1
+-opt1
+-neutron
+-sharedParam
+-opt2
- Using tree building DSL:
import scmdTreeDefDSL._
argTreeDef( //app entry
verbose, //opt
nova(
list(service ?, project)//cmds under list are optional.
//equivalent to list(service ?, project ?)
),
neutron(
alive | dead, // --alive | --dead, mutual exclusion
list(service, project) ?
//cmd list is optional. but once list is entered, one of its sub-cmds is required.
),
cinder(
list(service, project), //every level of cmds is required.
)
)
This will build the structure:
openstack
+-verbose
+-nova
+-list
+-service
+-project
+-neutron
+-alive
+-dead
+-list
+-service
+-project
+-cinder
+-list
+-service
+-project
Notice, service
, project
and list
are reused in the DSL.
Tree's legality is checked at compile time by macros.
This refers to argument basic(low-level) validation. Mutual limitation is defined above, and its validation is implicit.
@ScmdValid
class CatValidation(argDef: CatDef) {
validation(argDef.files) { files =>
files.foreach { f =>
if (!f.toFile.exists()) throw new IllegalArgumentException(s"$f not exists.")
}
}
}
val conf = (new CatDef(args)).withValidation(new CatValidation(_)).parsed
Arguments will be checked by validation statements when they are evaluated(parsed from cmd-line args).
Scmd provides 2 styles of getting evaluated arguments:
- Implicit conversion:
import scmdValueImplicitConversion._
val src:Seq[File] = conf.SRC
val dst:File = conf.DST //mandatory arg will be converted into value directly.
val port:Option[Int] = conf.remotePort //optional arg will be converted into an Option
If an arg has default value, it will fall back to default value when not specified by user.
- Converter(Recommended):
import scmdValueConverter._
val src:Seq[File] = conf.SRC.value
val dst:File = conf.DST.value
val port:Option[Int] = conf.remotePort.value
Manual routing: if(conf.cmd.met){...} else {...}
Scmd provides a DSL similar to that of akka-http to route through commands:
def buildRoute(argDef: ArgDef): ArgRoute = {
import scmdRouteDSL._
import argDef._
import scmdValueConverter._
app.onConditions(
boolOpt.expectTrue,
properties.expectByKey("key1")(_.forall(_ > 6))
).run {
println(intOpt.value)
//do something
} ~
app.run {
//fall back behavior.
}
}
(new ArgDef(args)).runWithRoute(buildRoute)
~
links routes together that if the first route's conditions are not met,
then the second route is tried, until the statements inside run
are called,
the route continues to try through. Once a run
is done, the whole route ends.
If one does not want the whole route to end
after one of the run
finishes, use runThrough
instead.
- Customize argument exception handling.
implicit val customHandler: ScmdExceptionHandler[ScmdException] =
new ScmdExceptionHandler[ScmdException] {
override def handle(e: ScmdException): Nothing = e match {
case ex: ArgParseException => //...handle
case ex: ArgValidationException => //...handle
}
}
Put evidence above inside def-class or suitable scope.
- limitations:
- Reusing arg-def in tree-building-DSL: arg cannot duplicate through lineage. Duplication through lineage makes it possibly ambiguous for an argument's scope. This makes features, like trailing option, very hard to implement.
- built-in priors:
help
andversion
are built-in prior arguments. When they are matched against top cmd(app itself), usage info and version info will be printed respectively. The alias of them will be matched only, i.e.-help
,--help
define them in route against top cmd will override the default behavior.
import scmdRouteDSL._
app.runOnPrior(help){
//different behavior
}.run{...}
- ScmdDefStub[T]
ScmdDefStub[T]
contains some abstract public methods for IDE to recognize the api, which are stripped off during macro expansion. That meansextends ScmdDefStub[T]
could be omitted without errors.
This project is inspired by mow.cli. Ansi formatting is modified from backuity/clist.
See: Internal explanation.
Contribution is welcomed.
Cause Chung ([email protected])/([email protected])