Command

This page covers commands in detail. See Command basics for a general explanation.

Description

A command is a system-level building block of sbt, often used to capture user interactions. At the command level, there is little support for subprojects and parallel processing since those are implemented in the act command.

Plugin authors should try to solve their problem using settings, tasks, and input tasks first. Several notable exceptions are:

  • Extending the user experience of sbt itself
  • Providing sequential processing, for example for release command

Introduction

There are three main aspects to commands:

  1. The syntax used by the user to invoke the command, including:
    • Tab completion for the syntax
    • The parser to turn input into an appropriate data structure
  2. The action to perform using the parsed data structure. This action transforms the build State.
  3. Help provided to the user

In sbt, the syntax part, including tab completion, is specified with parser combinators. If you are familiar with the parser combinators in Scala's standard library, these are very similar. See the Tab-completion parser page for how to use the parser combinators.

State provides access to the build state, such as all registered commands, the remaining commands to execute, and all project-related information.

Finally, basic help information may be provided that is used by the help command to display command help.

Defining a Command

A command combines a function State => Parser[T] with an action (State, T) => State. The reason for State => Parser[T] and not simply Parser[T] is that often the current State is used to build the parser. For example, the currently loaded projects (provided by State) determine valid completions for the project command. Examples for the general and specific cases are shown in the following sections.

See Command.scala for the source API details for constructing commands.

General commands

General command construction looks like:

val action: (State, A) => State = ...
val parser: State => Parser[A] = ...
val command: Command = Command("name")(parser)(action)

No-argument commands

There is a convenience method for constructing commands that do not accept any arguments.

val action: State => State = ...
val command: Command = Command.command("name")(action)

Single-argument command

There is a convenience method for constructing commands that accept a single argument with arbitrary content.

// accepts the state and the single argument
val action: (State, String) => State = ...
val command: Command = Command.single("name")(action)

Multi-argument command

There is a convenience method for constructing commands that accept multiple arguments separated by spaces.

val action: (State, Seq[String]) => State = ...

// <arg> is the suggestion printed for tab completion on an argument
val command: Command = Command.args("name", "<arg>")(action)

Full Example

The following example is a sample build that adds commands to a project. To try it out:

  1. Create build.sbt and project/CommandExample.scala.
  2. Run sbt on the project.
  3. Try out the hello, helloAll, failIfTrue, color, and printState commands.
  4. Use tab-completion and the code below as guidance.

Here's build.sbt:

build.sbt

import CommandExample.*

scalaVersion := "3.8.2"
LocalRootProject / commands ++= Seq(hello, helloAll, failIfTrue, changeColor, printState)

Here's project/CommandExample.scala:

project/CommandExample.scala

import sbt.*
import Keys.*
import scala.Console

// imports standard command parsing functionality
import complete.DefaultParsers.*

object CommandExample:
  // A simple, no-argument command that prints "Hi",
  //  leaving the current state unchanged.
  def hello = Command.command("hello"): s0 =>
    Console.out.println("Hi!")
    s0

  // A simple, multiple-argument command that prints "Hi" followed by the arguments.
  //   Again, it leaves the current state unchanged.
  def helloAll = Command.args("helloAll", "<name>"): (s0, args) =>
    println("Hi " + args.mkString(" "))
    s0

  // A command that demonstrates failing or succeeding based on the input
  def failIfTrue = Command.single("failIfTrue"):
    case (s0, "true") => s0.fail
    case (s0, _)      => s0

  // Demonstration of a custom parser.
  // The command changes the foreground or background terminal color
  //  according to the input.
  lazy val change = Space ~> (reset | setColor)
  lazy val reset = token("reset" ^^^ Console.RESET)
  lazy val color = token( Space ~> ("blue" ^^^ "4" | "green" ^^^ "2") )
  lazy val select = token( "fg" ^^^ "3" | "bg" ^^^ "4" )
  lazy val setColor = (select ~ color).map: (g, c) =>
    s"\u001B[${g}${c}m"

  def changeColor = Command("color")(_ => change): (s0, ansicode) =>
    Console.out.print(ansicode)
    Console.out.println("Hi")
    s0

  // A command that demonstrates getting information out of State.
  def printState = Command.command("printState"): s0 =>
    import s0.*
    println(s"definedCommands.size registered commands")
    println(s"commands to run: ${show(remainingCommands)}")
    println()

    println(s"original arguments: ${show(configuration.arguments.toSeq)}")
    println(s"base directory: ${configuration.baseDirectory}")
    println()

    println(s"sbt version: ${configuration.provider.id.version}")
    println(s"Scala version (for sbt): ${configuration.provider.scalaProvider.version}")
    println()

    val extracted = Project.extract(s0)
    import extracted.*
    println(s"Current build: ${currentRef.build}")
    println(s"Current project: ${currentRef.project}")
    println(s"Original setting count: ${session.original.size}")
    println(s"Session setting count: ${session.append.size}")
    s0

  def show[A1](s: Seq[A1]) =
    s.map("'" + _ + "'").mkString("[", ", ", "]")

end CommandExample