Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wizard mode #141

Merged
merged 1 commit into from
Nov 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions zio-cli/shared/src/main/scala/zio/cli/Args.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ sealed trait Args[+A] { self =>

final def between(min: Int, max: Int): Args[List[A]] = Args.Variadic(self, Some(min), Some(max))

def generateArgs: UIO[List[String]]

def helpDoc: HelpDoc

final def map[B](f: A => B): Args[B] = Args.Map(self, (a: A) => Right(f(a)))
Expand All @@ -63,6 +65,8 @@ sealed trait Args[+A] { self =>

def synopsis: UsageSynopsis

def uid: Option[String]

def validate(args: List[String], conf: CliConfig): IO[ValidationError, (List[String], A)]
}

Expand Down Expand Up @@ -100,6 +104,11 @@ object Args {
}).mapError(ValidationError(ValidationErrorType.InvalidArgument, _))

private def name: String = "<" + self.pseudoName.getOrElse(self.primType.typeName) + ">"

def generateArgs: UIO[List[String]] =
(Console.print(s"${self.uid.getOrElse("")} (${self.primType.typeName}): ") *> Console.readLine).orDie.map(List(_))

def uid: Option[String] = Some(self.name)
}

case object Empty extends Args[Unit] {
Expand All @@ -115,6 +124,10 @@ object Args {

def validate(args: List[String], conf: CliConfig): UIO[(List[String], Unit)] =
ZIO.succeed((args, ()))

def generateArgs: UIO[List[String]] = ZIO.succeed(List.empty)

def uid: Option[String] = None
}

final case class Both[+A, +B](head: Args[A], tail: Args[B]) extends Args[(A, B)] { self =>
Expand All @@ -135,6 +148,13 @@ object Args {
tuple <- self.tail.validate(args, conf)
(args, b) = tuple
} yield (args, (a, b))

def generateArgs: UIO[List[String]] = self.head.generateArgs.zipWith(self.tail.generateArgs)(_ ++ _)

def uid: Option[String] = self.head.uid.toList ++ self.tail.uid.toList match {
case Nil => None
case list => Some(list.mkString(", "))
}
}

final case class Variadic[+A](value: Args[A], min: Option[Int], max: Option[Int]) extends Args[List[A]] { self =>
Expand Down Expand Up @@ -178,6 +198,23 @@ object Args {

loop(args, Nil).map { case (args, list) => (args, list.reverse) }
}

def generateArgs: UIO[List[String]] = {
val repetitionsString =
(self.min, self.max) match {
case (Some(min), Some(max)) => s"$min - $max repetitions"
case (Some(1), _) => "1 repetition minimum"
case (Some(min), _) => s"$min repetitions minimum"
case (_, Some(1)) => "1 repetition maximum"
case (_, Some(max)) => s"$max repetitions maximum"
case _ => ""
}
(Console.print(s"${self.uid.getOrElse("")} ($repetitionsString): ") *> Console.readLine).orDie.map { input =>
input.split(" ").toList
}
}

def uid: Option[String] = self.value.uid
}

final case class Map[A, B](value: Args[A], f: A => Either[HelpDoc, B]) extends Args[B] { self =>
Expand All @@ -198,6 +235,10 @@ object Args {
case Right(value) => ZIO.succeed((r, value))
}
}

def generateArgs: UIO[List[String]] = self.value.generateArgs

def uid: Option[String] = self.value.uid
}

