├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── ci.yml ├── project ├── build.properties ├── plugins.sbt └── UpdateReadme.scala ├── version.sbt ├── .gitignore ├── core └── shared │ └── src │ ├── main │ └── scala │ │ └── optparse_applicative │ │ ├── common │ │ ├── package.scala │ │ ├── OptWord.scala │ │ ├── MatchResult.scala │ │ └── Common.scala │ │ ├── extra │ │ ├── package.scala │ │ └── Extra.scala │ │ ├── helpdoc │ │ ├── package.scala │ │ ├── OptDescStyle.scala │ │ ├── ParserHelp.scala │ │ ├── Chunk.scala │ │ └── Help.scala │ │ ├── builder │ │ ├── package.scala │ │ ├── internal │ │ │ ├── HasName.scala │ │ │ ├── DefaultProp.scala │ │ │ ├── OptionFields.scala │ │ │ ├── Mod.scala │ │ │ └── package.scala │ │ └── Builder.scala │ │ ├── types │ │ ├── package.scala │ │ ├── CReader.scala │ │ ├── ParserPrefs.scala │ │ ├── ParseError.scala │ │ ├── ParserInfo.scala │ │ ├── ParserResult.scala │ │ ├── ParserM.scala │ │ ├── ReadM.scala │ │ ├── Opt.scala │ │ ├── OptReader.scala │ │ ├── Parser.scala │ │ └── Doc.scala │ │ ├── package.scala │ │ └── internal │ │ ├── Context.scala │ │ ├── package.scala │ │ ├── NondetT.scala │ │ └── MonadP.scala │ └── test │ └── scala │ └── optparse_applicative │ ├── test │ └── ChunkSpec.scala │ └── types │ └── DocSpec.scala ├── .scalafmt.conf ├── example └── src │ ├── main │ └── scala │ │ └── example │ │ ├── ExampleMain.scala │ │ ├── Commands.scala │ │ ├── SubparserExample.scala │ │ └── ValidationExample.scala │ └── test │ └── scala │ └── optparse_applicative │ └── test │ └── ParserSpec.scala ├── CHANGELOG.md ├── LICENSE └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @xuwei-k 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.9.5-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea*/ 2 | target/ 3 | *sublime-project 4 | *sublime-workspace 5 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/common/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative 2 | 3 | package object common extends Common 4 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/extra/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative 2 | 3 | package object extra extends Extra 4 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/helpdoc/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative 2 | 3 | package object helpdoc extends Help 4 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative 2 | 3 | package object builder extends Builder 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative 2 | 3 | package object types { 4 | type Args = List[String] 5 | } 6 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/common/OptWord.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.common 2 | 3 | import optparse_applicative.types.OptName 4 | 5 | final case class OptWord(name: OptName, value: Option[String]) 6 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/helpdoc/OptDescStyle.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.helpdoc 2 | 3 | import optparse_applicative.types.Doc 4 | 5 | /** Style for rendering an option. */ 6 | final case class OptDescStyle(sep: Doc, hidden: Boolean, surround: Boolean) 7 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/internal/HasName.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.builder.internal 2 | 3 | import optparse_applicative.types.OptName 4 | 5 | trait HasName[F[_]] { 6 | def name[A](n: OptName, fa: F[A]): F[A] 7 | } 8 | 9 | trait HasMetavar[F[_]] {} 10 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/package.scala: -------------------------------------------------------------------------------- 1 | package object optparse_applicative 2 | extends builder.Builder 3 | with common.Common 4 | with extra.Extra 5 | with helpdoc.Help 6 | with types.ParserFunctions { 7 | type Parser[A] = types.Parser[A] 8 | type ParserInfo[A] = types.ParserInfo[A] 9 | } 10 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/CReader.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaz.Functor 4 | import scalaz.syntax.functor._ 5 | 6 | // As we don't have the completion functionality, this is serving as a useless wrapper 7 | final case class CReader[A](reader: ReadM[A]) 8 | 9 | object CReader { 10 | implicit val cReaderFunctor: Functor[CReader] = 11 | new Functor[CReader] { 12 | def map[A, B](fa: CReader[A])(f: A => B): CReader[B] = 13 | CReader(fa.reader.map(f)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | runner.dialect = Scala213source3 2 | project.layout = StandardConvention 3 | maxColumn = 120 4 | align.preset = none 5 | rewrite.rules = [SortImports] 6 | continuationIndent.callSite = 2 7 | continuationIndent.defnSite = 2 8 | docstrings.style = keep 9 | includeCurlyBraceInSelectChains = false 10 | optIn.breakChainOnFirstMethodDot = false 11 | version = "3.10.3" 12 | rewrite.scala3.convertToNewSyntax = true 13 | runner.dialectOverride.allowSignificantIndentation = false 14 | runner.dialectOverride.allowAsForImportRename = false 15 | runner.dialectOverride.allowStarWildcardImport = false 16 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 2 | addSbtPlugin("com.github.scalaprops" % "sbt-scalaprops" % "0.5.1") 3 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") 4 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 5 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") 6 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.9") 7 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 8 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.0") 9 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") 10 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/common/MatchResult.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.common 2 | 3 | import scalaz.Monoid 4 | 5 | sealed trait MatchResult 6 | case object NoMatch extends MatchResult 7 | case class Match(s: Option[String]) extends MatchResult 8 | 9 | object MatchResult { 10 | implicit def matchResultMonoid: Monoid[MatchResult] = 11 | new Monoid[MatchResult] { 12 | def zero: MatchResult = NoMatch 13 | def append(f1: MatchResult, f2: => MatchResult): MatchResult = 14 | f1 match { 15 | case Match(_) => f1 16 | case NoMatch => f2 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/ParserPrefs.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | /** 4 | * @param multiSuffix metavar suffix for multiple options 5 | * @param disambiguate automatically disambiguate abbreviations 6 | * @param showHelpOnError show help text on parse errors 7 | * @param backtrack backtrack to parent parser when a subcommand fails 8 | * @param columns format the help page 9 | */ 10 | final case class ParserPrefs( 11 | multiSuffix: String, 12 | disambiguate: Boolean = false, 13 | showHelpOnError: Boolean = false, 14 | backtrack: Boolean = false, 15 | columns: Int = 80 16 | ) 17 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/internal/DefaultProp.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.builder.internal 2 | 3 | import scalaz.Monoid 4 | import scalaz.std.option._ 5 | import scalaz.syntax.plus._ 6 | 7 | case class DefaultProp[A](default: Option[A], sDef: Option[A => String]) 8 | 9 | object DefaultProp { 10 | implicit def defaultPropMonoid[A]: Monoid[DefaultProp[A]] = 11 | new Monoid[DefaultProp[A]] { 12 | def zero: DefaultProp[A] = 13 | DefaultProp(None, None) 14 | 15 | def append(f1: DefaultProp[A], f2: => DefaultProp[A]): DefaultProp[A] = 16 | DefaultProp(f1.default <+> f2.default, f1.sDef <+> f2.sDef) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/ParseError.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaz.Monoid 4 | 5 | sealed trait ParseError 6 | case class ErrorMsg(msg: String) extends ParseError 7 | case class InfoMsg(msg: String) extends ParseError 8 | case object ShowHelpText extends ParseError 9 | case object UnknownError extends ParseError 10 | 11 | object ParseError { 12 | implicit val parseErrorMonoid: Monoid[ParseError] = 13 | new Monoid[ParseError] { 14 | def zero: ParseError = UnknownError 15 | def append(f1: ParseError, f2: => ParseError): ParseError = 16 | f1 match { 17 | case UnknownError => f2 18 | case _ => f1 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/internal/Context.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.internal 2 | 3 | import scalaz.Monoid 4 | import optparse_applicative.types.ParserInfo 5 | 6 | sealed trait Context 7 | case class HasContext[A](names: List[String], p: ParserInfo[A]) extends Context 8 | case object NullContext extends Context 9 | 10 | object Context { 11 | def contextNames(c: Context): List[String] = 12 | c match { 13 | case HasContext(ns, _) => ns 14 | case NullContext => Nil 15 | } 16 | 17 | implicit val contextMonoid: Monoid[Context] = 18 | new Monoid[Context] { 19 | def zero: Context = NullContext 20 | 21 | def append(f1: Context, f2: => Context): Context = 22 | f2 match { 23 | case HasContext(ns, i) => HasContext(contextNames(f1) ++ ns, i) 24 | case NullContext => f1 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/internal/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative 2 | 3 | import optparse_applicative.types.{ParseError, ParserPrefs, ReadM} 4 | 5 | import scalaz.\/ 6 | 7 | package object internal { 8 | def runP[A](p: P[A], pprefs: ParserPrefs): (Context, ParseError \/ A) = 9 | p.run.run.run.run(pprefs) 10 | 11 | def uncons[A](xs: List[A]): Option[(A, List[A])] = 12 | xs match { 13 | case Nil => None 14 | case x :: xs => Some((x, xs)) 15 | } 16 | 17 | def min[A](a1: A, a2: A)(implicit A: Ordering[A]): A = 18 | A.min(a1, a2) 19 | 20 | def words(s: String): List[String] = 21 | s.split("\\s+").toList.filter(_.nonEmpty) 22 | 23 | def unwords(xs: List[String]): String = 24 | xs.mkString(" ") 25 | 26 | def runReadM[F[_], A](reader: ReadM[A], s: String)(implicit F: MonadP[F]): F[A] = 27 | P.hoistEither(reader.run.run(s)) 28 | } 29 | -------------------------------------------------------------------------------- /example/src/main/scala/example/ExampleMain.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import java.io.File 4 | 5 | import optparse_applicative._ 6 | 7 | import scalaz.NonEmptyList 8 | import scalaz.syntax.applicativePlus._ 9 | 10 | object ExampleMain { 11 | case class Opts(verbose: Boolean, name: String, inputs: NonEmptyList[File], output: Option[File]) 12 | 13 | val parseOpts: Parser[Opts] = 14 | ^^^( 15 | switch(short('v'), long("verbose")), 16 | strOption(short('n'), long("name")) <|> pure(""), 17 | some(strArgument(metavar("FILE"), help("Files to read")).map(new File(_))), 18 | optional(strOption(short('f'), long("file"), metavar("FILE")).map(new File(_))) 19 | )(Opts.apply) 20 | 21 | def main(args: Array[String]): Unit = { 22 | val opts = execParser(args, "ExampleMain", info(parseOpts <*> helper, progDesc("An example program."))) 23 | println(opts) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/src/main/scala/example/Commands.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import optparse_applicative._ 4 | 5 | import scalaz.syntax.apply._ 6 | 7 | sealed trait Sample 8 | case class Hello(targets: List[String]) extends Sample 9 | case object Goodbye extends Sample 10 | 11 | object Commands { 12 | val hello: Parser[Sample] = many(strArgument(metavar("TARGET..."))).map(Hello) 13 | 14 | val sample: Parser[Sample] = 15 | subparser( 16 | command("hello", info(hello, progDesc("Print greeting"))), 17 | command("goodbye", info(pure(Goodbye), progDesc("Say goodbye"))) 18 | ) 19 | 20 | def run: Sample => Unit = { 21 | case Hello(targets) => println(s"Hello, ${targets.mkString(", ")}!") 22 | case Goodbye => println("Goodbye.") 23 | } 24 | 25 | val opts: ParserInfo[Sample] = info(sample <*> helper) 26 | 27 | def main(args: Array[String]): Unit = { 28 | println(execParser(args, "SubparserExample", opts)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/internal/OptionFields.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.builder.internal 2 | 3 | import optparse_applicative.types.{OptName, ParseError, ParserInfo} 4 | 5 | final case class OptionFields[A](names: List[OptName], noArgError: ParseError) 6 | 7 | object OptionFields { 8 | implicit val optionFieldsHasName: HasName[OptionFields] = 9 | new HasName[OptionFields] { 10 | def name[A](n: OptName, fa: OptionFields[A]): OptionFields[A] = 11 | fa.copy(names = n :: fa.names) 12 | } 13 | } 14 | 15 | final case class FlagFields[A](names: List[OptName], active: A) 16 | 17 | object FlagFields { 18 | implicit val flagFieldsHasName: HasName[FlagFields] = 19 | new HasName[FlagFields] { 20 | def name[A](n: OptName, fa: FlagFields[A]): FlagFields[A] = 21 | fa.copy(names = n :: fa.names) 22 | } 23 | } 24 | 25 | final case class CommandFields[A](commands: List[(String, ParserInfo[A])]) 26 | 27 | final case class ArgumentFields[A]() 28 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/ParserInfo.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import optparse_applicative.helpdoc.Chunk 4 | 5 | import scalaz.Functor 6 | 7 | /** 8 | * A full description for a runnable Parser for a program. 9 | * 10 | * @param parser the option parser for the program 11 | * @param fullDesc whether the help text should contain full documentation 12 | * @param failureCode exit code for a parser failure 13 | * @tparam A 14 | */ 15 | final case class ParserInfo[A]( 16 | parser: Parser[A], 17 | fullDesc: Boolean, 18 | progDesc: Chunk[Doc], 19 | header: Chunk[Doc], 20 | footer: Chunk[Doc], 21 | failureCode: Int, 22 | intersperse: Boolean 23 | ) { 24 | def map[B](f: A => B): ParserInfo[B] = copy(parser = parser.map(f)) 25 | } 26 | 27 | object ParserInfo { 28 | implicit val parserInfoFunctor: Functor[ParserInfo] = 29 | new Functor[ParserInfo] { 30 | def map[A, B](fa: ParserInfo[A])(f: A => B): ParserInfo[B] = 31 | fa.map(f) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/internal/Mod.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.builder.internal 2 | 3 | import optparse_applicative.types.OptProperties 4 | 5 | import scalaz.Monoid 6 | import scalaz.syntax.semigroup._ 7 | 8 | /** 9 | * An option modifier. 10 | */ 11 | case class Mod[F[_], A](f: F[A] => F[A], prop: DefaultProp[A], g: OptProperties => OptProperties) { 12 | def <>(that: Mod[F, A]): Mod[F, A] = 13 | this |+| that 14 | } 15 | 16 | object Mod { 17 | def option[F[_], A](f: OptProperties => OptProperties): Mod[F, A] = 18 | Mod(identity, Monoid[DefaultProp[A]].zero, f) 19 | 20 | def field[F[_], A](f: F[A] => F[A]): Mod[F, A] = 21 | Mod(f, Monoid[DefaultProp[A]].zero, identity) 22 | 23 | implicit def modMonoid[F[_], A]: Monoid[Mod[F, A]] = 24 | new Monoid[Mod[F, A]] { 25 | def zero: Mod[F, A] = 26 | Mod(identity, Monoid[DefaultProp[A]].zero, identity) 27 | 28 | def append(f1: Mod[F, A], f2: => Mod[F, A]): Mod[F, A] = 29 | Mod(f2.f compose f1.f, f2.prop |+| f1.prop, f2.g compose f1.g) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/ParserResult.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import optparse_applicative.helpdoc.ParserHelp 4 | 5 | import scalaz.Functor 6 | 7 | sealed trait ParserResult[A] 8 | 9 | case class Success[A](a: A) extends ParserResult[A] 10 | 11 | case class Failure[A](failure: ParserFailure[ParserHelp]) extends ParserResult[A] 12 | 13 | sealed trait ExitCode { 14 | final def toInt: Int = 15 | this match { 16 | case ExitSuccess => 0 17 | case ExitFailure(i) => i 18 | } 19 | } 20 | case object ExitSuccess extends ExitCode 21 | case class ExitFailure(code: Int) extends ExitCode 22 | 23 | case class ParserFailure[H](run: String => (H, ExitCode, Int)) 24 | 25 | object ParserFailure { 26 | implicit val parserFailureFunctor: Functor[ParserFailure] = 27 | new Functor[ParserFailure] { 28 | def map[A, B](fa: ParserFailure[A])(f: A => B): ParserFailure[B] = 29 | ParserFailure { progName => 30 | val (h, exit, cols) = fa.run(progName) 31 | (f(h), exit, cols) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | schedule: 6 | - cron: '0 13 * * 6' 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 30 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - name: jvm 16 | - name: js 17 | - name: native 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: actions/setup-java@v5 21 | with: 22 | java-version: 8 23 | distribution: temurin 24 | - uses: coursier/cache-action@v7 25 | - uses: sbt/setup-sbt@v1 26 | - run: | 27 | case ${{ matrix.name }} in 28 | "jvm") 29 | sbt -v \ 30 | scalafmtSbtCheck \ 31 | "+scalafmtCheckAll" \ 32 | "+optparseApplicativeJVM/test" \ 33 | "+publishLocal" 34 | ;; 35 | "js") 36 | sbt -v \ 37 | scalafmtSbtCheck \ 38 | "+scalafmtCheckAll" \ 39 | "+optparseApplicativeJS/test" 40 | ;; 41 | "native") 42 | sbt -v \ 43 | scalafmtSbtCheck \ 44 | "+scalafmtCheckAll" \ 45 | "+optparseApplicativeNative/test" 46 | ;; 47 | *) 48 | echo "unknown job-name" 49 | exit 1 50 | esac 51 | - run: rm -rf ~/.ivy2/local 52 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/internal/package.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.builder 2 | 3 | import optparse_applicative.common.liftOpt 4 | import optparse_applicative.helpdoc.Chunk 5 | import optparse_applicative.types._ 6 | 7 | import scalaz.syntax.applicativePlus._ 8 | import scalaz.syntax.std.option._ 9 | import scalaz.std.option._ 10 | 11 | package object internal { 12 | def mkCommand[A](mod: Mod[CommandFields, A]): (List[String], String => Option[ParserInfo[A]]) = { 13 | val CommandFields(cmds) = mod.f(CommandFields(Nil)) 14 | (cmds.map(_._1), cmds.toMap.lift) 15 | } 16 | 17 | def mkParser[A](prop: DefaultProp[A], g: OptProperties => OptProperties, reader: OptReader[A]): Parser[A] = 18 | liftOpt(mkOption(prop, g, reader)) <+> prop.default.orEmpty[Parser] 19 | 20 | def mkOption[A](prop: DefaultProp[A], g: OptProperties => OptProperties, reader: OptReader[A]): Opt[A] = 21 | Opt(reader, mkProps(prop, g)) 22 | 23 | def mkProps[A](prop: DefaultProp[A], g: OptProperties => OptProperties): OptProperties = 24 | g(baseProps).copy(showDefault = prop.default <*> prop.sDef) 25 | 26 | val baseProps: OptProperties = 27 | OptProperties(metaVar = "", visibility = Visible, help = Chunk.empty, showDefault = None) 28 | 29 | /** Hide this option from the help text */ 30 | def internal[F[_], A]: Mod[F, A] = 31 | Mod.option(_.copy(visibility = Internal)) 32 | } 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 0.8.0 (2 April 2018) 2 | * fork from https://github.com/bmjames/scala-optparse-applicative. change maven gropuId, artifactId and package names 3 | * support scala-js and scala-native 4 | * support Scala 2.13.0-M3 5 | * update dependencies 6 | * drop Scala 2.10 support 7 | 8 | ### Version 0.7 (15 April 2017) 9 | * Linear bounded pretty printer implementation (Colt Frederickson) 10 | 11 | ### Version 0.6 (21 Dec 2016) 12 | * Remove dependency on Kiama (Colt Frederickson) 13 | 14 | ### Version 0.5 (5 Nov 2016) 15 | * Add support for Scala 2.12 (Rob Norris) 16 | 17 | ### Version 0.4 (27 July 2016) 18 | 19 | * Updated Scalaz version to 7.2.4 (Kris Nuttycombe, Sam Roberts) 20 | * Updated Scala 2.11 series version to 2.11.8 (Sam Roberts) 21 | * Add footer builders to API (Maun Suang Boey) 22 | 23 | ### Version 0.3 (13 Oct 2015) 24 | 25 | * `ReadM` and `Parser` instances for more Scala standard library types (Adelbert Chang). 26 | * Updated Scalaz version to 7.1.4, and Scala cross-compilation targets to 2.10.6 and 2.11.7 (Colt Frederickson). 27 | 28 | ### Version 0.2.1 (26 Nov 2014) 29 | 30 | * Fixed implementation of `many` on the `ApplicativePlus[Parser]` instance. 31 | 32 | ### Version 0.2 (24 Nov 2014) 33 | 34 | * Fixed bug reported by Sukant Hajra: `Parser#many` and `Parser#some` had their names 35 | mixed up. 36 | * Changed the signature of `Parser#some` so that it returns a `scalaz.NonEmptyList`. 37 | -------------------------------------------------------------------------------- /example/src/main/scala/example/SubparserExample.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import optparse_applicative._ 4 | 5 | import scalaz.syntax.apply._ 6 | 7 | case class Options(globalOpt: String, globalFlag: Boolean, command: Command) 8 | 9 | sealed trait Command 10 | case class Add(paths: List[String]) extends Command 11 | case class Commit(message: String) extends Command 12 | 13 | object SubparserExample { 14 | val parseOpts: Parser[Options] = 15 | ^^( 16 | strOption(long("globalOpt"), help("Option that applies to all commands")), 17 | switch(long("globalFlag"), help("Switch that applies to all commands")), 18 | subparser[Command]( 19 | command("add", info(many(strArgument(metavar("PATH"))).map(Add))), 20 | command("commit", info(strArgument(metavar("MESSAGE")).map(Commit))) 21 | ) 22 | )(Options) 23 | 24 | def main(args: Array[String]): Unit = { 25 | val opts = info(parseOpts <*> helper, progDesc("A program with some global opts and command subparsers")) 26 | println(execParser(args, "SubparserExample", opts)) 27 | } 28 | } 29 | 30 | /* Notes: 31 | * If you fail to provide the required --globalOpt option, but do provide the 32 | * required arguments to a subparser, the error message shows only the usage for the 33 | * subparser, and does not mention --globalOpt at all! This is confusing, however 34 | * optparse-applicative also appears to have the same behaviour (bug?). 35 | */ 36 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/ParserM.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaz.{Applicative, IList, Monad, NonEmptyList} 4 | import scalaz.std.option.{none, some} 5 | import scalaz.syntax.applicativePlus._ 6 | 7 | trait ParserM[R] { 8 | def run[X](f: R => Parser[X]): Parser[X] 9 | } 10 | 11 | object ParserM { 12 | def fromM[A](p: ParserM[A]): Parser[A] = 13 | p.run(_.pure[Parser]) 14 | 15 | def oneM[A](p: Parser[A]): ParserM[A] = 16 | new ParserM[A] { 17 | def run[X](f: A => Parser[X]): Parser[X] = BindP(p, f) 18 | } 19 | 20 | import scalaz.syntax.bind._ 21 | 22 | def manyM[A](p: Parser[A]): ParserM[List[A]] = 23 | oneM(p.map(some) <+> none[A].pure[Parser]).flatMap { 24 | case None => Applicative[ParserM].point(List.empty[A]) 25 | case Some(x) => manyM(p).map(x :: _) 26 | } 27 | 28 | def someM[A](p: Parser[A]): ParserM[NonEmptyList[A]] = 29 | ^(oneM(p), manyM(p).map(IList.fromList))(NonEmptyList.nel) 30 | 31 | implicit val parserMMonad: Monad[ParserM] = 32 | new Monad[ParserM] { 33 | def bind[A, B](fa: ParserM[A])(f: A => ParserM[B]): ParserM[B] = { 34 | val g = f 35 | new ParserM[B] { 36 | def run[X](f: B => Parser[X]): Parser[X] = 37 | fa.run(x => g(x).run(f)) 38 | } 39 | } 40 | 41 | def point[A](a: => A): ParserM[A] = 42 | new ParserM[A] { 43 | def run[X](f: A => Parser[X]): Parser[X] = f(a) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/ReadM.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaz.{-\/, \/, Kleisli, MonadPlus, Plus, ReaderT} 4 | import scalaz.syntax.either._ 5 | 6 | /** 7 | * A newtype over the Either monad used by option readers. 8 | */ 9 | final case class ReadM[A](run: ReaderT[String, \/[ParseError, *], A]) 10 | 11 | object ReadM { 12 | def mkReadM[A](f: String => ParseError \/ A): ReadM[A] = 13 | ReadM(Kleisli[\/[ParseError, *], String, A](f)) 14 | 15 | /** Return the value being read. */ 16 | def ask: ReadM[String] = 17 | ReadM(Kleisli.ask[\/[ParseError, *], String]) 18 | 19 | /** Abort option reader by exiting with a ParseError. */ 20 | def abort[A](e: ParseError): ReadM[A] = 21 | mkReadM(_ => e.left) 22 | 23 | /** Abort option reader by exiting with an error message. */ 24 | def error[A](e: String): ReadM[A] = 25 | abort(ErrorMsg(e)) 26 | 27 | implicit val readMMonadPlus: MonadPlus[ReadM] = 28 | new MonadPlus[ReadM] { 29 | def bind[A, B](fa: ReadM[A])(f: A => ReadM[B]): ReadM[B] = 30 | ReadM(fa.run.flatMap(a => f(a).run)) 31 | 32 | def point[A](a: => A): ReadM[A] = 33 | mkReadM(_ => a.right) 34 | 35 | def empty[A]: ReadM[A] = 36 | mkReadM(_ => -\/(UnknownError)) 37 | 38 | def plus[A](a: ReadM[A], b: => ReadM[A]): ReadM[A] = 39 | mkReadM(s => Plus[\/[ParseError, *]].plus(a.run.run(s), b.run.run(s))) 40 | // a.run.fold(_ => b, a => a.point[ReadM]) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/helpdoc/ParserHelp.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.helpdoc 2 | 3 | import optparse_applicative.types.Doc 4 | import Chunk._ 5 | import scalaz.{Monoid, Show} 6 | import scalaz.std.string._ 7 | import scalaz.syntax.semigroup._ 8 | import scalaz.syntax.show._ 9 | 10 | final case class ParserHelp( 11 | error: Chunk[Doc], 12 | header: Chunk[Doc], 13 | usage: Chunk[Doc], 14 | body: Chunk[Doc], 15 | footer: Chunk[Doc] 16 | ) 17 | 18 | object ParserHelp { 19 | val empty: ParserHelp = 20 | ParserHelp(Chunk.empty, Chunk.empty, Chunk.empty, Chunk.empty, Chunk.empty) 21 | 22 | implicit val parserHelpShow: Show[ParserHelp] = 23 | new Show[ParserHelp] { 24 | override def show(f: ParserHelp) = renderHelp(80, f).show 25 | } 26 | 27 | implicit val parserHelpMonoid: Monoid[ParserHelp] = 28 | new Monoid[ParserHelp] { 29 | override def zero: ParserHelp = 30 | ParserHelp.empty 31 | override def append(f1: ParserHelp, f2: => ParserHelp): ParserHelp = 32 | ParserHelp( 33 | f1.error |+| f2.error, 34 | f1.header |+| f2.header, 35 | f1.usage |+| f2.usage, 36 | f1.body |+| f2.body, 37 | f1.footer |+| f2.footer 38 | ) 39 | } 40 | 41 | def helpText(help: ParserHelp): Doc = 42 | extract(vsepChunks(List(help.error, help.header, help.usage, help.body, help.footer))) 43 | 44 | /** Convert a help text to a String */ 45 | def renderHelp(cols: Int, help: ParserHelp): String = helpText(help).pretty(cols) 46 | } 47 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/optparse_applicative/test/ChunkSpec.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.test 2 | 3 | import optparse_applicative.helpdoc.Chunk 4 | import optparse_applicative.internal._ 5 | import optparse_applicative.types.Doc 6 | import scalaz.std.list._ 7 | import scalaz.std.string._ 8 | import scalaz.syntax.applicative._ 9 | import scalaprops._ 10 | import scalaprops.Property.forAll 11 | import scalaz.Foldable 12 | 13 | object ChunkSpec extends Scalaprops { 14 | private[this] implicit val strGen: Gen[String] = Gen.alphaNumString 15 | 16 | implicit def chunkGen[A: Gen]: Gen[Chunk[A]] = 17 | Gen.from1(Chunk.apply[A]) 18 | 19 | def equalDocs(w: Int, d1: Doc, d2: Doc): Boolean = 20 | Doc.prettyRender(w, d1) == Doc.prettyRender(w, d2) 21 | 22 | val `fromList 1` = forAll { (xs: List[String]) => Chunk.fromList(xs).isEmpty == xs.isEmpty } 23 | 24 | val `fromList 2` = forAll { (xs: List[String]) => Chunk.fromList(xs) == Foldable[List].suml(xs.map(_.point[Chunk])) } 25 | 26 | val `extract 1` = forAll { (s: String) => Chunk.extract(s.point[Chunk]) == s } 27 | 28 | val `extract 2` = forAll { (x: Chunk[String]) => Chunk.extract(x.map(_.pure[Chunk])) == x } 29 | 30 | val `fromString 1` = Property.forAllG(Gen.positiveInt, Gen[String]) { (w, s) => 31 | equalDocs(w, Chunk.extract(Chunk.fromString(s)), Doc.string(s)) 32 | } 33 | 34 | val `fromString 2` = forAll { (s: String) => Chunk.fromString(s).isEmpty == s.isEmpty } 35 | 36 | val `paragraph` = forAll { (s: String) => Chunk.paragraph(s).isEmpty == words(s).isEmpty } 37 | } 38 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/Opt.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import optparse_applicative.helpdoc.Chunk 4 | 5 | import scalaz.Functor 6 | import scalaz.syntax.functor._ 7 | 8 | /** 9 | * A single option of a parser. 10 | */ 11 | final case class Opt[A](main: OptReader[A], props: OptProperties) 12 | 13 | /** 14 | * Specification for an individual parser option. 15 | */ 16 | final case class OptProperties( 17 | visibility: OptVisibility, 18 | help: Chunk[Doc], 19 | metaVar: String, 20 | showDefault: Option[String] 21 | ) 22 | 23 | object Opt { 24 | implicit val optFunctor: Functor[Opt] = 25 | new Functor[Opt] { 26 | def map[A, B](fa: Opt[A])(f: A => B): Opt[B] = 27 | fa.copy(main = fa.main.map(f)) 28 | } 29 | } 30 | 31 | sealed trait ArgPolicy 32 | case object SkipOpts extends ArgPolicy 33 | case object AllowOpts extends ArgPolicy 34 | 35 | final case class OptHelpInfo(multi: Boolean, default: Boolean) 36 | 37 | sealed trait OptTree[A] 38 | case class Leaf[A](a: A) extends OptTree[A] 39 | case class MultNode[A](as: List[OptTree[A]]) extends OptTree[A] 40 | case class AltNode[A](as: List[OptTree[A]]) extends OptTree[A] 41 | 42 | sealed trait OptVisibility extends Ordered[OptVisibility] { 43 | private val toInt: OptVisibility => Int = 44 | Map(Internal -> 1, Hidden -> 2, Visible -> 3) 45 | def compare(that: OptVisibility): Int = toInt(this) - toInt(that) 46 | } 47 | 48 | case object Internal extends OptVisibility 49 | case object Hidden extends OptVisibility 50 | case object Visible extends OptVisibility 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 - 2017 Paolo Capriotti, Ben James, 2018 Kenji Yoshida 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /project/UpdateReadme.scala: -------------------------------------------------------------------------------- 1 | import sbt._, Keys._ 2 | import sbtrelease.ReleasePlugin.autoImport.ReleaseStep 3 | import sbtrelease.Git 4 | import scala.sys.process.Process 5 | 6 | object UpdateReadme { 7 | val optparseApplicativeName = "optparse-applicative" 8 | val modules = optparseApplicativeName :: Nil 9 | 10 | val updateReadmeTask = { state: State => 11 | val extracted = Project.extract(state) 12 | val v = extracted get version 13 | val org = extracted get organization 14 | val snapshotOrRelease = if (extracted get isSnapshot) "snapshots" else "releases" 15 | val readme = "README.md" 16 | val readmeFile = file(readme) 17 | val newReadme = Predef 18 | .augmentString(IO.read(readmeFile)) 19 | .lines 20 | .map { line => 21 | val matchReleaseOrSnapshot = line.contains("SNAPSHOT") == v.contains("SNAPSHOT") 22 | if (line.startsWith("libraryDependencies") && matchReleaseOrSnapshot) { 23 | val i = modules.map("\"" + _ + "\"").indexWhere(line.contains) 24 | if (line.contains(" %%% ")) { 25 | s"""libraryDependencies += "$org" %%% "${modules(i)}" % "$v"""" 26 | } else { 27 | s"""libraryDependencies += "$org" %% "${modules(i)}" % "$v"""" 28 | } 29 | } else line 30 | } 31 | .mkString("", "\n", "\n") 32 | IO.write(readmeFile, newReadme) 33 | val git = new Git(extracted get baseDirectory) 34 | git.add(readme) ! state.log 35 | git.commit(message = "update " + readme, sign = false, signOff = false) ! state.log 36 | Process("git diff HEAD^") ! state.log 37 | state 38 | } 39 | 40 | val updateReadmeProcess: ReleaseStep = updateReadmeTask 41 | } 42 | -------------------------------------------------------------------------------- /example/src/test/scala/optparse_applicative/test/ParserSpec.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.test 2 | 3 | import optparse_applicative._ 4 | import example.Commands 5 | import optparse_applicative.types.{Failure, ParserInfo, ParserResult, Success} 6 | 7 | import scalaprops._ 8 | import scalaprops.Property.forAll 9 | import scalaz.syntax.apply._ 10 | 11 | import example._ 12 | 13 | class ParserSpec extends Scalaprops { 14 | def run[A](pinfo: ParserInfo[A], args: List[String]): ParserResult[A] = 15 | execParserPure(prefs(), pinfo, args) 16 | 17 | val `dash-dash args` = forAll { 18 | val result = run(Commands.opts, List("hello", "foo", "--", "--bar", "--", "baz")) 19 | result match { 20 | case Success(Hello(List("foo", "--bar", "--", "baz"))) => true 21 | case _ => false 22 | } 23 | } 24 | 25 | val flags: Parser[Int] = 26 | flag_(1, long("foo")) <|> flag_(2, long("bar")) <|> flag_(3, long("baz")) 27 | 28 | val `test disambiguate` = forAll { 29 | val result = execParserPure(prefs(disambiguate), info(flags), List("--f")) 30 | result == Success(1) 31 | } 32 | 33 | val ambiguous = forAll { 34 | val result = execParserPure(prefs(disambiguate), info(flags), List("--ba")) 35 | result match { 36 | case Success(_) => false 37 | case Failure(_) => true 38 | } 39 | } 40 | 41 | val backtracking = forAll { 42 | val p2 = switch(short('a')) 43 | val p1 = ^(subparser(command("c", info(p2))), switch(short('b')))((_, _)) 44 | val i = info(p1 <*> helper) 45 | val result = execParserPure(prefs(noBacktrack), i, List("c", "-b")) 46 | result match { 47 | case Success(_) => false 48 | case Failure(_) => true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/OptReader.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaz.Functor 4 | import scalaz.syntax.functor._ 5 | 6 | import CReader._ 7 | 8 | sealed trait OptReader[A] { 9 | final def names: List[OptName] = 10 | this match { 11 | case OptionReader(ns, _, _) => ns 12 | case FlagReader(ns, _) => ns 13 | case _ => Nil 14 | } 15 | } 16 | 17 | case class OptionReader[A](ns: List[OptName], cr: CReader[A], e: ParseError) extends OptReader[A] 18 | 19 | case class FlagReader[A](ns: List[OptName], a: A) extends OptReader[A] 20 | 21 | case class ArgReader[A](cr: CReader[A]) extends OptReader[A] 22 | 23 | case class CmdReader[A](ns: List[String], f: String => Option[ParserInfo[A]]) extends OptReader[A] 24 | 25 | object OptReader { 26 | implicit val optReaderFunctor: Functor[OptReader] = 27 | new Functor[OptReader] { 28 | def map[A, B](fa: OptReader[A])(f: A => B): OptReader[B] = 29 | fa match { 30 | case OptionReader(ns, cr, e) => OptionReader(ns, cr.map(f), e) 31 | case FlagReader(ns, a) => FlagReader(ns, f(a)) 32 | case ArgReader(cr) => ArgReader(cr.map(f)) 33 | case CmdReader(ns, g) => CmdReader(ns, g.andThen(_.map(_.map(f)))) 34 | } 35 | } 36 | } 37 | 38 | sealed trait OptName 39 | case class OptShort(name: Char) extends OptName 40 | case class OptLong(name: String) extends OptName 41 | 42 | object OptName { 43 | implicit val optNameOrdering: Ordering[OptName] = 44 | Ordering.fromLessThan { 45 | case (OptShort(n1), OptShort(n2)) => n1 < n2 46 | case (OptLong(n1), OptLong(n2)) => n1 < n2 47 | case (OptShort(_), _) => true 48 | case (OptLong(_), _) => false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/optparse_applicative/types/DocSpec.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaprops._ 4 | import scalaprops.Property.forAllG 5 | 6 | object DocSpec extends Scalaprops { 7 | private[this] implicit val stringDocGen: Gen[Doc] = Gen.alphaNumString.map(Doc.string(_)) 8 | val lineOrEmptyDocGen = Gen.elements(Doc.line, Doc.Empty) 9 | val appendDocGen = Gen.listOfN(10, Gen.frequency((3, stringDocGen), (1, lineOrEmptyDocGen))).map { 10 | _.reduce(Doc.append(_, _)) 11 | } 12 | def equalDocs(w: Int, d1: Doc, d2: Doc): Boolean = 13 | Doc.prettyRender(w, d1) == Doc.prettyRender(w, d2) 14 | 15 | val `text append is same as concat of strings` = forAllG(Gen.positiveInt, Gen.alphaNumString, Gen.alphaNumString) { 16 | (w, s1, s2) => equalDocs(w, Doc.string(s1 ++ s2), Doc.append(Doc.string(s1), Doc.string(s2))) 17 | } 18 | 19 | val `nesting law` = forAllG(Gen.positiveInt, Gen.positiveInt, Gen.positiveInt, Gen[Doc]) { (w, w2, w3, doc) => 20 | val List(nest1, nest2, width) = List(w, w2, w3).sorted 21 | equalDocs(w, Doc.nest(nest1 + nest2, doc), Doc.nest(nest1, Doc.nest(nest2, doc))) 22 | } 23 | 24 | val `zero nesting is id` = forAllG(Gen.positiveInt, Gen[Doc]) { (w, doc) => equalDocs(w, Doc.nest(0, doc), doc) } 25 | 26 | val `nesting distributes` = forAllG(Gen.positiveInt, Gen.positiveInt, Gen[Doc], Gen[Doc]) { (w, w2, doc, doc2) => 27 | val List(nesting, width) = List(w, w2).sorted 28 | equalDocs(width, Doc.nest(nesting, Doc.append(doc, doc2)), Doc.nest(nesting, doc).append(Doc.nest(nesting, doc2))) 29 | } 30 | 31 | val `nesting single line is noop` = forAllG(Gen.positiveInt, Gen.positiveInt, Gen.alphaNumString) { (w, w2, s) => 32 | val List(nesting, width) = List(w, w2).sorted 33 | val noNewlines = s.filter(_ != '\n') 34 | equalDocs(width, Doc.nest(nesting, Doc.string(noNewlines)), Doc.string(noNewlines)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/src/main/scala/example/ValidationExample.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import optparse_applicative._ 4 | import scalaz.{Applicative, Failure, Success, Validation, ValidationNel} 5 | import scalaz.syntax.apply._ 6 | 7 | /** 8 | * Demonstrates combining option parsing (which fails fast when it encounters errors) 9 | * with error-accumulating validation (using scalaz.Validation). 10 | */ 11 | object ValidationExample { 12 | case class UserData(username: String, email: String) 13 | 14 | type V[A] = ValidationNel[String, A] 15 | type ParserV[A] = Parser[V[A]] 16 | 17 | // Doesn't really validate emails, but gives us something to demonstrate 18 | def validEmail(email: String): V[String] = 19 | if (email.contains("@")) Validation.success(email) 20 | else Validation.failureNel("What kind of email address contains no '@' symbol?!") 21 | 22 | def validUsername(username: String): V[String] = 23 | if (username.length < 3) Validation.failureNel("That username is too short!") 24 | else if (username.length > 10) Validation.failureNel("That username is too looooong") 25 | else Validation.success(username) 26 | 27 | implicit val ParserVInstance: Applicative[ParserV] = 28 | Applicative[Parser] compose Applicative[V] 29 | 30 | val username: ParserV[String] = strOption(short('u'), long("username")).map(validUsername) 31 | val email: ParserV[String] = strOption(short('e'), long("email")).map(validEmail) 32 | 33 | val userData: ParserV[UserData] = ^(username, email)(UserData) 34 | 35 | def main(args: Array[String]): Unit = { 36 | val validatedUserData = execParser(args, "ValidationExample", info(userData)) 37 | validatedUserData match { 38 | case Success(UserData(u, e)) => 39 | println(s"Congratulations, $u <$e>, you are officially super-valid.") 40 | case Failure(errors) => 41 | errors.foreach(System.err.println) 42 | System.exit(1) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/internal/NondetT.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.internal 2 | 3 | import scalaz._ 4 | import scalaz.syntax.monad._ 5 | 6 | import ListT.listTMonadPlus 7 | 8 | final case class NondetT[F[_], A](run: ListT[BoolState[F]#λ, A]) { 9 | import NondetT._ 10 | 11 | def !(that: NondetT[F, A])(implicit F: Monad[F]): NondetT[F, A] = { 12 | val run2 = for { 13 | s <- mState[F].get.liftM[ListT].filter(!_) 14 | a2 <- that.run 15 | } yield a2 16 | NondetT(ltmp[F].plus(run, run2)) 17 | } 18 | 19 | def flatMap[B](f: A => NondetT[F, B])(implicit F: Monad[F]): NondetT[F, B] = 20 | NondetT(ltmp[F].bind(run)(f andThen (_.run))) 21 | 22 | def orElse(that: NondetT[F, A])(implicit F: Monad[F]): NondetT[F, A] = 23 | NondetT(ltmp[F].plus(run, that.run)) 24 | } 25 | 26 | private[internal] trait BoolState[F[_]] { 27 | type λ[A] = StateT[Boolean, F, A] 28 | } 29 | 30 | object NondetT { 31 | def empty[F[_]: Monad, A]: NondetT[F, A] = 32 | NondetT(ltmp[F].empty) 33 | 34 | def pure[F[_]: Monad, A](a: => A): NondetT[F, A] = 35 | NondetT(ltmp[F].point(a)) 36 | 37 | def cut[F[_]: Monad]: NondetT[F, Unit] = 38 | NondetT(mState[F].put(true).liftM[ListT]) 39 | 40 | def disamb[F[_]: Monad, A](allowAmb: Boolean, xs: NondetT[F, A]): F[Option[A]] = 41 | xs.run.take(if (allowAmb) 1 else 2).run.eval(false).map { 42 | _.headOption 43 | } 44 | 45 | protected def ltmp[F[_]: Monad] = listTMonadPlus[BoolState[F]#λ] 46 | protected def mState[F[_]: Monad] = MonadState[StateT[Boolean, F, *], Boolean] 47 | 48 | implicit def nondetTMonadPlus[F[_]: Monad]: MonadPlus[NondetT[F, *]] = 49 | new MonadPlus[NondetT[F, *]] { 50 | def bind[A, B](fa: NondetT[F, A])(f: A => NondetT[F, B]): NondetT[F, B] = fa.flatMap(f) 51 | 52 | def point[A](a: => A): NondetT[F, A] = NondetT.pure(a) 53 | 54 | def empty[A]: NondetT[F, A] = NondetT.empty 55 | 56 | def plus[A](a: NondetT[F, A], b: => NondetT[F, A]): NondetT[F, A] = a orElse b 57 | } 58 | 59 | implicit def nondetTTrans: MonadTrans[NondetT] = 60 | new MonadTrans[NondetT] { 61 | implicit def apply[G[_]: Monad]: Monad[NondetT[G, *]] = 62 | nondetTMonadPlus[G] 63 | 64 | def liftM[G[_]: Monad, A](a: G[A]): NondetT[G, A] = 65 | NondetT(StateT[Boolean, G, A](s => a.map(s -> _)).liftM[ListT]) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scala-optparse-applicative 2 | ========================== 3 | 4 | [![scaladoc](https://javadoc.io/badge2/com.github.xuwei-k/optparse-applicative_2.13/javadoc.svg)](https://javadoc.io/doc/com.github.xuwei-k/optparse-applicative_2.13/latest/optparse_applicative/index.html) 5 | 6 | A port of the [optparse-applicative][1] library to the Scala programming language. 7 | 8 | Most functionality has been ported, except completion. 9 | 10 | This library depends on [Scalaz][2] for functional data structures, type classes and combinators. 11 | 12 | How to get it 13 | ------------- 14 | 15 | for jvm 16 | 17 | ```scala 18 | libraryDependencies += "com.github.xuwei-k" %% "optparse-applicative" % "0.9.4" 19 | ``` 20 | 21 | 22 | for scala-js, scala-native 23 | 24 | ```scala 25 | libraryDependencies += "com.github.xuwei-k" %%% "optparse-applicative" % "0.9.4" 26 | ``` 27 | 28 | 29 | License 30 | ------- 31 | This library is distributed under a [BSD 3-Clause][3] license (see `LICENSE`). 32 | 33 | Simple example 34 | -------------- 35 | 36 | This example follows the one from the [optparse-applicative][1] docs. 37 | 38 | ```scala 39 | case class Sample(hello: String, quiet: Boolean) 40 | 41 | object SampleMain { 42 | 43 | val sample: Parser[Sample] = 44 | ^( 45 | strOption(long("hello"), metavar("TARGET"), help("Target for the greeting")), 46 | switch(long("quiet"), help("Whether to be quiet")) 47 | )(Sample.apply) 48 | 49 | def greet(s: Sample): Unit = s match { 50 | case Sample(h, false) => println("Hello, " ++ h) 51 | case _ => 52 | } 53 | 54 | def main(args: Array[String]): Unit = { 55 | val opts = info(sample <*> helper, 56 | progDesc("Print a greeting for TARGET"), 57 | header("hello - a test for scala-optparse-applicative")) 58 | greet(execParser(args, "SampleMain", opts)) 59 | } 60 | 61 | } 62 | ``` 63 | 64 | When run with the `--help` option, it prints: 65 | 66 | hello - a test for scala-optparse-applicative 67 | 68 | Usage: SampleMain --hello TARGET [--quiet] 69 | Print a greeting for TARGET 70 | 71 | Available options: 72 | -h,--help Show this help text 73 | --hello TARGET Target for the greeting 74 | --quiet Whether to be quiet 75 | 76 | 77 | Scalaz 7.2.x 78 | -------------- 79 | 80 | 81 | 82 | [1]: https://hackage.haskell.org/package/optparse-applicative 83 | [2]: https://github.com/scalaz/scalaz 84 | [3]: http://opensource.org/licenses/BSD-3-Clause 85 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/internal/MonadP.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.internal 2 | 3 | import scalaz._ 4 | import scalaz.WriterT.{writerT, writerTHoist} 5 | import scalaz.EitherT.eitherTHoist 6 | import scalaz.syntax.monadPlus._ 7 | import scalaz.syntax.std.option._ 8 | 9 | import optparse_applicative.types.{ParseError, Parser, ParserInfo, ParserPrefs} 10 | 11 | trait Completer 12 | 13 | trait MonadP[F[_]] extends MonadPlus[F] { 14 | def setContext[A](s: Option[String], p: ParserInfo[A]): F[Unit] 15 | def setParser[A](s: Option[String], p: Parser[A]): F[Unit] 16 | def getPrefs: F[ParserPrefs] 17 | 18 | def missingArg[A](e: ParseError): F[A] 19 | def attempt[A](fa: F[A]): F[ParseError \/ A] 20 | def error[A](e: ParseError): F[A] 21 | def exit[A, B](p: Parser[B], a: Option[A]): F[A] 22 | } 23 | 24 | import P._ 25 | 26 | final case class P[A](run: P_[A]) 27 | 28 | object P { 29 | type P_[A] = EitherT[ParseError, ContextWriter, A] 30 | type ParserPrefsReader[A] = Reader[ParserPrefs, A] 31 | type ContextWriter[A] = WriterT[Context, ParserPrefsReader, A] 32 | 33 | def tell[F[_]: Applicative, W](w: W): WriterT[W, F, Unit] = 34 | writerT(Applicative[F].point((w, ()))) 35 | 36 | def hoistEither[F[_], A](fa: ParseError \/ A)(implicit F: MonadP[F]): F[A] = 37 | fa.fold(F.error, a => F.point(a)) 38 | 39 | implicit val pMonadP: MonadP[P] = 40 | new MonadP[P] { 41 | def bind[A, B](fa: P[A])(f: A => P[B]): P[B] = 42 | P(fa.run.flatMap(f andThen (_.run))) 43 | 44 | def point[A](a: => A): P[A] = P(a.point[P_]) 45 | 46 | def empty[A]: P[A] = P(PlusEmpty[P_].empty) 47 | 48 | def plus[A](a: P[A], b: => P[A]): P[A] = P(a.run <+> b.run) 49 | 50 | def setContext[A](name: Option[String], p: ParserInfo[A]): P[Unit] = { 51 | val set: ContextWriter[Unit] = tell(HasContext(name.toList, p)) 52 | P(eitherTHoist[ParseError].liftM(set)) 53 | } 54 | 55 | def setParser[A](s: Option[String], p: Parser[A]): P[Unit] = 56 | point(()) 57 | 58 | def getPrefs: P[ParserPrefs] = { 59 | val ask: ContextWriter[ParserPrefs] = 60 | writerTHoist[Context].liftM(Kleisli.ask: ParserPrefsReader[ParserPrefs]) 61 | P(eitherTHoist[ParseError].liftM(ask)) 62 | } 63 | 64 | def missingArg[A](e: ParseError): P[A] = 65 | error(e) 66 | 67 | def attempt[A](fa: P[A]): P[ParseError \/ A] = 68 | P(eitherTHoist[ParseError].liftM(fa.run.run)) 69 | 70 | def error[A](e: ParseError): P[A] = 71 | P(EitherT.leftT(Applicative[ContextWriter].point(e))) 72 | 73 | def exit[A, B](p: Parser[B], a: Option[A]): P[A] = 74 | P(a.orEmpty[P_]) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/Parser.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import optparse_applicative.common.{mapParser, treeMapParser} 4 | import optparse_applicative.types.ParserM._ 5 | import scalaz.{~>, ApplicativePlus, Const, Functor, NonEmptyList} 6 | import scalaz.syntax.applicativePlus._ 7 | 8 | sealed trait Parser[A] { 9 | final def map[B](f: A => B): Parser[B] = 10 | this match { 11 | case NilP(fa) => NilP(fa map f) 12 | case OptP(fa) => OptP(Functor[Opt].map(fa)(f)) 13 | case m @ MultP() => MultP(m.p1 map (_ andThen f), m.p2) 14 | case AltP(p1, p2) => AltP(p1 map f, p2 map f) 15 | case BindP(p, k) => BindP(p, k andThen (_ map f)) 16 | } 17 | 18 | final def mapPoly[B](f: OptHelpInfo => (Opt ~> Const[B, *])): List[B] = 19 | mapParser[A, B](f, this) 20 | 21 | final def treeMap[B](g: OptHelpInfo => (Opt ~> Const[B, *])): OptTree[B] = 22 | treeMapParser[A, B](g, this) 23 | 24 | /** Alias for <+> */ 25 | final def <|>(that: Parser[A]): Parser[A] = this <+> that 26 | } 27 | 28 | case class NilP[A](fa: Option[A]) extends Parser[A] 29 | 30 | case class OptP[A](fa: Opt[A]) extends Parser[A] 31 | 32 | sealed abstract case class MultP[B] private () extends Parser[B] { 33 | type A 34 | def p1: Parser[A => B] 35 | def p2: Parser[A] 36 | } 37 | 38 | object MultP { 39 | def apply[A1, B1](a1: Parser[A1 => B1], a2: Parser[A1]): MultP[B1] { type A = A1 } = 40 | new MultP[B1] { 41 | type A = A1 42 | def p1 = a1 43 | def p2 = a2 44 | } 45 | } 46 | 47 | case class AltP[A](p1: Parser[A], p2: Parser[A]) extends Parser[A] 48 | 49 | case class BindP[A, B](p: Parser[A], f: A => Parser[B]) extends Parser[B] 50 | 51 | object Parser extends ParserInstances with ParserFunctions 52 | 53 | private[optparse_applicative] trait ParserInstances { 54 | implicit val parserApplicativePlus: ApplicativePlus[Parser] = 55 | new ApplicativePlus[Parser] { 56 | override def map[A, B](fa: Parser[A])(f: A => B): Parser[B] = 57 | fa.map(f) 58 | 59 | def ap[A, B](fa: => Parser[A])(f: => Parser[A => B]): Parser[B] = 60 | MultP(f, fa) 61 | 62 | def point[A](a: => A): Parser[A] = Parser.pure(a) 63 | 64 | def empty[A]: Parser[A] = NilP(None) 65 | 66 | def plus[A](a: Parser[A], b: => Parser[A]): Parser[A] = AltP(a, b) 67 | 68 | } 69 | } 70 | 71 | private[optparse_applicative] trait ParserFunctions { 72 | def pure[A](a: A): Parser[A] = 73 | NilP(Some(a)) 74 | 75 | def many[A](p: Parser[A]): Parser[List[A]] = 76 | fromM(manyM(p)) 77 | 78 | def some[A](p: Parser[A]): Parser[NonEmptyList[A]] = 79 | fromM(someM(p)) 80 | 81 | def optional[A](p: Parser[A]): Parser[Option[A]] = 82 | p.map[Option[A]](Some(_)) <+> pure(None) 83 | } 84 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/helpdoc/Chunk.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.helpdoc 2 | 3 | import optparse_applicative.internal.words 4 | import optparse_applicative.types.Doc 5 | 6 | import scalaz.{Applicative, MonadPlus, Monoid} 7 | import scalaz.std.list._ 8 | import scalaz.std.option._ 9 | 10 | import scalaz.syntax.std.option._ 11 | import scalaz.syntax.monadPlus._ 12 | import scalaz.syntax.foldable._ 13 | 14 | /** The free monoid on a semigroup A */ 15 | final case class Chunk[A](run: Option[A]) { 16 | def isEmpty: Boolean = run.isEmpty 17 | 18 | def <>(that: => Chunk[A])(implicit A: Monoid[A]): Chunk[A] = 19 | Chunk.chunked[A]((f1, f2) => A.append(f1, f2))(this, that) 20 | } 21 | 22 | object Chunk { 23 | def empty[A]: Chunk[A] = Chunk(None) 24 | 25 | implicit val chunkMonadPlus: MonadPlus[Chunk] = 26 | new MonadPlus[Chunk] { 27 | def point[A](a: => A): Chunk[A] = 28 | Chunk(Some(a)) 29 | 30 | def empty[A]: Chunk[A] = 31 | Chunk.empty 32 | 33 | def bind[A, B](fa: Chunk[A])(f: A => Chunk[B]): Chunk[B] = 34 | Chunk(fa.run.flatMap(f andThen (_.run))) 35 | 36 | def plus[A](a: Chunk[A], b: => Chunk[A]): Chunk[A] = 37 | Chunk(a.run <+> b.run) 38 | } 39 | 40 | implicit def chunkMonoid[A](implicit A: Monoid[A]): Monoid[Chunk[A]] = 41 | new Monoid[Chunk[A]] { 42 | def zero: Chunk[A] = Chunk.empty 43 | def append(f1: Chunk[A], f2: => Chunk[A]): Chunk[A] = f1 <> f2 44 | } 45 | 46 | /** Given a semigroup structure on A, return a monoid structure on Chunk[A] */ 47 | def chunked[A](f: (A, A) => A): (Chunk[A], Chunk[A]) => Chunk[A] = { 48 | case (Chunk(None), y) => y 49 | case (x, Chunk(None)) => x 50 | case (Chunk(Some(x)), Chunk(Some(y))) => Chunk(Some(f(x, y))) 51 | } 52 | 53 | /** Concatenate a list into a Chunk. */ 54 | def fromList[A: Monoid](as: List[A]): Chunk[A] = 55 | as match { 56 | case Nil => Monoid[Chunk[A]].zero 57 | case as => as.foldMap().point[Chunk] 58 | } 59 | 60 | implicit class DocChunkSyntax(self: Chunk[Doc]) { 61 | 62 | /** Concatenate two Chunks with a space in between. */ 63 | def <<+>>(that: Chunk[Doc]): Chunk[Doc] = 64 | chunked[Doc](_.withSpace(_))(self, that) 65 | 66 | /** Concatenate two Chunks with a softline in between */ 67 | def <>(that: Chunk[Doc]): Chunk[Doc] = 68 | chunked[Doc](_.withSoftline(_))(self, that) 69 | } 70 | 71 | /** Concatenate Chunks vertically. */ 72 | def vcatChunks(chunks: List[Chunk[Doc]]): Chunk[Doc] = 73 | chunks.foldRight(Chunk.empty[Doc])(chunked(_.withLine(_))) 74 | 75 | /** Concatenate Chunks vertically separated by empty lines. */ 76 | def vsepChunks(chunks: List[Chunk[Doc]]): Chunk[Doc] = 77 | chunks.foldRight(Chunk.empty[Doc])(chunked((x, y) => x.withLine(Doc.Empty).withLine(y))) 78 | 79 | def extract[A: Monoid](chunk: Chunk[A]): A = 80 | chunk.run.orZero 81 | 82 | def fromString(s: String): Chunk[Doc] = 83 | s match { 84 | case "" => Chunk.empty 85 | case s => Applicative[Chunk].pure(Doc.string(s)) 86 | } 87 | 88 | def paragraph(s: String): Chunk[Doc] = 89 | words(s).foldRight(Chunk.empty[Doc])((c, cs) => chunked[Doc](_.withSoftline(_))(fromString(c), cs)) 90 | 91 | def tabulate(table: List[(Doc, Doc)], size: Int = 24): Chunk[Doc] = 92 | table match { 93 | case Nil => Chunk.empty 94 | case xs => 95 | Applicative[Chunk].pure(xs.map { case (k, v) => 96 | Doc.indent(2, Doc.fillBreak(size, k).withSpace(v)) 97 | }.reduce(_.withLine(_))) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/extra/Extra.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.extra 2 | 3 | import optparse_applicative.builder._ 4 | import optparse_applicative.common._ 5 | import optparse_applicative.helpdoc._ 6 | import optparse_applicative.internal._ 7 | import optparse_applicative.types._ 8 | import optparse_applicative.builder.internal.OptionFields 9 | 10 | import scalaz.{-\/, \/-, ~>, Const} 11 | import scalaz.syntax.applicative._ 12 | import scalaz.syntax.semigroup._ 13 | import scalaz.Endo.endoInstance 14 | 15 | private[optparse_applicative] trait Extra { 16 | 17 | /** A hidden "helper" option which always fails */ 18 | def helper[A]: Parser[A => A] = 19 | abortOption(ShowHelpText, long[OptionFields, A => A]("help") <> short('h') <> help("Show this help text") <> hidden) 20 | 21 | def execParser[A](args: Array[String], progName: String, pinfo: ParserInfo[A]): A = 22 | customExecParser(args.toList, progName, prefs(idm[PrefsMod]), pinfo) 23 | 24 | def customExecParser[A](args: List[String], progName: String, pprefs: ParserPrefs, pinfo: ParserInfo[A]): A = 25 | handleParseResult(progName, execParserPure(pprefs, pinfo, args)) 26 | 27 | def handleParseResult[A](progName: String, result: ParserResult[A]): A = 28 | result match { 29 | case Success(a) => a 30 | case Failure(f) => 31 | val (msg, exit) = renderFailure(f, progName) 32 | exit match { 33 | case ExitSuccess => println(msg) 34 | case _ => Console.err.println(msg) 35 | } 36 | sys.exit(exit.toInt) 37 | } 38 | 39 | def execParserPure[A](pprefs: ParserPrefs, pinfo: ParserInfo[A], args: List[String]): ParserResult[A] = { 40 | val p = runParserInfo[P, A](pinfo, args) 41 | runP(p, pprefs) match { 42 | case (_, \/-(r)) => Success(r) 43 | case (ctx, -\/(err)) => Failure(parserFailure(pprefs, pinfo, err, ctx)) 44 | } 45 | } 46 | 47 | /** Generate a ParserFailure from a ParseError in a given Context. */ 48 | def parserFailure[A]( 49 | pprefs: ParserPrefs, 50 | pinfo: ParserInfo[A], 51 | msg: ParseError, 52 | ctx: Context 53 | ): ParserFailure[ParserHelp] = 54 | ParserFailure { progName => 55 | val exitCode = msg match { 56 | case ErrorMsg(_) | UnknownError => ExitFailure(pinfo.failureCode) 57 | case _ => ExitSuccess 58 | } 59 | 60 | def withContext[AA, B](ctx: Context, pinfo: ParserInfo[AA], f: List[String] => ParserInfo ~> (Const[B, *])): B = 61 | ctx match { 62 | case NullContext => f(Nil)(pinfo).getConst 63 | case HasContext(n, i) => f(n)(i).getConst 64 | } 65 | 66 | def usage_help[AA](progName: String, names: List[String], i: ParserInfo[AA]): ParserHelp = 67 | msg match { 68 | case InfoMsg(_) => ParserHelp.empty 69 | case _ => 70 | usageHelp( 71 | Chunk.vcatChunks( 72 | List( 73 | parserUsage(pprefs, i.parser, unwords(progName :: names)).pure[Chunk], 74 | i.progDesc.map(Doc.indent(2, _)) 75 | ) 76 | ) 77 | ) 78 | } 79 | 80 | val error_help: ParserHelp = 81 | errorHelp(msg match { 82 | case ShowHelpText => Chunk.empty 83 | case ErrorMsg(m) => Chunk.fromString(m) 84 | case InfoMsg(m) => Chunk.fromString(m) 85 | case UnknownError => Chunk.empty 86 | }) 87 | 88 | val showFullHelp = msg match { 89 | case ShowHelpText => true 90 | case _ => pprefs.showHelpOnError 91 | } 92 | 93 | def baseHelp[AA](i: ParserInfo[AA]): ParserHelp = 94 | if (showFullHelp) headerHelp(i.header) |+| footerHelp(i.footer) |+| parserHelp(pprefs, i.parser) 95 | else ParserHelp.empty 96 | 97 | val h = withContext[A, ParserHelp]( 98 | ctx, 99 | pinfo, 100 | names => 101 | new (ParserInfo ~> (Const[ParserHelp, *])) { 102 | def apply[AA](fa: ParserInfo[AA]): Const[ParserHelp, AA] = 103 | Const { 104 | baseHelp(fa) |+| usage_help(progName, names, fa) |+| error_help 105 | } 106 | } 107 | ) 108 | 109 | (h, exitCode, pprefs.columns) 110 | } 111 | 112 | def renderFailure(failure: ParserFailure[ParserHelp], progName: String): (String, ExitCode) = { 113 | val (h, exit, cols) = failure.run(progName) 114 | (ParserHelp.renderHelp(cols, h), exit) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/helpdoc/Help.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.helpdoc 2 | 3 | import optparse_applicative.types._ 4 | import optparse_applicative.common.showOption 5 | 6 | import scalaz._ 7 | import scalaz.std.option._ 8 | import scalaz.syntax.std.list._ 9 | import scalaz.syntax.std.boolean._ 10 | import scalaz.syntax.functor._ 11 | import scalaz.syntax.monoid._ 12 | 13 | private[optparse_applicative] trait Help { 14 | import Chunk._ 15 | 16 | /** Generate description for a single option. */ 17 | def optDesc[A](pprefs: ParserPrefs, style: OptDescStyle, info: OptHelpInfo, opt: Opt[A]): Chunk[Doc] = { 18 | val ns = opt.main.names 19 | val mv = Chunk.fromString(opt.props.metaVar) 20 | val descs = ns.sorted.map(Doc.string _ compose showOption) 21 | val desc = Chunk.fromList(descs.intersperse(style.sep)) <<+>> mv 22 | val vis = opt.props.visibility 23 | val showOpt = if (vis == Hidden) style.hidden else vis == Visible 24 | val suffix: Chunk[Doc] = if (info.multi) Chunk.fromString(pprefs.multiSuffix) else Chunk.empty 25 | 26 | def render(chunk: Chunk[Doc]): Chunk[Doc] = 27 | if (!showOpt) Chunk.empty 28 | else if (chunk.isEmpty || !style.surround) chunk <> suffix 29 | else if (info.default) chunk.map(_.brackets) <> suffix 30 | else if (descs.drop(1).isEmpty) chunk <> suffix 31 | else chunk.map(_.parens) <> suffix 32 | 33 | render(desc) 34 | } 35 | 36 | /** Generate descriptions for commands. */ 37 | def cmdDesc[A](p: Parser[A]): Chunk[Doc] = 38 | Chunk.vcatChunks( 39 | p.mapPoly(_ => 40 | new (Opt ~> Const[Chunk[Doc], *]) { 41 | def apply[AA](fa: Opt[AA]): Const[Chunk[Doc], AA] = 42 | Const(fa.main match { 43 | case CmdReader(cmds, p) => 44 | Chunk.tabulate( 45 | for (cmd <- cmds.reverse; d <- p(cmd).map(_.progDesc).toList) 46 | yield (Doc.string(cmd), extract(d).align) 47 | ) 48 | case _ => Chunk.empty 49 | }) 50 | } 51 | ) 52 | ) 53 | 54 | /** Generate a brief help text for a parser. */ 55 | def briefDesc[A](pprefs: ParserPrefs, parser: Parser[A]): Chunk[Doc] = { 56 | val style = OptDescStyle(sep = Doc.string("|"), hidden = false, surround = true) 57 | 58 | def altNode(chunks: List[Chunk[Doc]]): Chunk[Doc] = 59 | chunks match { 60 | case List(n) => n 61 | case ns => 62 | ns.foldRight(Chunk.empty[Doc])(chunked(_.withSoftline(Doc.string("|")).withSoftline(_))).map(_.parens) 63 | } 64 | 65 | def foldTree(tree: OptTree[Chunk[Doc]]): Chunk[Doc] = 66 | tree match { 67 | case Leaf(x) => x 68 | case MultNode(xs) => xs.foldRight(Chunk.empty[Doc])((x, y) => foldTree(x) <> y) 69 | case AltNode(xs) => altNode(xs.map(foldTree).filterNot(_.isEmpty)) 70 | } 71 | 72 | foldTree( 73 | parser.treeMap(info => 74 | new (Opt ~> Const[Chunk[Doc], *]) { 75 | def apply[AA](fa: Opt[AA]): Const[Chunk[Doc], AA] = Const(optDesc(pprefs, style, info, fa)) 76 | } 77 | ) 78 | ) 79 | } 80 | 81 | /** Generate a full help text for a parser. */ 82 | def fullDesc[A](pprefs: ParserPrefs, parser: Parser[A]): Chunk[Doc] = { 83 | val style = OptDescStyle(sep = Doc.string(","), hidden = true, surround = false) 84 | 85 | tabulate( 86 | parser 87 | .mapPoly(info => 88 | new (Opt ~> Const[Option[(Doc, Doc)], *]) { 89 | def apply[AA](fa: Opt[AA]): Const[Option[(Doc, Doc)], AA] = 90 | Const { 91 | val n = optDesc(pprefs, style, info, fa) 92 | val h = fa.props.help 93 | val hdef = Chunk(fa.props.showDefault.map(s => (Doc.string("default:") |+| Doc.string(s)).parens)) 94 | (n.isEmpty || n.isEmpty).prevent[Option]((extract(n), extract(h <<+>> hdef).align)) 95 | } 96 | } 97 | ) 98 | .flatten 99 | ) 100 | } 101 | 102 | def errorHelp(chunk: Chunk[Doc]): ParserHelp = 103 | ParserHelp(chunk, empty, empty, empty, empty) 104 | 105 | def headerHelp(chunk: Chunk[Doc]): ParserHelp = 106 | ParserHelp(empty, chunk, empty, empty, empty) 107 | 108 | def usageHelp(chunk: Chunk[Doc]): ParserHelp = 109 | ParserHelp(empty, empty, chunk, empty, empty) 110 | 111 | def bodyHelp(chunk: Chunk[Doc]): ParserHelp = 112 | ParserHelp(empty, empty, empty, chunk, empty) 113 | 114 | def footerHelp(chunk: Chunk[Doc]): ParserHelp = 115 | ParserHelp(empty, empty, empty, empty, chunk) 116 | 117 | /** Generate the help text for a program. */ 118 | def parserHelp[A](pprefs: ParserPrefs, parser: Parser[A]): ParserHelp = { 119 | def withTitle(title: String, chunk: Chunk[Doc]): Chunk[Doc] = 120 | chunk.map(Doc.string(title).withLine(_)) 121 | 122 | bodyHelp( 123 | vsepChunks( 124 | List( 125 | withTitle("Available options:", fullDesc(pprefs, parser)), 126 | withTitle("Available commands:", cmdDesc(parser)) 127 | ) 128 | ) 129 | ) 130 | } 131 | 132 | /** Generate option summary. */ 133 | def parserUsage[A](pprefs: ParserPrefs, parser: Parser[A], progName: String): Doc = 134 | Doc.hsep(List(Doc.string("Usage:"), Doc.string(progName), extract(briefDesc(pprefs, parser)).align)) 135 | } 136 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/types/Doc.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.types 2 | 3 | import scalaz._, Scalaz._ 4 | import scala.collection.immutable.Queue 5 | import Trampoline.{delay, done, suspend} 6 | 7 | //Doc implementation as laid out in https://www.cs.kent.ac.uk/pubs/2009/2847/content.pdf section 3.3 8 | //The key is that it's *linear* bounded. Because of scala, we have to use an insane amount of Trampolines 9 | //to pull it off. That idea taken from here: http://code.ouroborus.net/fp-syd/past/2013/2013-07-Sloane-InstallingTrampolines.pdf 10 | //Additional combinators adapted from PPrint haskell lib. 11 | //This is *insanely* ugly encoded in Scala, even the paper was hard to read. I'm very sorry. 12 | object Doc { 13 | // Type aliases to try and give meaning to all the Ints and Functions 14 | type Indent = Int 15 | type Width = Int 16 | type Layout = String 17 | type Position = Int 18 | type Remaining = Int 19 | type Horizontal = Boolean 20 | type Out = Remaining => Free.Trampoline[Layout] 21 | type OutGroup = Horizontal => Out => Free.Trampoline[Out] 22 | type Dq = Queue[(Position, OutGroup)] 23 | type TreeCont = (Position, Dq) => Free.Trampoline[Out] 24 | type IW = (Doc.Indent, Width) 25 | type Cont = IW => TreeCont => Free.Trampoline[TreeCont] 26 | 27 | final val Empty: Doc = new Doc(_ => (c: TreeCont) => done(c)) 28 | implicit val docMonoid: Monoid[Doc] = new Monoid[Doc] { 29 | def zero: Doc = Empty 30 | def append(f1: Doc, f2: => Doc): Doc = Doc.append(f1, f2) 31 | } 32 | 33 | // helpers for pruning, scanning, writing, etc 34 | def output(o: Out, r: Remaining, s: String): Free.Trampoline[String] = suspend(o(r).map(s ++ _)) 35 | 36 | def scan(lengthOfText: Width, outGroup: OutGroup)(cont: TreeCont): Free.Trampoline[TreeCont] = 37 | delay((p: Position, dq: Dq) => 38 | dq.lastOption match { 39 | case Some((pos, group)) => 40 | val obligation = ( 41 | pos, 42 | (h: Horizontal) => 43 | (out1: Out) => 44 | suspend(for { 45 | out2 <- outGroup(h)(out1) 46 | out3 <- group(h)(out2) 47 | } yield out3) 48 | ) 49 | // Add the obligation to the end and see if we can prune 50 | prune(cont)(p + lengthOfText, dq.init :+ obligation) 51 | case None => 52 | // No choice but to print and move forward 53 | suspend(for { 54 | out1 <- cont(p + lengthOfText, Queue.empty) 55 | out2 <- outGroup(false)(out1) 56 | } yield out2) 57 | } 58 | ) 59 | 60 | def prune(cont1: TreeCont): TreeCont = 61 | (p: Position, dq: Dq) => 62 | done((r: Remaining) => 63 | dq.headOption match { 64 | case Some((s, grp)) => 65 | if (p > s + r) { 66 | suspend(for { 67 | cont2 <- prune(cont1)(p, dq.tail) 68 | out <- grp(false)(cont2) 69 | layout <- out(r) 70 | } yield layout) 71 | } else { 72 | suspend(for { 73 | out <- cont1(p, dq) 74 | layout <- out(r) 75 | } yield layout) 76 | } 77 | 78 | case None => 79 | suspend(for { 80 | out <- cont1(p, Queue.empty) 81 | layout <- out(r) 82 | } yield layout) 83 | } 84 | ) 85 | 86 | def leave(cont: TreeCont): TreeCont = 87 | (p: Position, dq: Dq) => 88 | if (dq.isEmpty) { 89 | cont(p, Queue.empty) 90 | } else if (dq.lengthCompare(1) == 0) { 91 | val (s1, group1) = dq.last 92 | suspend(for { 93 | out1 <- cont(p, Queue.empty) 94 | out2 <- group1(true)(out1) 95 | } yield out2) 96 | } else { 97 | val (s1, group1) = dq.last 98 | val (s2, group2) = dq.init.last 99 | val obligation = ( 100 | s2, 101 | (h: Horizontal) => 102 | (out1: Out) => { 103 | val out3 = 104 | (r: Remaining) => 105 | suspend(for { 106 | out2 <- group1(p <= s1 + r)(out1) 107 | layout <- out2(r) 108 | } yield layout) 109 | suspend(group2(h)(out3)) 110 | } 111 | ) 112 | cont(p, dq.init.init :+ obligation) 113 | } 114 | 115 | // Primatives for providing an instance for Doc. 116 | def append(d1: Doc, d2: Doc): Doc = 117 | new Doc(iw => 118 | cont1 => 119 | suspend(for { 120 | cont2 <- d2(iw)(cont1) 121 | c3 <- d1(iw)(cont2) 122 | } yield c3) 123 | ) 124 | 125 | def group(d: Doc): Doc = 126 | new Doc(iw => 127 | cont1 => { 128 | suspend(d(iw)(leave(cont1)).map { cont2 => (pos: Position, dq: Dq) => 129 | { 130 | // obligation to write 131 | val obligation = (_: Horizontal) => (o: Out) => done(o) 132 | cont2(pos, dq :+ ((pos, obligation))) 133 | } 134 | }) 135 | } 136 | ) 137 | 138 | /** 139 | * Output text on the line if horizontal is true, otherwise `\n` and indent. 140 | */ 141 | private def line(text: String): Doc = 142 | new Doc({ case (i, w) => 143 | val textLength = text.length 144 | val outLine = 145 | (horizontal: Horizontal) => 146 | (o: Out) => 147 | done((r: Remaining) => 148 | if (horizontal) 149 | output(o, r - textLength, text) 150 | else 151 | output(o, w - i, "\n" + " " * i) 152 | ) 153 | scan(textLength, outLine) 154 | }) 155 | def nest(j: Indent, d: Doc): Doc = new Doc({ case (i, w) => d((i + j, w)) }) 156 | 157 | def text(s: String): Doc = 158 | new Doc({ iw => 159 | val stringLength = s.length 160 | val outGroupFunc = 161 | (_: Horizontal) => (o: Out) => done((r: Remaining) => output(o, r - stringLength, s)) 162 | scan(stringLength, outGroupFunc)(_) 163 | }) 164 | 165 | def column(f: Int => Doc): Doc = 166 | new Doc({ case (indent, width) => 167 | cont => 168 | done((position: Position, dq: Dq) => 169 | done((remain: Remaining) => 170 | for { 171 | cont1 <- f(width - remain)((indent, width))(cont) 172 | out <- cont1(position, dq) 173 | bp <- out(remain) 174 | } yield bp 175 | ) 176 | ) 177 | }) 178 | 179 | def nesting(f: Int => Doc): Doc = new Doc({ case iw @ (i, _) => f(i)(iw) }) 180 | 181 | // Derived combinators 182 | 183 | def hang(d: Doc, i: Indent): Doc = align(nest(i, d)) 184 | 185 | def indent(i: Indent, d: Doc): Doc = hang(spaces(i).append(d), i) 186 | 187 | /** 188 | * Append spaces to d until it's requestedSpaces. If d is already wide enough, increase nesting 189 | * and add a line break. 190 | */ 191 | def fillBreak(requestedWidth: Int, d: Doc): Doc = 192 | width( 193 | d, 194 | { w => 195 | if (w > requestedWidth) { 196 | nest(requestedWidth, line) 197 | } else { 198 | // Insert the right amount of spaces 199 | spaces(requestedWidth - w) 200 | } 201 | } 202 | ) 203 | 204 | def string(s: String): Doc = 205 | if (s == "") { 206 | Empty 207 | } else if (s.head == '\n') { 208 | line.append(string(s.tail)) 209 | } else { 210 | val (beforeNewLine, afterNewline) = s.span(_ != '\n') 211 | text(beforeNewLine).append(string(afterNewline)) 212 | } 213 | 214 | def line: Doc = line(" ") 215 | 216 | def linebreak: Doc = line("") 217 | 218 | def width(d: Doc, f: Int => Doc): Doc = column(j => append(d, column(k => f(k - j)))) 219 | 220 | def align(d: Doc): Doc = column(current => nesting(indent => nest(current - indent, d))) 221 | 222 | // Fold up the docs using f, empty if it's Nil. 223 | def foldDoc(docs: Seq[Doc])(f: (Doc, Doc) => Doc): Doc = 224 | docs match { 225 | case Nil => Empty 226 | case docs => docs.reduceLeft(f) 227 | } 228 | 229 | // Separate the docs by a space. 230 | def hsep(docs: List[Doc]): Doc = foldDoc(docs.intersperse(space))(append(_, _)) 231 | 232 | def spaces(n: Int): Doc = 233 | if (n <= 0) 234 | Empty 235 | else 236 | text(" " * n) 237 | 238 | /** 239 | * Space if the resulting output fits on the line, otherwise it behaves like line. 240 | */ 241 | def softLine: Doc = group(line) 242 | 243 | def space: Doc = char(' ') 244 | 245 | def char(c: Char): Doc = if (c == '\n') line else text(c.toString) 246 | 247 | def enclose(left: Doc, d: Doc, right: Doc): Doc = left.append(d).append(right) 248 | 249 | def prettyRender(w: Width, doc: Doc): String = { 250 | val end = (_: Position, _: Dq) => done((_: Remaining) => done("")) 251 | 252 | val trampResult = for { 253 | cont <- doc(0 -> w)(end) 254 | out <- cont(0, Queue.empty) 255 | result <- out(w) 256 | } yield result 257 | trampResult.run 258 | } 259 | } 260 | 261 | class Doc(step: Doc.Cont) { self => 262 | import Doc._ 263 | def apply(iw: IW): TreeCont => Free.Trampoline[TreeCont] = step(iw) 264 | 265 | def append(d2: Doc): Doc = Doc.append(self, d2) 266 | def withSpace(d: Doc): Doc = enclose(self, space, d) 267 | def withSoftline(d: Doc): Doc = enclose(self, softLine, d) 268 | def withLine(d: Doc): Doc = enclose(self, line, d) 269 | def brackets: Doc = enclose(text("["), self, text("]")) 270 | def parens: Doc = enclose(text("("), self, text(")")) 271 | def align: Doc = column(current => nesting(indent => nest(current - indent, self))) 272 | def pretty(width: Width): String = prettyRender(width, self) 273 | } 274 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/common/Common.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.common 2 | 3 | import optparse_applicative.internal._ 4 | import optparse_applicative.types._ 5 | 6 | import scalaz._ 7 | import scalaz.std.option._ 8 | import scalaz.syntax.std.option._ 9 | import scalaz.syntax.monadPlus._ 10 | 11 | private[optparse_applicative] trait Common { 12 | def showOption(name: OptName): String = 13 | name match { 14 | case OptLong(n) => s"--$n" 15 | case OptShort(n) => s"-$n" 16 | } 17 | 18 | def argMatches[F[_], A](opt: OptReader[A], arg: String)(implicit F: MonadP[F]): Option[StateT[Args, F, A]] = 19 | opt match { 20 | case ArgReader(rdr) => 21 | Some(runReadM(rdr.reader, arg).liftM[({ type l[a[_], b] = StateT[Args, a, b] })#l]) 22 | case CmdReader(_, f) => 23 | f(arg).map { subp => 24 | StateT[Args, F, A] { args => 25 | for { 26 | _ <- F.setContext(Some(arg), subp) 27 | prefs <- F.getPrefs 28 | runSub <- 29 | if (prefs.backtrack) 30 | runParser(getPolicy(subp), subp.parser, args) 31 | else 32 | runParserInfo(subp, args).map(Nil -> _) 33 | } yield runSub 34 | } 35 | } 36 | case _ => None 37 | } 38 | 39 | private def argsMState[F[_]: Monad] = MonadState[StateT[Args, F, *], Args] 40 | 41 | def optMatches[F[_], A](disambiguate: Boolean, opt: OptReader[A], word: OptWord)(implicit 42 | F: MonadP[F] 43 | ): Option[StateT[Args, F, A]] = { 44 | def hasName(n: OptName, ns: List[OptName]): Boolean = 45 | if (disambiguate) ns.exists(isOptionPrefix(n, _)) else ns.contains(n) 46 | 47 | def errorFor(name: OptName, e: ParseError) = 48 | e match { 49 | case ErrorMsg(msg) => F.error(ErrorMsg(s"option ${showOption(name)}: $msg")) 50 | case _ => F.error(e) 51 | } 52 | 53 | val state = argsMState[F] 54 | opt match { 55 | case OptionReader(names, rdr, noArgErr) if hasName(word.name, names) => 56 | val read: StateT[Args, F, A] = for { 57 | args <- state.get 58 | mbArgs = uncons(word.value.toList ++ args) 59 | missingArg = 60 | F.missingArg[(String, List[String])](noArgErr).liftM[({ type l[a[_], b] = StateT[Args, a, b] })#l] 61 | as <- mbArgs.fold(missingArg)(_.point[StateT[Args, F, *]]) 62 | (arg1, args1) = as 63 | _ <- state.put(args1) 64 | run <- 65 | rdr.reader.run 66 | .run(arg1) 67 | .fold( 68 | e => errorFor(word.name, e).liftM[({ type l[a[_], b] = StateT[Args, a, b] })#l], 69 | r => r.point[StateT[Args, F, *]] 70 | ) 71 | } yield run 72 | Some(read) 73 | case FlagReader(names, x) if hasName(word.name, names) && word.value.isEmpty => 74 | Some(x.point[StateT[Args, F, *]]) 75 | case _ => None 76 | } 77 | } 78 | 79 | def isArg[A](r: OptReader[A]): Boolean = 80 | r match { 81 | case ArgReader(_) => true 82 | case _ => false 83 | } 84 | 85 | def parseWord(s: String): Option[OptWord] = 86 | if (s.startsWith("--")) { 87 | val w = s.drop(2) 88 | val (opt, arg) = w.span(_ != '=') match { 89 | case (_, "") => (w, None) 90 | case (w1, w2) => (w1, Some(w2.tail)) 91 | } 92 | Some(OptWord(OptLong(opt), arg)) 93 | } else if (s.startsWith("-")) { 94 | s.drop(1) match { 95 | case "" => None 96 | case w => 97 | val (a, rest) = w.splitAt(1) 98 | val arg = Some(rest).filter(_.nonEmpty) 99 | Some(OptWord(OptShort(a.head), arg)) 100 | } 101 | } else None 102 | 103 | def searchParser[F[_]: Monad, A](f: Opt ~> NondetT[F, *], p: Parser[A]): NondetT[F, Parser[A]] = 104 | p match { 105 | case NilP(_) => PlusEmpty[NondetT[F, *]].empty 106 | case OptP(opt) => f(opt).map(Applicative[Parser].point(_)) 107 | case m @ MultP() => 108 | searchParser(f, m.p1).map(Apply[Parser].ap(m.p2)(_)) ! searchParser(f, m.p2).map(Apply[Parser].ap(_)(m.p1)) 109 | case AltP(p1, p2) => 110 | searchParser(f, p1) <+> searchParser(f, p2) 111 | case bindP @ BindP(p, k) => 112 | for { 113 | p1 <- searchParser(f, p) 114 | x <- evalParser(p1).orEmpty[NondetT[F, *]] 115 | } yield k(x) 116 | } 117 | 118 | /** 119 | * The default value of a Parser. This function returns an error if any of the options don't have a default value 120 | */ 121 | def evalParser[A](p: Parser[A]): Option[A] = 122 | p match { 123 | case NilP(r) => r 124 | case OptP(_) => None 125 | case m @ MultP() => Apply[Option].ap(evalParser(m.p2))(evalParser(m.p1)) 126 | case AltP(p1, p2) => evalParser(p1) <+> evalParser(p2) 127 | case BindP(p, k) => evalParser(p) >>= k.andThen(evalParser[A]) 128 | } 129 | 130 | /** 131 | * Map a polymorphic function over all the options of a parser, and collect the results in a list. 132 | */ 133 | def mapParser[A, B](f: OptHelpInfo => (Opt ~> Const[B, *]), p: Parser[A]): List[B] = { 134 | def flatten[AA](t: OptTree[AA]): List[AA] = 135 | t match { 136 | case Leaf(x) => List(x) 137 | case MultNode(xs) => xs.flatMap(flatten) 138 | case AltNode(xs) => xs.flatMap(flatten) 139 | } 140 | flatten(treeMapParser(f, p)) 141 | } 142 | 143 | /** 144 | * Like mapParser, but collect the results in a tree structure. 145 | */ 146 | def treeMapParser[A, B](g: OptHelpInfo => (Opt ~> Const[B, *]), p: Parser[A]): OptTree[B] = { 147 | def hasDefault[AA](p: Parser[AA]): Boolean = 148 | evalParser(p).isDefined 149 | 150 | def go[AA](m: Boolean, d: Boolean, f: OptHelpInfo => (Opt ~> Const[B, *]), p: Parser[AA]): OptTree[B] = 151 | p match { 152 | case NilP(_) => MultNode(Nil) 153 | case OptP(opt) if opt.props.visibility > Internal => Leaf(f(OptHelpInfo(m, d))(opt).getConst) 154 | case OptP(opt) => MultNode(Nil) 155 | case x @ MultP() => MultNode(List(go(m, d, f, x.p1), go(m, d, f, x.p2))) 156 | case AltP(p1, p2) => 157 | val d1 = d || hasDefault(p1) || hasDefault(p2) 158 | AltNode(List(go(m, d1, f, p1), go(m, d1, f, p2))) 159 | case BindP(p, _) => go(true, d, f, p) 160 | } 161 | 162 | simplify(go(false, false, g, p)) 163 | } 164 | 165 | def simplify[A](as: OptTree[A]): OptTree[A] = { 166 | def removeMult[AA](as: OptTree[AA]): List[OptTree[AA]] = 167 | as match { 168 | case MultNode(ts) => ts 169 | case t => List(t) 170 | } 171 | 172 | def removeAlt[AA](as: OptTree[AA]): List[OptTree[AA]] = 173 | as match { 174 | case AltNode(ts) => ts 175 | case MultNode(Nil) => Nil 176 | case t => List(t) 177 | } 178 | 179 | as match { 180 | case Leaf(x) => as 181 | case MultNode(xs) => 182 | xs.flatMap(x => removeMult(simplify(x))) match { 183 | case List(x) => x 184 | case xs => MultNode(xs) 185 | } 186 | case AltNode(xs) => 187 | xs.flatMap(x => removeAlt(simplify(x))) match { 188 | case Nil => MultNode(Nil) 189 | case List(x) => x 190 | case xs => AltNode(xs) 191 | } 192 | } 193 | } 194 | 195 | def isOptionPrefix(n1: OptName, n2: OptName): Boolean = 196 | (n1, n2) match { 197 | case (OptShort(x), OptShort(y)) => x == y 198 | case (OptLong(x), OptLong(y)) => y.startsWith(x) 199 | case _ => false 200 | } 201 | 202 | /** Create a parser composed of a single operation. */ 203 | def liftOpt[A](opt: Opt[A]): Parser[A] = OptP(opt) 204 | 205 | trait ArgsState[F[_]] { 206 | type G[A] = StateT[Args, F, A] 207 | } 208 | 209 | def searchOpt[F[_]: MonadP, A](pprefs: ParserPrefs, w: OptWord, p: Parser[A]): NondetT[ArgsState[F]#G, Parser[A]] = { 210 | val f = new (Opt ~> NondetT[ArgsState[F]#G, *]) { 211 | def apply[AA](fa: Opt[AA]): NondetT[ArgsState[F]#G, AA] = { 212 | val disambiguate = pprefs.disambiguate && fa.props.visibility > Internal 213 | optMatches(disambiguate, fa.main, w) match { 214 | case Some(matcher) => matcher.liftM[NondetT] 215 | case None => NondetT.empty[ArgsState[F]#G, AA] 216 | } 217 | } 218 | } 219 | searchParser[ArgsState[F]#G, A](f, p) 220 | } 221 | 222 | import NondetT._ 223 | 224 | def searchArg[F[_]: MonadP, A](arg: String, p: Parser[A]): NondetT[ArgsState[F]#G, Parser[A]] = { 225 | val f = new (Opt ~> NondetT[ArgsState[F]#G, *]) { 226 | def apply[AA](fa: Opt[AA]): NondetT[ArgsState[F]#G, AA] = 227 | (if (isArg(fa.main)) cut[ArgsState[F]#G] else NondetT.pure[ArgsState[F]#G, Unit](())).flatMap(p => 228 | argMatches[F, AA](fa.main, arg) match { 229 | case Some(matcher) => matcher.liftM[NondetT] 230 | case None => NondetT.empty[ArgsState[F]#G, AA] 231 | } 232 | ) 233 | } 234 | searchParser[ArgsState[F]#G, A](f, p) 235 | } 236 | 237 | def stepParser[F[_]: MonadP, A]( 238 | pprefs: ParserPrefs, 239 | policy: ArgPolicy, 240 | arg: String, 241 | p: Parser[A] 242 | ): NondetT[ArgsState[F]#G, Parser[A]] = 243 | policy match { 244 | case SkipOpts => 245 | parseWord(arg) match { 246 | case Some(w) => searchOpt(pprefs, w, p) 247 | case None => searchArg(arg, p) 248 | } 249 | case AllowOpts => 250 | val p1: NondetT[ArgsState[F]#G, Parser[A]] = searchArg[F, A](arg, p) 251 | val ev = NondetT.nondetTMonadPlus[ArgsState[F]#G] 252 | val w = parseWord(arg).orEmpty[NondetT[ArgsState[F]#G, *]](using ev, ev) 253 | val p2 = w.flatMap(searchOpt[F, A](pprefs, _, p)) 254 | p1 orElse p2 255 | } 256 | 257 | /** 258 | * Apply a Parser to a command line, and return a result and leftover arguments. 259 | * This function returns an error if any parsing error occurs, or if any options are missing and don't have a default value. 260 | */ 261 | def runParser[F[_], A](policy: ArgPolicy, p: Parser[A], args: Args)(implicit F: MonadP[F]): F[(Args, A)] = { 262 | lazy val result = evalParser(p).map(args -> _) 263 | 264 | def doStep(prefs: ParserPrefs, arg: String, argt: Args): F[(Args, Option[Parser[A]])] = 265 | disamb[ArgsState[F]#G, Parser[A]](!prefs.disambiguate, stepParser(prefs, policy, arg, p)).run(argt) 266 | 267 | (policy, args) match { 268 | case (SkipOpts, "--" :: argt) => runParser(AllowOpts, p, argt) 269 | case (_, Nil) => F.exit(p, result) 270 | case (_, arg :: argt) => 271 | for { 272 | prefs <- F.getPrefs 273 | s <- doStep(prefs, arg, argt) 274 | (args1, mp) = s 275 | run <- mp match { 276 | case None => result.orEmpty[F] <+> parseError(arg) 277 | case Some(p1) => runParser(policy, p1, args1) 278 | } 279 | } yield run 280 | } 281 | } 282 | 283 | def parseError[F[_], A](arg: String)(implicit F: MonadP[F]): F[A] = { 284 | val msg = 285 | if (arg.startsWith("-")) s"Invalid option `$arg'" 286 | else s"Invalid argument `$arg'" 287 | F.error(ErrorMsg(msg)) 288 | } 289 | 290 | def getPolicy[A](i: ParserInfo[A]): ArgPolicy = 291 | if (i.intersperse) SkipOpts else AllowOpts 292 | 293 | def runParserInfo[F[_]: MonadP, A](i: ParserInfo[A], args: Args): F[A] = 294 | runParserFully(getPolicy(i), i.parser, args) 295 | 296 | def runParserFully[F[_]: MonadP, A](policy: ArgPolicy, p: Parser[A], args: Args): F[A] = 297 | for (case (Nil, r) <- runParser(policy, p, args)) yield r 298 | } 299 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/optparse_applicative/builder/Builder.scala: -------------------------------------------------------------------------------- 1 | package optparse_applicative.builder 2 | 3 | import optparse_applicative.internal.min 4 | import optparse_applicative.builder.internal._ 5 | import optparse_applicative.types._ 6 | import optparse_applicative.helpdoc.Chunk 7 | 8 | import scalaz._ 9 | import scalaz.syntax.semigroup._ 10 | import scalaz.syntax.monadPlus._ 11 | import scalaz.syntax.foldable._ 12 | import scalaz.std.list._ 13 | 14 | import scala.util.control.NonFatal 15 | 16 | private[optparse_applicative] trait Builder { 17 | // Since Scalaz has no Read type class, there is no 'auto' function here. 18 | // Instead, I've implemented fromTryCatch, so you can use Scala's unsafe conversions such as 'toInt' 19 | 20 | /** String reader. */ 21 | val readStr: ReadM[String] = ReadM.ask 22 | 23 | /** Int reader. */ 24 | val readInt: ReadM[Int] = fromTryCatch(_.toInt) 25 | 26 | /** Char reader */ 27 | val readChar: ReadM[Char] = 28 | ReadM.ask.flatMap { arg => 29 | arg.toList match { 30 | case c :: Nil => c.point[ReadM] 31 | case _ => ReadM.error(s"cannot parse value `$arg'") 32 | } 33 | } 34 | 35 | /** Byte reader */ 36 | val readByte: ReadM[Byte] = fromTryCatch(_.toByte) 37 | 38 | /** Short reader */ 39 | val readShort: ReadM[Short] = fromTryCatch(_.toShort) 40 | 41 | /** Long reader */ 42 | val readLong: ReadM[Long] = fromTryCatch(_.toLong) 43 | 44 | /** BigInt reader */ 45 | val readBigInt: ReadM[BigInt] = fromTryCatch(BigInt.apply) 46 | 47 | /** Float reader */ 48 | val readFloat: ReadM[Float] = fromTryCatch(_.toFloat) 49 | 50 | /** Double reader */ 51 | val readDouble: ReadM[Double] = fromTryCatch(_.toDouble) 52 | 53 | /** BigDecimal reader */ 54 | val readBigDecimal: ReadM[BigDecimal] = fromTryCatch(BigDecimal.apply) 55 | 56 | /** Turns an unsafe conversion function into a reader by catching non-fatal exceptions. */ 57 | def fromTryCatch[A](f: String => A): ReadM[A] = 58 | ReadM.mkReadM { arg => 59 | try { \/-(f(arg)) } 60 | catch { case NonFatal(_) => -\/(ErrorMsg(s"cannot parse value `$arg'")) } 61 | } 62 | 63 | /** Null Option reader. All arguments will fail validation. */ 64 | def disabled[A]: ReadM[A] = 65 | ReadM.error("disabled option") 66 | 67 | /** Specify a short name for an option. */ 68 | def short[F[_], A](c: Char)(implicit F: HasName[F]): Mod[F, A] = 69 | Mod.field(F.name[A](OptShort(c), _)) 70 | 71 | /** Specify a long name for an option. */ 72 | def long[F[_], A](s: String)(implicit F: HasName[F]): Mod[F, A] = 73 | Mod.field(F.name[A](OptLong(s), _)) 74 | 75 | /** Specify a default value for an option. */ 76 | def value[F[_], A](a: A): Mod[F, A] = 77 | Mod(identity, DefaultProp(Some(a), None), identity) 78 | 79 | /** Specify a function to show the default value for an option. */ 80 | def showDefaultWith[F[_], A](f: A => String): Mod[F, A] = 81 | Mod(identity, DefaultProp(None, Some(f)), identity) 82 | 83 | /** Show the default value for this option using its Show instance. */ 84 | def showDefault[F[_], A](implicit A: Show[A]): Mod[F, A] = 85 | showDefaultWith(a => A.show(a).toString) 86 | 87 | /** Specify the help text for an option. */ 88 | def help[F[_], A](s: String): Mod[F, A] = 89 | Mod.option(_.copy(help = Chunk.paragraph(s))) 90 | 91 | /** Specify the help Doc. */ 92 | def helpDoc[F[_], A](doc: Option[Doc]): Mod[F, A] = 93 | Mod.option(_.copy(help = Chunk(doc))) 94 | 95 | /** Convert a function in the Either monad to a reader. */ 96 | def eitherReader[A](f: String => String \/ A): ReadM[A] = 97 | ReadM.ask.flatMap(arg => f(arg).fold(ReadM.error, _.point[ReadM])) 98 | 99 | /** Specify the error to display when no argument is provided to this option. */ 100 | def noArgError[A](e: ParseError): Mod[OptionFields, A] = 101 | Mod.field(_.copy(noArgError = e)) 102 | 103 | /** 104 | * Specify a metavariable for the argument. 105 | * 106 | * Metavariables have no effect on the parser, and only serve to specify the symbolic name for 107 | * an argument to be displayed in the help text. 108 | */ 109 | def metavar[F[_], A](v: String): Mod[F, A] = 110 | Mod.option(_.copy(metaVar = v)) 111 | 112 | /** Hide this option from the brief description. */ 113 | def hidden[F[_], A]: Mod[F, A] = 114 | Mod.option(p => p.copy(visibility = min(Hidden, p.visibility))) 115 | 116 | /** Add a command to a subparser option. */ 117 | def command[A](cmd: String, info: ParserInfo[A]): Mod[CommandFields, A] = 118 | Mod.field(p => p.copy(commands = (cmd, info) :: p.commands)) 119 | 120 | /** Builder for a command parser. The command modifier can be used to specify individual commands. */ 121 | def subparser[A](mod: Mod[CommandFields, A]*): Parser[A] = { 122 | val m = mod.toList.suml 123 | val Mod(_, d, g) = metavar[CommandFields, A]("COMMAND") |+| m 124 | val reader = Function.tupled(CmdReader.apply[A] _)(mkCommand(m)) 125 | mkParser(d, g, reader) 126 | } 127 | 128 | /** Builder for an argument parser. */ 129 | def argument[A](p: ReadM[A], mod: Mod[ArgumentFields, A]*): Parser[A] = { 130 | val m = mod.toList.suml 131 | mkParser(m.prop, m.g, ArgReader(CReader(p))) 132 | } 133 | 134 | /** Builder for a String argument. */ 135 | def strArgument(mod: Mod[ArgumentFields, String]*): Parser[String] = makeArgument(readStr, mod) 136 | 137 | /** Builder for a Int argument. */ 138 | def intArgument(mod: Mod[ArgumentFields, Int]*): Parser[Int] = makeArgument(readInt, mod) 139 | 140 | /** Builder for a Char argument. */ 141 | def charArgument(mod: Mod[ArgumentFields, Char]*): Parser[Char] = makeArgument(readChar, mod) 142 | 143 | /** Builder for a Byte argument. */ 144 | def byteArgument(mod: Mod[ArgumentFields, Byte]*): Parser[Byte] = makeArgument(readByte, mod) 145 | 146 | /** Builder for a Short argument. */ 147 | def shortArgument(mod: Mod[ArgumentFields, Short]*): Parser[Short] = makeArgument(readShort, mod) 148 | 149 | /** Builder for a Long argument. */ 150 | def longArgument(mod: Mod[ArgumentFields, Long]*): Parser[Long] = makeArgument(readLong, mod) 151 | 152 | /** Builder for a BigInt argument. */ 153 | def bigIntArgument(mod: Mod[ArgumentFields, BigInt]*): Parser[BigInt] = makeArgument(readBigInt, mod) 154 | 155 | /** Builder for a Float argument. */ 156 | def floatArgument(mod: Mod[ArgumentFields, Float]*): Parser[Float] = makeArgument(readFloat, mod) 157 | 158 | /** Builder for a Double argument. */ 159 | def doubleArgument(mod: Mod[ArgumentFields, Double]*): Parser[Double] = makeArgument(readDouble, mod) 160 | 161 | /** Builder for a BigDecimal argument. */ 162 | def bigDecimalArgument(mod: Mod[ArgumentFields, BigDecimal]*): Parser[BigDecimal] = makeArgument(readBigDecimal, mod) 163 | 164 | private def makeArgument[A](readM: ReadM[A], mod: Seq[Mod[ArgumentFields, A]]): Parser[A] = 165 | argument(readM, mod.toList.suml) 166 | 167 | /** Builder for a flag parser. */ 168 | def flag[A](defV: A, actV: A, mod: Mod[FlagFields, A]*): Parser[A] = 169 | Plus[Parser].plus(flag_(actV, mod.toList.suml), Applicative[Parser].point(defV)) 170 | 171 | /** Builder for a flag parser without a default value. */ 172 | def flag_[A](actV: A, mod: Mod[FlagFields, A]*): Parser[A] = { 173 | val m = mod.toList.suml 174 | val fields = m.f(FlagFields(Nil, actV)) 175 | val reader = FlagReader(fields.names, fields.active) 176 | mkParser(m.prop, m.g, reader) 177 | } 178 | 179 | /** Builder for a boolean flag. */ 180 | def switch(mod: Mod[FlagFields, Boolean]*): Parser[Boolean] = 181 | flag(false, true, mod.toList.suml) 182 | 183 | /** An option that always fails. */ 184 | def abortOption[A](err: ParseError, mod: Mod[OptionFields, A => A]*): Parser[A => A] = 185 | option(ReadM.abort(err), noArgError[A => A](err) |+| value(identity) |+| metavar("") |+| mod.toList.suml) 186 | 187 | /** An option that always fails and displays a message. */ 188 | def infoOption[A](s: String, mod: Mod[OptionFields, A => A]*): Parser[A => A] = 189 | abortOption(InfoMsg(s), mod.toList.suml) 190 | 191 | /** Builder for an option taking a String argument. */ 192 | def strOption(mod: Mod[OptionFields, String]*): Parser[String] = makeOption(readStr, mod) 193 | 194 | /** Builder for an option taking a Int argument. */ 195 | def intOption(mod: Mod[OptionFields, Int]*): Parser[Int] = makeOption(readInt, mod) 196 | 197 | /** Builder for an option taking a Char argument. */ 198 | def charOption(mod: Mod[OptionFields, Char]*): Parser[Char] = makeOption(readChar, mod) 199 | 200 | /** Builder for an option taking a Byte argument. */ 201 | def byteOption(mod: Mod[OptionFields, Byte]*): Parser[Byte] = makeOption(readByte, mod) 202 | 203 | /** Builder for an option taking a Short argument. */ 204 | def shortOption(mod: Mod[OptionFields, Short]*): Parser[Short] = makeOption(readShort, mod) 205 | 206 | /** Builder for an option taking a Long argument. */ 207 | def longOption(mod: Mod[OptionFields, Long]*): Parser[Long] = makeOption(readLong, mod) 208 | 209 | /** Builder for an option taking a BigInt argument. */ 210 | def bigIntOption(mod: Mod[OptionFields, BigInt]*): Parser[BigInt] = makeOption(readBigInt, mod) 211 | 212 | /** Builder for an option taking a Float argument. */ 213 | def floatOption(mod: Mod[OptionFields, Float]*): Parser[Float] = makeOption(readFloat, mod) 214 | 215 | /** Builder for an option taking a Double argument. */ 216 | def doubleOption(mod: Mod[OptionFields, Double]*): Parser[Double] = makeOption(readDouble, mod) 217 | 218 | /** Builder for an option taking a BigDecimal argument. */ 219 | def bigDecimalOption(mod: Mod[OptionFields, BigDecimal]*): Parser[BigDecimal] = makeOption(readBigDecimal, mod) 220 | 221 | private def makeOption[A](readM: ReadM[A], mod: Seq[Mod[OptionFields, A]]): Parser[A] = 222 | option(readM, mod.toList.suml) 223 | 224 | def option[A](r: ReadM[A], mod: Mod[OptionFields, A]*): Parser[A] = { 225 | val Mod(f, d, g) = metavar[OptionFields, A]("ARG") |+| mod.toList.suml 226 | val fields = f(OptionFields(Nil, ErrorMsg(""))) 227 | val cReader = CReader(r) 228 | val reader = OptionReader(fields.names, cReader, fields.noArgError) 229 | mkParser(d, g, reader) 230 | } 231 | 232 | type InfoMod[A] = Endo[ParserInfo[A]] 233 | 234 | /** Specify a short program description. */ 235 | def progDesc[A](desc: String): InfoMod[A] = 236 | Endo(_.copy(progDesc = Chunk.paragraph(desc))) 237 | 238 | def progDescDoc[A](doc: Option[Doc]): InfoMod[A] = 239 | Endo(_.copy(progDesc = Chunk(doc))) 240 | 241 | def failureCode[A](code: Int): InfoMod[A] = 242 | Endo(_.copy(failureCode = code)) 243 | 244 | def noIntersperse[A]: InfoMod[A] = 245 | Endo(_.copy(intersperse = false)) 246 | 247 | def header[A](header: String): InfoMod[A] = 248 | Endo(_.copy(header = Chunk.paragraph(header))) 249 | 250 | def headerDoc[A](doc: Option[Doc]): InfoMod[A] = 251 | Endo(_.copy(header = Chunk(doc))) 252 | 253 | def footer[A](footer: String): InfoMod[A] = 254 | Endo(_.copy(footer = Chunk.paragraph(footer))) 255 | 256 | def footerDoc[A](doc: Option[Doc]): InfoMod[A] = 257 | Endo(_.copy(footer = Chunk(doc))) 258 | 259 | import Chunk.empty 260 | 261 | def info[A](parser: Parser[A], mod: InfoMod[A]*): ParserInfo[A] = { 262 | val base = ParserInfo( 263 | parser = parser, 264 | fullDesc = true, 265 | progDesc = empty, 266 | header = empty, 267 | footer = empty, 268 | failureCode = 1, 269 | intersperse = true 270 | ) 271 | mod.toList.suml.run(base) 272 | } 273 | 274 | type PrefsMod = Endo[ParserPrefs] 275 | 276 | def multiSuffix(suffix: String): PrefsMod = 277 | Endo(_.copy(multiSuffix = suffix)) 278 | 279 | val disambiguate: PrefsMod = 280 | Endo(_.copy(disambiguate = true)) 281 | 282 | val showHelpOnError: PrefsMod = 283 | Endo(_.copy(showHelpOnError = true)) 284 | 285 | val noBacktrack: PrefsMod = 286 | Endo(_.copy(backtrack = false)) 287 | 288 | def columns(cols: Int): PrefsMod = 289 | Endo(_.copy(columns = cols)) 290 | 291 | def prefs(mod: PrefsMod*): ParserPrefs = { 292 | val base = 293 | ParserPrefs(multiSuffix = "", disambiguate = false, showHelpOnError = false, backtrack = true, columns = 80) 294 | mod.toList.suml.run(base) 295 | } 296 | 297 | /** Trivial option modifier. */ 298 | def idm[M](implicit M: Monoid[M]): M = 299 | M.zero 300 | } 301 | --------------------------------------------------------------------------------