airframe-launcher

airframe-laucnher is a handy command-line program launcher.

Usage

Maven Central

build.sbt

libraryDependencies += "org.wvlet.airframe" %% "airframe-launcher" % "(version)"

Quick Start

  1. Import wvlet.airframe.launcher._
  2. Create a class A with constructor arguments annotated with @option or @argument
  3. Call Launcher.of[A].execute("command line")
import wvlet.airframe.launcher._

class MyApp(@option(prefix = "-h,--help", description = "display help messages", isHelp = true) 
            help: Boolean = false,
            @option(prefix = "-p", description = "port number") 
            port: Int = 8080) {

   @command(isDefault = true)
   def default: Unit = {
     println(s"Hello airframe. port:${port}")
   }
}

A function annotated with @defaultCommand will be executed.

Launcher.execute[MyApp]("-p 10010")
// Hello airframe. port:10010

Launcher.execute[MyApp]("")
// Hello airframe. port:8080

If there is a help option (isHelp = true), command line help messages can be generated automatically.

For example, enable the help option like this:

Launcher.execute[MyApp]("--help")

Then it will show the following help message:

usage: myapp [options]

[options]
 -p [PORT]   port number
 -h, --help  show help messages

Available Annotations

  • @option options
    • You can specify multiple option prefixes (e.g., -h,--help) for the same option
  • @argument
    • For mapping non-option arguments. If you want to handle multiple arguments, use Seq[String], Array[String] types.
  • @command
    • Defining function or class as a command module. You can specify description and (one-line) usage of the command in this annotation.
    • If no sub command name is given, a function annotated with @command(isDefault = true) will be executed as the default command.

Defining Multiple Sub Commands

You can define a command module (= a set of sub commands) using functions in a class.

Add @command annotation to functions in order to define sub commands:

import wvlet.airframe.launcher._
import wvlet.log.LogSupport

// Define a global option
case class GlobalOption(
  @option(prefix = "-h,--help", description = "display help messages", isHelp = true) 
  help: Boolean = false,
  @option(prefix = "-l,--loglevel", description = "log level") 
  loglevel: Option[LogLevel] = None
)

class MyApp(g:GlobalOption) extends LogSupport {
  Logger.setDefaultLogLevel(g.loglevel)

  @command(isDefault = true)
  def default {
    println("Type --help to display the list of commands")
  }

  @command(description = "say hello")
  def hello = {
    println("hello")
  }

  @command(description = "say world")
  def world(@argument message: String) {
    println(s"world ${message}")
  }
  
  @command(description = "start a server")
  def start(
    @option(prefix="-p,--port", description = "port number")
    port:Int = 8080,
    @option(prefix="--host",description = "server address")
    host:Option[String] = None
  ) {
    val addr = host.getOrElse("localhost")
    println(s"Starting server at ${addr}:${port}")
  }
}

You can call sub commands by specifying the name of the function. The sub command name will be matched in a canonical form (ignoring case and removing any symbols like ‘_’, ‘-‘)

Launcher.execute[MyApp]("hello")
// hello

Launcher.execute[MyApp]("start --host localhost -p 10000")
// Straing server at localhost:10000

Nesting Sub Commands

With addModule, you can add nested command set

// Add sub commands `command1` and `command2`:
val l = Launcher.of[A]
  .addModule[B](name="command1", description="nested command set")
  .addModule[C](name="command2", description="...")

// Launch B
l.execute("command1")

// Launch C
l.execute("command2")

It is also possible to nest Launcher(s):

val subModule = Launcher.of[A]

val l = Launcher.of[Main]
  .add(subModule, name="cmd-a", description="sub command set")

// Launch A in subModule
l.exucute("cmd-a")

Packaging with sbt-pack

If you use sbt-pack plugin, you can create a stand-alone Scala program with a command-line interface:

enablePlugins(PackPlugin)

// This example creates `myapp` command (target/pack/bin/hello) that calls org.mydomain.MyApp#main(Array[String]) 
packMain := Map("myapp" -> "org.mydomain.MyApp")

Run the program

$ sbt pack
...

$ ./target/pack/bin/myapp 
Type --help to display the list of commands
$ ./target/pack/bin/myapp --help
usage: [options] <command name>

[options]
 -h, --help                 display help messages
 -l, --loglevel=[LOGLEVEL]  log level 
[commands]
 hello      say hello
 world     	say world

$ ./target/pack/bin/myapp hello
hello