/**
Expand Down
22 changes: 15 additions & 7 deletions zio-cli/shared/src/main/scala/zio/cli/BuiltInOption.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,35 @@ object BuiltInOption {
final case class ShowHelp(synopsis: UsageSynopsis, helpDoc: HelpDoc) extends BuiltInOption
final case class ShowCompletionScript(pathToExecutable: JPath, shellType: ShellType) extends BuiltInOption
final case class ShowCompletions(index: Int, shellType: ShellType) extends BuiltInOption
final case class Wizard(command: Command[_]) extends BuiltInOption

final case class BuiltIn(
help: Boolean,
shellCompletionScriptPath: Option[JPath],
shellType: Option[ShellType],
shellCompletionIndex: Option[Int]
shellCompletionIndex: Option[Int],
wizard: Boolean
)

def builtInOptions(usageSynopsis: => UsageSynopsis, helpDoc: => HelpDoc): Options[Option[BuiltInOption]] = {
def builtInOptions(
command: => Command[_],
usageSynopsis: => UsageSynopsis,
helpDoc: => HelpDoc
): Options[Option[BuiltInOption]] = {
val options = (
Options.boolean("help").alias("h") ++
Options.file("shell-completion-script").optional ++
ShellType.option.optional ++
Options.integer("shell-completion-index").map(_.toInt).optional
Options.integer("shell-completion-index").map(_.toInt).optional ++
Options.boolean("wizard")
).as(BuiltIn.apply _)

options.map {
case BuiltIn(true, _, _, _) => Some(ShowHelp(usageSynopsis, helpDoc))
case BuiltIn(_, Some(path), Some(shellType), _) => Some(ShowCompletionScript(path, shellType))
case BuiltIn(_, _, Some(shellType), Some(index)) => Some(ShowCompletions(index, shellType))
case _ => None
case BuiltIn(true, _, _, _, _) => Some(ShowHelp(usageSynopsis, helpDoc))
case BuiltIn(_, _, _, _, true) => Some(Wizard(command))
case BuiltIn(_, Some(path), Some(shellType), _, _) => Some(ShowCompletionScript(path, shellType))
case BuiltIn(_, _, Some(shellType), Some(index), _) => Some(ShowCompletions(index, shellType))
case _ => None
}
}
}
29 changes: 23 additions & 6 deletions zio-cli/shared/src/main/scala/zio/cli/CliApp.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package zio.cli

import zio.Console.printLine
import zio.Console.{print, printLine, readLine}
import zio.System.envs
import zio._
import zio.cli.BuiltInOption._
Expand All @@ -16,7 +16,7 @@ import scala.annotation.tailrec
* requires environment `R`, and may fail with a value of type `E`.
*/
sealed trait CliApp[-R, +E, +Model] {
def run(args: List[String]): ZIO[R, Nothing, ExitCode]
def run(args: List[String]): ZIO[R, Any, Any]

def config(newConfig: CliConfig): CliApp[R, E, Model]

Expand Down Expand Up @@ -56,8 +56,8 @@ object CliApp {
def printDocs(helpDoc: HelpDoc): UIO[Unit] =
printLine(helpDoc.toPlaintext(80)).!

def run(args: List[String]): ZIO[R, Nothing, ExitCode] = {
def executeBuiltIn(builtInOption: BuiltInOption): Task[Unit] =
def run(args: List[String]): ZIO[R, Any, Any] = {
def executeBuiltIn(builtInOption: BuiltInOption): ZIO[R, Any, Any] =
builtInOption match {
case ShowHelp(synopsis, helpDoc) =>
val fancyName = p(code(self.figFont.render(self.name)))
Expand Down Expand Up @@ -86,6 +86,21 @@ object CliApp {
ZIO.foreachDiscard(completions)(word => printLine(word))
}
}
case Wizard(command) =>
val subcommands = command.getSubcommands

for {
subcommandName <- if (subcommands.size == 1) ZIO.succeed(subcommands.keys.head)
else
(print("Command" + subcommands.keys.mkString("(", "|", "): ")) *> readLine).orDie
subcommand <-
ZIO
.fromOption(subcommands.get(subcommandName))
.orElseFail(ValidationError(ValidationErrorType.InvalidValue, HelpDoc.p("Invalid subcommand")))
args <- subcommand.generateArgs
_ <- Console.printLine(s"Executing command: ${(prefix(self.command) ++ args).mkString(" ")}")
result <- self.run(args)
} yield result
}

// prepend a first argument in case the CliApp's command is expected to consume it
Expand All @@ -104,10 +119,12 @@ object CliApp {
e => printDocs(e.error),
{
case CommandDirective.UserDefined(_, value) => self.execute(value)
case CommandDirective.BuiltIn(x) => executeBuiltIn(x)
case CommandDirective.BuiltIn(x) =>
executeBuiltIn(x).catchSome { case e: ValidationError =>
printDocs(e.error)
}
}
)
.exitCode
}

def summary(s: HelpDoc.Span): CliApp[R, E, Model] =
Expand Down
46 changes: 43 additions & 3 deletions zio-cli/shared/src/main/scala/zio/cli/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package zio.cli

import zio.cli.HelpDoc.h1
import zio.cli.ValidationErrorType.CommandMismatch
import zio.{IO, ZIO}
import zio.{IO, UIO, ZIO}

/**
* A `Command` represents a command in a command-line application. Every command-line application
Expand Down Expand Up @@ -35,6 +35,10 @@ sealed trait Command[+A] { self =>
subcommands.copy(parent = subcommands.parent.withHelp(help)).asInstanceOf[Command[A]]
}

def generateArgs: UIO[List[String]]

def getSubcommands: Map[String, Command[_]]

def helpDoc: HelpDoc

final def map[B](f: A => B): Command[B] = Command.Map(self, f)
Expand Down Expand Up @@ -110,7 +114,7 @@ object Command {
val parseBuiltInArgs =
if (args.headOption.exists(conf.normalizeCase(_) == conf.normalizeCase(self.name)))
BuiltInOption
.builtInOptions(self.synopsis, self.helpDoc)
.builtInOptions(self, self.synopsis, self.helpDoc)
.validate(args, conf)
.mapBoth(_.error, _._2)
.some
Expand Down Expand Up @@ -150,6 +154,14 @@ object Command {

lazy val synopsis: UsageSynopsis =
UsageSynopsis.Named(List(self.name), None) + self.options.synopsis + self.args.synopsis

def generateArgs: UIO[List[String]] =
for {
options <- self.options.generateArgs
args <- self.args.generateArgs
} yield List(self.name) ++ options ++ args

def getSubcommands: Predef.Map[String, Command[_]] = Predef.Map(self.name -> self)
}

final case class Map[A, B](command: Command[A], f: A => B) extends Command[B] { self =>
Expand All @@ -164,6 +176,10 @@ object Command {
self.command.parse(args, conf).map(_.map(f))

lazy val synopsis: UsageSynopsis = self.command.synopsis

def generateArgs: UIO[List[String]] = self.command.generateArgs

def getSubcommands: Predef.Map[String, Command[_]] = self.command.getSubcommands
}

final case class OrElse[A](left: Command[A], right: Command[A]) extends Command[A] { self =>
Expand All @@ -178,6 +194,10 @@ object Command {
self.left.parse(args, conf).catchSome { case ValidationError(CommandMismatch, _) => self.right.parse(args, conf) }

lazy val synopsis: UsageSynopsis = UsageSynopsis.Mixed

def generateArgs: UIO[List[String]] = self.left.generateArgs.zipWith(self.right.generateArgs)(_ ++ _)

def getSubcommands: Predef.Map[String, Command[_]] = self.left.getSubcommands ++ self.right.getSubcommands
}

final case class Subcommands[A, B](parent: Command[A], child: Command[B]) extends Command[(A, B)] { self =>
Expand Down Expand Up @@ -244,12 +264,28 @@ object Command {
val helpDirectiveForParent =
ZIO.succeed(CommandDirective.builtIn(BuiltInOption.ShowHelp(self.synopsis, self.helpDoc)))

val wizardDirectiveForChild = {
val safeTail = args match {
case Nil => Nil
case _ :: tail => tail
}
self.child
.parse(safeTail, conf)
.collect(ValidationError(ValidationErrorType.InvalidArgument, HelpDoc.empty)) {
case directive @ CommandDirective.BuiltIn(BuiltInOption.Wizard(_)) => directive
}
}

val wizardDirectiveForParent =
ZIO.succeed(CommandDirective.builtIn(BuiltInOption.Wizard(self)))

self.parent
.parse(args, conf)
.flatMap {
case CommandDirective.BuiltIn(BuiltInOption.ShowHelp(_, _)) =>
helpDirectiveForChild orElse helpDirectiveForParent
case builtIn @ CommandDirective.BuiltIn(_) => ZIO.succeed(builtIn)
case CommandDirective.BuiltIn(_) =>
wizardDirectiveForChild orElse wizardDirectiveForParent
case CommandDirective.UserDefined(leftover, a) if leftover.nonEmpty =>
self.child.parse(leftover, conf).map(_.map((a, _)))
case _ =>
Expand All @@ -262,6 +298,10 @@ object Command {
}

lazy val synopsis: UsageSynopsis = self.parent.synopsis + self.child.synopsis

def generateArgs: UIO[List[String]] = self.parent.generateArgs.zipWith(self.child.generateArgs)(_ ++ _)

def getSubcommands: Predef.Map[String, Command[_]] = self.child.getSubcommands
}

/**
Expand Down
11 changes: 11 additions & 0 deletions zio-cli/shared/src/main/scala/zio/cli/HelpDoc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,17 @@ object HelpDoc {
case Span.URI(value) => value.toString.length
case Span.Sequence(left, right) => left.size + right.size
}

final def text: String =
self match {
case Span.Text(value) => value
case Span.Code(value) => value
case Span.Error(value) => value.text
case Span.Weak(value) => value.text
case Span.Strong(value) => value.text
case Span.URI(value) => value.toString
case Span.Sequence(left, right) => left.text + right.text
}
}
object Span {
final case class Text(value: String) extends Span
Expand Down
Loading