Skip to main content
Version: 2.x

Commands

A command is a piece of text representing a directive for a CLI application. This allows the user to communicate easily which task must perform the application. A popular CLI app is the Version Control System called Git. Commonly used commands of Git are among the following.

git clone // Creates a copy of a repository
git add // Adds modified or new files that will be committed after using git commit

ZIO CLI uses class Command[Model] as a description of a CLI command. When using CliApp.make, it is needed to specify a Command[Model] instance. This parameter is parsed by CliApp to produce a collection of possible commands and transform a valid input from a user into an instance of type Model. Then you can implement the functionality of the CLI App using pattern matching on instances of Model.

Subcommands​

A subcommand is a command that belongs to a larger command. Thus, it is possible for a command to have different subcommands. Subcommands can be used to better organize and design the functionality of a CLI app. Different subcommands can represent different types of task that a CLI can perform and give them a descriptive name. In the Git example above, git is the name of the command while clone or add are subcommands of git. As the functionality of git clone and git add are very different, it is better to use different subcommands. In the same way, it would be possible for a subcommand to have other subcommands.

Basic construction​

It's possible to create a Command using the apply method. You must always specify the name of the command that will be used in the CLI, but you can also specify options and/or arguments:

import zio.cli._

object Command {
def apply(name: String) = ???
def apply[ArgsType](name: String, args: Args[ArgsType]) = ???
def apply[OptionsType](name: String, options: Options[OptionsType]) = ???
def apply[OptionsType, ArgsType](name: String, options: Options[OptionsType], args: Args[ArgsType]) = ???
}

Combining commands and transformations​

ZIO CLI also allows to combine different commands in a functional manner. This can be used for a cleaner design of the commands of a CLI app.

trait Command[+A] {
def |[A1 >: A](that: Command[A1]): Command[A1]
def orElse[A1 >: A](that: Command[A1]): Command[A1]
def orElseEither[B](that: Command[B]): Command[Either[A, B]]
def withHelp(help: HelpDoc): Command[A]
def withHelp(help: String): Command[A]
def subcommands[B](that: Command[B])
def subcommands[B](c1: Command[B], c2: Command[B], cs: Command[B]*)
def map[B](f: A => B): Command[B]
}

Choosing between commands​

If we have more than one command, we can create a new Command that allows to choose between two commands using methods |, orElse and orElseEither as with Options. The difference between these three methods lies in the type parameter of the new Command. The user will be able to enter only the command that he wants, triggering the desired functionality.

  • Method orElse

| is just an alias for orElse. In the Git CLI, it is possible to choose between different commands. This can be realized using orElse.

import zio.cli._
import java.nio.file.Path

val gitAdd: Command[Unit] = Command("add")
val gitClone: Command[Path] = Command("clone", Options.directory("directory"))

val newCommand: Command[Any] = gitAdd orElse gitClone
  • Method orElseEither

This method wraps the types in an Either class.

import zio.cli._
import java.nio.file.Path

val gitAdd: Command[Unit] = Command("add")
val gitClone: Command[Path] = Command("clone", Options.directory("directory"))

val newCommand: Command[Either[Unit, Path]] = gitAdd orElseEither gitClone

Adding Help Documentation​

Method withHelp returns a new command with the parameter HelpDoc or String as the helpDoc of the command.

import zio.cli._
import java.nio.file.Path

val helpGit = "This is command git."
val helpGitAdd = "Stages changes in the working repository."
val helpGitClone = "Creates a copy of an existing repository."

// Adding HelpDoc to Commands
val gitCommand: Command[Unit] = Command("git").withHelp(helpGit)
val gitAdd: Command[Unit] = Command("add").withHelp(helpGitAdd)
val gitClone: Command[Path] = Command("clone", Options.directory("directory")).withHelp(helpGitClone)

The output that this HelpDoc produces is the following.

DESCRIPTION

This is command git.

COMMANDS

- add Stages changes in the working repository.
- clone --directory directory Creates a copy of an existing repository.

Adding subcommands​

Method subcommands returns a new command with a new set of child commands.

// Adds subcommands add and clone to command git
val git: Command[Any] = gitCommand.subcommands(gitAdd, gitClone)

Changing the type parameter​

Command[_] is a generic class. We can apply map to construct commands returning any custom type that helps us implement business logic. In this way, we can avoid using types as Unit, Path, Any or Either that are not very informative. For example, we can modify the type parameter of git to the trait Git.

sealed trait Git
case class Add() extends Git
case class Clone(directory: Path) extends Git

val customCommand: Command[Git] =
git.map {
case () => Add()
case directory: Path => Clone(directory)
}

This makes possible to implement the business logic of our CLI app using directly a custom type. This can be of great use if we have employed various (orElseEither). We are going to implement another subcommand of Git using orElseEither.

// new Git subcommand
val gitBranch = Command("branch", Args.text("branch-name"))

// new Git subclass
case class Branch(branchName: String) extends Git

val eitherCommand: Command[Either[Either[Unit, Path], String]] = gitAdd orElseEither gitClone orElseEither gitBranch
val customEitherCommand: Command[Git] =
eitherCommand.map {
case Left(Left(_)) => Add()
case Left(Right(directory)) => Clone(directory)
case Right(branchName) => Branch(branchName)
}

In this example, we have transformed a rather cumbersome Command[Either[Either[Unit, Path], String]] into a Command[Git].