├── .gitignore ├── mainargs ├── src-3 │ ├── acyclic.scala │ ├── ParserForMethodsCompanionVersionSpecific.scala │ ├── ParserForClassCompanionVersionSpecific.scala │ └── Macros.scala ├── src │ ├── Flag.scala │ ├── Leftover.scala │ ├── Annotations.scala │ ├── Util.scala │ ├── Result.scala │ ├── Invoker.scala │ ├── TokenGrouping.scala │ ├── Renderer.scala │ ├── TokensReader.scala │ └── Parser.scala ├── src-jvm │ └── Compat.scala ├── src-native │ └── Compat.scala ├── src-js │ └── Compat.scala ├── test │ ├── src-3 │ │ ├── VersionSpecific.scala │ │ └── VarargsScala2CompatTests.scala │ ├── src-2 │ │ └── VersionSpecific.scala │ ├── src │ │ ├── TestUtils.scala │ │ ├── VarargsOldTests.scala │ │ ├── Checker.scala │ │ ├── HygieneTests.scala │ │ ├── VarargsNewTests.scala │ │ ├── ConstantTests.scala │ │ ├── MultiTraitTests.scala │ │ ├── IssueTests.scala │ │ ├── VarargsWrappedTests.scala │ │ ├── HelloWorldTests.scala │ │ ├── InvocationArgs.scala │ │ ├── VarargsCustomTests.scala │ │ ├── ClassWithDefaultTests.scala │ │ ├── ParserTests.scala │ │ ├── EqualsSyntaxTests.scala │ │ ├── PositionalTests.scala │ │ ├── OptionSeqTests.scala │ │ ├── ManyTests.scala │ │ ├── FlagTests.scala │ │ ├── DashedArgumentName.scala │ │ ├── VarargsBaseTests.scala │ │ ├── ClassTests.scala │ │ └── CoreTests.scala │ └── src-jvm-2 │ │ ├── MillTests.scala │ │ └── AmmoniteTests.scala └── src-2 │ ├── ParserForClassCompanionVersionSpecific.scala │ ├── ParserForMethodsCompanionVersionSpecific.scala │ └── Macros.scala ├── .github ├── dependabot.yml └── workflows │ ├── run-tests.yml │ └── publish-artifacts.yml ├── .git-blame-ignore-revs ├── example ├── varargold │ └── src │ │ └── Main.scala ├── vararg │ └── src │ │ └── Main.scala ├── vararg2 │ └── src │ │ └── Main.scala ├── short │ └── src │ │ └── Main.scala ├── optseq │ └── src │ │ └── Main.scala ├── custom │ └── src │ │ └── Main.scala ├── hello │ └── src │ │ └── Main.scala ├── caseclass │ └── src │ │ └── Main.scala ├── hello2 │ └── src │ │ └── Main.scala └── classarg │ └── src │ └── Main.scala ├── .scalafmt.conf ├── LICENSE ├── mill └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | .idea 4 | out/ 5 | -------------------------------------------------------------------------------- /mainargs/src-3/acyclic.scala: -------------------------------------------------------------------------------- 1 | package acyclic 2 | 3 | def skipped = ??? 4 | -------------------------------------------------------------------------------- /mainargs/src/Flag.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | case class Flag(value: Boolean = false) 3 | -------------------------------------------------------------------------------- /mainargs/src/Leftover.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | case class Leftover[T](value: T*) 3 | -------------------------------------------------------------------------------- /mainargs/src-jvm/Compat.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object Compat { 4 | def exit(n: Int) = sys.exit(n) 5 | } 6 | -------------------------------------------------------------------------------- /mainargs/src-native/Compat.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object Compat { 4 | def exit(n: Int) = sys.exit(n) 5 | } 6 | -------------------------------------------------------------------------------- /mainargs/src-js/Compat.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object Compat { 4 | def exit(n: Int) = throw new Exception() 5 | } 6 | -------------------------------------------------------------------------------- /mainargs/test/src-3/VersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object VersionSpecific { 4 | val isScala3 = true 5 | } 6 | -------------------------------------------------------------------------------- /mainargs/test/src-2/VersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object VersionSpecific { 4 | val isScala3 = false 5 | } 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /mainargs/test/src/TestUtils.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object TestUtils { 4 | def scala2Only(f: => Unit): Unit = { 5 | if (VersionSpecific.isScala3) {} else f 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Reformatted all sourcefiles with scalafmt 2 | f6bdd1574b34dd2f7eb6737863bff6dbb876bbb2 3 | 4 | 5 | # Scala Steward: Reformat with scalafmt 3.7.2 6 | 306a9a501c20a74348d8640da8f2b1cb89172706 7 | -------------------------------------------------------------------------------- /mainargs/src-3/ParserForMethodsCompanionVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | private [mainargs] trait ParserForMethodsCompanionVersionSpecific { 4 | inline def apply[B](base: B): ParserForMethods[B] = ${ Macros.parserForMethods[B]('base) } 5 | } 6 | -------------------------------------------------------------------------------- /mainargs/src-3/ParserForClassCompanionVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import scala.language.experimental.macros 4 | 5 | private [mainargs] trait ParserForClassCompanionVersionSpecific { 6 | inline def apply[T]: ParserForClass[T] = ${ Macros.parserForClass[T] } 7 | } 8 | -------------------------------------------------------------------------------- /mainargs/src-2/ParserForClassCompanionVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import acyclic.skipped 4 | 5 | import scala.language.experimental.macros 6 | 7 | private[mainargs] trait ParserForClassCompanionVersionSpecific { 8 | def apply[T]: ParserForClass[T] = macro Macros.parserForClass[T] 9 | } 10 | -------------------------------------------------------------------------------- /mainargs/src-2/ParserForMethodsCompanionVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import acyclic.skipped 4 | 5 | import scala.language.experimental.macros 6 | 7 | private[mainargs] trait ParserForMethodsCompanionVersionSpecific { 8 | def apply[B](base: B): ParserForMethods[B] = macro Macros.parserForMethods[B] 9 | } 10 | -------------------------------------------------------------------------------- /example/varargold/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.vararg 2 | import mainargs.{main, arg, Parser, Leftover} 3 | 4 | object Main { 5 | @main 6 | def run(foo: String, myNum: Int, rest: String*) = { 7 | println(foo * myNum + " " + rest.value) 8 | } 9 | 10 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 11 | } 12 | -------------------------------------------------------------------------------- /example/vararg/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.vararg 2 | import mainargs.{main, arg, Parser, Leftover} 3 | 4 | object Main { 5 | @main 6 | def run(foo: String, myNum: Int = 2, rest: Leftover[String]) = { 7 | println(foo * myNum + " " + rest.value) 8 | } 9 | 10 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 11 | } 12 | -------------------------------------------------------------------------------- /example/vararg2/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.vararg2 2 | import mainargs.{main, arg, ParserForClass, Leftover} 3 | 4 | object Main { 5 | @main 6 | case class Config(foo: String, myNum: Int = 2, rest: Leftover[String]) 7 | 8 | def main(args: Array[String]): Unit = { 9 | val config = Parser[Config].constructOrExit(args) 10 | println(config) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/short/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.hello 2 | import mainargs.{main, arg, Parser, Flag} 3 | 4 | object Main { 5 | @main 6 | def bools(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) 7 | 8 | @main 9 | def strs(a: Flag, b: String) = println(Seq(a.value, b)) 10 | 11 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 12 | } 13 | -------------------------------------------------------------------------------- /example/optseq/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.optseq 2 | import mainargs.{main, arg, Parser, TokensReader} 3 | 4 | object Main { 5 | @main 6 | def runOpt(opt: Option[Int]) = println(opt) 7 | 8 | @main 9 | def runSeq(seq: Seq[Int]) = println(seq) 10 | 11 | @main 12 | def runVec(seq: Vector[Int]) = println(seq) 13 | 14 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 15 | } 16 | -------------------------------------------------------------------------------- /mainargs/src/Annotations.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import scala.annotation.ClassfileAnnotation 3 | 4 | class arg( 5 | val name: String = null, 6 | val short: Char = 0, 7 | val doc: String = null, 8 | val noDefaultName: Boolean = false, 9 | val positional: Boolean = false, 10 | val hidden: Boolean = false 11 | ) extends ClassfileAnnotation 12 | 13 | class main(val name: String = null, val doc: String = null) extends ClassfileAnnotation 14 | -------------------------------------------------------------------------------- /mainargs/test/src/VarargsOldTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object VarargsOldTests extends VarargsBaseTests { 5 | object Base { 6 | 7 | @main 8 | def pureVariadic(nums: Int*) = nums.sum 9 | 10 | @main 11 | def mixedVariadic(@arg(short = 'f') first: Int, args: String*) = 12 | first.toString + args.mkString 13 | } 14 | 15 | val check = new Checker(Parser(Base), allowPositional = true) 16 | val isNewVarargsTests = false 17 | } 18 | -------------------------------------------------------------------------------- /example/custom/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.custom 2 | import mainargs.{main, arg, Parser, TokensReader} 3 | 4 | object Main { 5 | implicit object PathRead extends TokensReader[os.Path]( 6 | "path", 7 | strs => Right(os.Path(strs.head, os.pwd)) 8 | ) 9 | @main 10 | def run(from: os.Path, to: os.Path) = { 11 | println("from: " + from) 12 | println("to: " + to) 13 | } 14 | 15 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 16 | } 17 | -------------------------------------------------------------------------------- /mainargs/test/src/Checker.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | class Checker[B](val parser: ParserForMethods[B], allowPositional: Boolean, nameMapper: String => Option[String] = Util.kebabCaseNameMapper) { 4 | val mains = parser.mains 5 | def parseInvoke(input: List[String]) = { 6 | parser.runRaw(input, allowPositional = allowPositional, nameMapper = nameMapper) 7 | } 8 | def apply[T](input: List[String], expected: Result[T]) = { 9 | val result = parseInvoke(input) 10 | utest.assert(result == expected) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/hello/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.hello 2 | import mainargs.{main, arg, Parser, Flag} 3 | 4 | object Main { 5 | @main 6 | def run( 7 | @arg(short = 'f', doc = "String to print repeatedly") 8 | foo: String, 9 | @arg(name = "my-num", doc = "How many times to print string") 10 | myNum: Int = 2, 11 | @arg(doc = "Example flag") 12 | bool: Flag 13 | ) = { 14 | println(foo * myNum + " " + bool.value) 15 | } 16 | 17 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 18 | } 19 | -------------------------------------------------------------------------------- /example/caseclass/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.caseclass 2 | import mainargs.{main, arg, ParserForClass, Flag} 3 | 4 | object Main { 5 | @main 6 | case class Config( 7 | @arg(short = 'f', doc = "String to print repeatedly") 8 | foo: String, 9 | @arg(name = "my-num", doc = "How many times to print string") 10 | myNum: Int = 2, 11 | @arg(doc = "Example flag") 12 | bool: Flag = Flag() 13 | ) 14 | def main(args: Array[String]): Unit = { 15 | val config = Parser[Config].constructOrExit(args) 16 | println(config) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.7.3" 2 | 3 | project.git = true 4 | 5 | align.preset = none 6 | align.openParenCallSite = false 7 | align.stripMargin = true 8 | 9 | assumeStandardLibraryStripMargin = true 10 | 11 | continuationIndent.callSite = 2 12 | continuationIndent.defnSite = 4 13 | 14 | docstrings.style = Asterisk 15 | docstrings.oneline = keep 16 | docstrings.wrap = no 17 | 18 | maxColumn = 100 19 | 20 | newlines.source = keep 21 | 22 | runner.dialect = scala213 23 | 24 | fileOverride { 25 | "glob:**/mainargs/src-3/**" { 26 | runner.dialect = scala3 27 | } 28 | "glob:**/mainargs/test/src-3/**" { 29 | runner.dialect = scala3 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mainargs/test/src/HygieneTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object HygieneTests extends TestSuite { 5 | 6 | object Main { 7 | @main 8 | def run( 9 | @arg(short = 'f', doc = "String to print repeatedly") 10 | foo: String, 11 | @arg(name = "my-num", doc = "How many times to print string") 12 | myNum: Int = 2, 13 | @arg(doc = "Example flag") 14 | bool: Flag 15 | ) = { 16 | foo * myNum + " " + bool.value 17 | } 18 | } 19 | 20 | val tests = Tests { 21 | import scala.collection.mutable._ 22 | test("importingSeqShouldntFailCompile") { 23 | Parser(Main) 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mainargs/test/src/VarargsNewTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | object VarargsNewTests extends VarargsBaseTests { 4 | object Base { 5 | @main 6 | def pureVariadic(nums: Leftover[Int]) = nums.value.sum 7 | 8 | @main 9 | def mixedVariadic(@arg(short = 'f') first: Int, args: Leftover[String]) = { 10 | first + args.value.mkString 11 | } 12 | @main 13 | def mixedVariadicWithDefault( 14 | @arg(short = 'f') first: Int = 1337, 15 | args: Leftover[String] 16 | ) = { 17 | first + args.value.mkString 18 | } 19 | } 20 | 21 | val check = new Checker(Parser(Base), allowPositional = true) 22 | val isNewVarargsTests = true 23 | } 24 | -------------------------------------------------------------------------------- /mainargs/test/src-3/VarargsScala2CompatTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object VarargsScala2CompatTests extends VarargsBaseTests { 5 | object Base { 6 | 7 | // (foo: scala.``[T]) === (foo: T*) in Scala 2 pickles (which can be read from Scala 3) 8 | // in Scala 3, the equivalent is (foo: Seq[T] @repeated) 9 | @main 10 | def pureVariadic(nums: scala.``[Int]) = nums.sum 11 | 12 | @main 13 | def mixedVariadic(@arg(short = 'f') first: Int, args: scala.``[String]) = 14 | first.toString + args.mkString 15 | } 16 | 17 | val check = new Checker(ParserForMethods(Base), allowPositional = true) 18 | val isNewVarargsTests = false 19 | } 20 | -------------------------------------------------------------------------------- /example/hello2/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.hello2 2 | import mainargs.{main, arg, Parser, Flag} 3 | 4 | object Main { 5 | @main 6 | def foo( 7 | @arg(short = 'f', doc = "String to print repeatedly") 8 | foo: String, 9 | @arg(name = "my-num", doc = "How many times to print string") 10 | myNum: Int = 2, 11 | @arg(doc = "Example flag") 12 | bool: Flag 13 | ) = { 14 | println(foo * myNum + " " + bool.value) 15 | } 16 | @main 17 | def bar( 18 | i: Int, 19 | @arg(doc = "Pass in a custom `s` to override it") 20 | s: String = "lols" 21 | ) = { 22 | println(s * i) 23 | } 24 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 25 | } 26 | -------------------------------------------------------------------------------- /mainargs/test/src/ConstantTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object ConstantTests extends TestSuite { 5 | 6 | case class Injected() 7 | implicit def InjectedTokensReader: TokensReader.Constant[Injected] = 8 | new TokensReader.Constant[Injected]{ 9 | def read() = Right(new Injected()) 10 | } 11 | object Base { 12 | @main 13 | def flaggy(a: Injected, b: Boolean) = a.toString + " " + b 14 | } 15 | val check = new Checker(Parser(Base), allowPositional = true) 16 | 17 | val tests = Tests { 18 | test - check( 19 | List("-b", "true"), 20 | Result.Success("Injected() true") 21 | ) 22 | test - check( 23 | List("-b", "false"), 24 | Result.Success("Injected() false") 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mainargs/test/src/MultiTraitTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | trait CommandList { 5 | @main 6 | def list(@arg v: String): String = v 7 | } 8 | 9 | trait CommandCopy { 10 | @main 11 | def copy(@arg from: String, @arg to: String): (String, String) = (from, to) 12 | } 13 | 14 | object Joined extends CommandCopy with CommandList { 15 | @main 16 | def test(@arg from: String, @arg to: String): (String, String) = (from, to) 17 | } 18 | 19 | object MultiTraitTests extends TestSuite { 20 | val check = new Checker(Parser(Joined), allowPositional = true) 21 | val tests = Tests { 22 | test - check(List("copy", "fromArg", "toArg"), Result.Success(("fromArg", "toArg"))) 23 | test - check(List("test", "fromArg", "toArg"), Result.Success(("fromArg", "toArg"))) 24 | test - check(List("list", "vArg"), Result.Success("vArg")) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mainargs/test/src/IssueTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object IssueTests extends TestSuite { 5 | 6 | object Main { 7 | @main 8 | def mycmd(@arg(name = "the-flag") f: mainargs.Flag = mainargs.Flag(false), 9 | @arg str: String = "s", 10 | args: Leftover[String]) = { 11 | (f.value, str, args.value) 12 | } 13 | } 14 | 15 | val tests = Tests { 16 | test("issue60") { 17 | test { 18 | val parsed = Parser(Main) 19 | .runEither(Seq("--str", "str", "a", "b", "c", "d"), allowPositional = true) 20 | 21 | assert(parsed == Right((false, "str", List("a", "b", "c", "d")))) 22 | } 23 | test { 24 | val parsed = Parser(Main) 25 | .runEither(Seq("a", "b", "c", "d"), allowPositional = true) 26 | 27 | assert(parsed == Right((false, "a", List("b", "c", "d")))) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/classarg/src/Main.scala: -------------------------------------------------------------------------------- 1 | package example.classarg 2 | import mainargs.{main, arg, Parser, ParserForClass, Flag} 3 | 4 | object Main { 5 | @main 6 | case class Config( 7 | @arg(short = 'f', doc = "String to print repeatedly") 8 | foo: String, 9 | @arg(name = "my-num", doc = "How many times to print string") 10 | myNum: Int = 2, 11 | @arg(doc = "Example flag") 12 | bool: Flag = Flag() 13 | ) 14 | implicit def configParser = Parser[Config] 15 | 16 | @main 17 | def bar( 18 | config: Config, 19 | @arg(name = "extra-message") 20 | extraMessage: String 21 | ) = { 22 | println(config.foo * config.myNum + " " + config.bool.value + " " + extraMessage) 23 | } 24 | @main 25 | def qux(config: Config, n: Int) = { 26 | println((config.foo * config.myNum + " " + config.bool.value + "\n") * n) 27 | } 28 | 29 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | java: ['11', '17'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Run tests 23 | run: | 24 | sed -i 's#//| mill-jvm-version: 11#//| mill-jvm-version: ${{ matrix.java }}#' build.mill 25 | head build.mill 26 | ./mill -i -k __.publishArtifacts + __.test 27 | 28 | check-binary-compatibility: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | - uses: actions/setup-java@v4 35 | with: 36 | distribution: 'temurin' 37 | java-version: 11 38 | - name: Check Binary Compatibility 39 | run: ./mill -i -k __.mimaReportBinaryIssues 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Li Haoyi (haoyi.sg@gmail.com) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Publish Artifacts 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish-sonatype: 11 | if: github.repository == 'com-lihaoyi/mainargs' 12 | runs-on: ubuntu-latest 13 | env: 14 | MILL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 15 | MILL_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 16 | MILL_PGP_SECRET_BASE64: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY }} 17 | MILL_PGP_PASSPHRASE: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY_PASSWORD }} 18 | LANG: "en_US.UTF-8" 19 | LC_MESSAGES: "en_US.UTF-8" 20 | LC_ALL: "en_US.UTF-8" 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Publish to Maven Central 25 | run: ./mill -i mill.scalalib.SonatypeCentralPublishModule/ 26 | 27 | - name: Create GitHub Release 28 | id: create_gh_release 29 | uses: actions/create-release@v1.1.4 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: ${{ github.ref }} 35 | draft: false 36 | -------------------------------------------------------------------------------- /mainargs/test/src/VarargsWrappedTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object VarargsWrappedTests extends VarargsBaseTests { 5 | // Test that we are able to wrap the `Leftover` type we use for Varargs in 6 | // our own custom types, and have things work 7 | class Wrapper[T](val unwrap: T) 8 | class WrapperRead[T](implicit wrapped: TokensReader.ShortNamed[T]) 9 | extends TokensReader.Leftover[Wrapper[T], T] { 10 | 11 | def read(strs: Seq[String]) = wrapped 12 | .asInstanceOf[TokensReader.Leftover[T, _]] 13 | .read(strs).map(new Wrapper(_)) 14 | 15 | def shortName = wrapped.shortName 16 | } 17 | 18 | implicit def WrapperRead[T: TokensReader.ShortNamed]: TokensReader[Wrapper[T]] = 19 | new WrapperRead[T] 20 | 21 | object Base { 22 | @main 23 | def pureVariadic(nums: Wrapper[Leftover[Int]]) = nums.unwrap.value.sum 24 | 25 | @main 26 | def mixedVariadic(@arg(short = 'f') first: Int, args: Wrapper[Leftover[String]]) = { 27 | first + args.unwrap.value.mkString 28 | } 29 | @main 30 | def mixedVariadicWithDefault( 31 | @arg(short = 'f') first: Int = 1337, 32 | args: Wrapper[Leftover[String]] 33 | ) = { 34 | first + args.unwrap.value.mkString 35 | } 36 | } 37 | 38 | val check = new Checker(Parser(Base), allowPositional = true) 39 | val isNewVarargsTests = true 40 | } 41 | -------------------------------------------------------------------------------- /mainargs/test/src/HelloWorldTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object HelloWorldTests extends TestSuite { 5 | 6 | object Main { 7 | @main 8 | def run( 9 | @arg(short = 'f', doc = "String to print repeatedly") 10 | foo: String, 11 | @arg(name = "my-num", doc = "How many times to print string") 12 | myNum: Int = 2, 13 | @arg(doc = "Example flag") 14 | bool: Flag 15 | ) = { 16 | foo * myNum + " " + bool.value 17 | } 18 | } 19 | 20 | val tests = Tests { 21 | test("explicit") { 22 | Parser(Main).runOrThrow(Array("--foo", "bar", "--my-num", "3")) ==> 23 | "barbarbar false" 24 | } 25 | test("short") { 26 | Parser(Main).runOrThrow(Array("-f", "bar")) ==> 27 | "barbar false" 28 | } 29 | test("default") { 30 | Parser(Main).runOrThrow(Array("--foo", "bar")) ==> 31 | "barbar false" 32 | } 33 | test("positional") { 34 | Parser(Main).runOrThrow(Array("qux", "4"), allowPositional = true) ==> 35 | "quxquxquxqux false" 36 | } 37 | test("flags") { 38 | Parser(Main).runOrThrow(Array("qux", "4", "--bool"), allowPositional = true) ==> 39 | "quxquxquxqux true" 40 | } 41 | test("either") { 42 | Parser(Main).runEither(Array("qux", "4", "--bool"), allowPositional = true) ==> 43 | Right("quxquxquxqux true") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mainargs/test/src/InvocationArgs.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | @main 4 | case class LargeArgs( 5 | v1: String, 6 | v2: String = "v2-default", 7 | v3: Option[String] = None, 8 | v4: Option[String] = None, 9 | v5: Int, 10 | v6: Int = 0, 11 | v7: Int = 123, 12 | v8: Int = 3.14.toInt, 13 | v9: Boolean, 14 | v10: Boolean = true, 15 | v11: Boolean = false, 16 | v12: Option[Int] = None, 17 | v13: Option[Int] = None, 18 | v14: String = "v14-default", 19 | v15: String = "v15-default", 20 | v16: String = "v16-default", 21 | v17: String = "v17-default", 22 | v18: String = "v18-default", 23 | v19: String = "v19-default", 24 | v20: String = "v20-default", 25 | v21: String = "v21-default", 26 | v22: String = "v22-default", 27 | v23: String = "v23-default", 28 | v24: String = "v24-default", 29 | v25: String = "v25-default", 30 | v26: String = "v26-default", 31 | v27: String = "v27-default", 32 | v28: String = "v28-default", 33 | v29: String = "v29-default", 34 | v30: String = "v30-default", 35 | v31: String = "v31-default", 36 | v32: String = "v32-default", 37 | ) 38 | 39 | object LargeClassTests extends TestSuite{ 40 | val largeArgsParser = Parser[LargeArgs] 41 | 42 | val tests = Tests { 43 | test("simple") { 44 | largeArgsParser.constructOrThrow( 45 | Seq("--v1", "v1-value", "--v5", "5", "--v9", "true", "--v23", "v23-value") 46 | ) ==> 47 | LargeArgs(v1 = "v1-value", v5 = 5, v9 = true, v23 = "v23-value") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mainargs/test/src/VarargsCustomTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object VarargsCustomTests extends VarargsBaseTests { 5 | // Test that we are able to replace the `Leftover` type entirely with our 6 | // own implementation 7 | class Wrapper[T](val unwrap: Seq[T]) 8 | class WrapperRead[T](implicit wrapped: TokensReader.Simple[T]) 9 | extends TokensReader.Leftover[Wrapper[T], T] { 10 | def read(strs: Seq[String]) = { 11 | val results = strs.map(s => implicitly[TokensReader.Simple[T]].read(Seq(s))) 12 | val failures = results.collect { case Left(x) => x } 13 | val successes = results.collect { case Right(x) => x } 14 | 15 | if (failures.nonEmpty) Left(failures.head) 16 | else Right(new Wrapper(successes)) 17 | } 18 | def shortName = wrapped.shortName 19 | } 20 | 21 | implicit def WrapperRead[T: TokensReader.Simple]: TokensReader[Wrapper[T]] = 22 | new WrapperRead[T] 23 | 24 | object Base { 25 | @main 26 | def pureVariadic(nums: Wrapper[Int]) = nums.unwrap.sum 27 | 28 | @main 29 | def mixedVariadic(@arg(short = 'f') first: Int, args: Wrapper[String]) = { 30 | first + args.unwrap.mkString 31 | } 32 | @main 33 | def mixedVariadicWithDefault( 34 | @arg(short = 'f') first: Int = 1337, 35 | args: Wrapper[String] 36 | ) = { 37 | first + args.unwrap.mkString 38 | } 39 | } 40 | 41 | val check = new Checker(Parser(Base), allowPositional = true) 42 | val isNewVarargsTests = true 43 | } 44 | -------------------------------------------------------------------------------- /mainargs/test/src/ClassWithDefaultTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | // Make sure 5 | object ClassWithDefaultTests extends TestSuite { 6 | @main 7 | case class Foo(x: Int, y: Int = 1) 8 | 9 | implicit val fooParser: ParserForClass[Foo] = Parser[Foo] 10 | 11 | object Main { 12 | @main 13 | def run(foo: Foo, bool: Boolean = false) = s"${foo.x} ${foo.y} $bool" 14 | } 15 | 16 | val mainParser = Parser(Main) 17 | 18 | val tests = Tests { 19 | test("simple") { 20 | test("success") { 21 | fooParser.constructOrThrow(Seq("-x", "1", "-y", "2")) ==> Foo(1, 2) 22 | } 23 | test("default") { 24 | fooParser.constructOrThrow(Seq("-x", "0")) ==> Foo(0, 1) 25 | } 26 | test("missing") { 27 | fooParser.constructRaw(Seq()) ==> 28 | Result.Failure.MismatchedArguments( 29 | Seq( 30 | ArgSig( 31 | None, 32 | Some('x'), 33 | None, 34 | None, 35 | mainargs.TokensReader.IntRead, 36 | positional = false, 37 | hidden = false 38 | ) 39 | ), 40 | List(), 41 | List(), 42 | None 43 | ) 44 | 45 | } 46 | } 47 | 48 | test("nested") { 49 | test("success"){ 50 | mainParser.runOrThrow(Seq("-x", "1", "-y", "2", "--bool", "true")) ==> "1 2 true" 51 | } 52 | test("default"){ 53 | mainParser.runOrThrow(Seq("-x", "1", "-y", "2")) ==> "1 2 false" 54 | } 55 | test("default2"){ 56 | mainParser.runOrThrow(Seq("-x", "0")) ==> "0 1 false" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mainargs/test/src/ParserTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object ParserTests extends TestSuite { 5 | 6 | object SingleBase { 7 | @main(doc = "Qux is a function that does stuff") 8 | def run( 9 | i: Int, 10 | @arg(doc = "Pass in a custom `s` to override it") 11 | s: String = "lols" 12 | ) = s * i 13 | } 14 | 15 | object MultiBase { 16 | @main 17 | def foo() = 1 18 | 19 | @main 20 | def bar(i: Int) = i 21 | } 22 | 23 | @main 24 | case class ClassBase(code: Option[String] = None, other: String = "hello") 25 | 26 | val multiMethodParser = Parser(MultiBase) 27 | val singleMethodParser = Parser(SingleBase) 28 | val classParser = Parser[ClassBase] 29 | val tests = Tests { 30 | test("runEitherMulti") { 31 | 32 | test { 33 | multiMethodParser.runEither(Array("foo")) ==> Right(1) 34 | } 35 | test { 36 | multiMethodParser.runEither(Array("bar", "-i", "123")) ==> Right(123) 37 | } 38 | test { 39 | assert( 40 | multiMethodParser 41 | .runEither(Array("f")) 42 | .left 43 | .exists(_.contains("Unable to find subcommand: f")) 44 | ) 45 | } 46 | } 47 | test("runEitherSingle") { 48 | singleMethodParser.runEither( 49 | Array("5", "x"), 50 | allowPositional = true 51 | ) ==> Right("xxxxx") 52 | } 53 | test("constructEither") { 54 | classParser.constructEither(Array("--code", "println(1)")) ==> 55 | Right(ClassBase(code = Some("println(1)"), other = "hello")) 56 | } 57 | test("simplerunOrExit") { 58 | singleMethodParser.runOrExit(Array("-i", "2")) ==> "lolslols" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /mainargs/test/src/EqualsSyntaxTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object EqualsSyntaxTests extends TestSuite { 5 | 6 | object Main { 7 | @main 8 | def run( 9 | @arg(short = 'f', doc = "String to print repeatedly") 10 | foo: String, 11 | @arg(doc = "How many times to print string") 12 | myNum: Int = 2, 13 | @arg(doc = "Example flag") 14 | bool: Flag 15 | ) = { 16 | foo * myNum + " " + bool.value 17 | } 18 | } 19 | 20 | val tests = Tests { 21 | test("simple") { 22 | Parser(Main).runOrThrow(Array("--foo=bar", "--my-num=3")) ==> 23 | "barbarbar false" 24 | } 25 | test("multipleEquals") { 26 | // --foo=bar syntax still works when there's an `=` on the right 27 | Parser(Main).runOrThrow(Array("--foo=bar=qux")) ==> 28 | "bar=quxbar=qux false" 29 | } 30 | test("empty") { 31 | // --foo= syntax sets `foo` to an empty string 32 | Parser(Main).runOrThrow(Array("--foo=")) ==> 33 | " false" 34 | } 35 | test("shortName") { 36 | // -f=bar syntax does work for short names 37 | Parser(Main).runEither(Array("-f=bar")) ==> 38 | Right("barbar false") 39 | } 40 | test("notFlags") { 41 | // -f=bar syntax doesn't work for flags 42 | Parser(Main).runEither(Array("--foo=bar", "--bool=true")) ==> 43 | Left("""Unknown argument: "--bool=true" 44 | |Expected Signature: run 45 | | --bool Example flag 46 | | -f --foo String to print repeatedly 47 | | --my-num How many times to print string 48 | | 49 | |""".stripMargin) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mainargs/test/src/PositionalTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object PositionalTests extends TestSuite { 5 | 6 | object Base { 7 | @main 8 | def positional(x: Boolean, @arg(positional = true) y: Boolean, z: Boolean) = (x, y, z) 9 | } 10 | val check = new Checker(Parser(Base), allowPositional = false) 11 | 12 | val tests = Tests { 13 | test - check( 14 | List("true", "true", "true"), 15 | Result.Failure.MismatchedArguments( 16 | Vector( 17 | ArgSig( 18 | None, 19 | Some('x'), 20 | None, 21 | None, 22 | TokensReader.BooleanRead, 23 | positional = false, 24 | hidden = false 25 | ), 26 | ArgSig( 27 | None, 28 | Some('z'), 29 | None, 30 | None, 31 | TokensReader.BooleanRead, 32 | positional = false, 33 | hidden = false 34 | ) 35 | ), 36 | List("true", "true"), 37 | List(), 38 | None 39 | ) 40 | ) 41 | test - check( 42 | List("-x", "true", "false", "-z", "false"), 43 | Result.Success((true, false, false)) 44 | ) 45 | test - check( 46 | List("-x", "true", "-y", "false", "-z", "false"), 47 | Result.Failure.MismatchedArguments( 48 | List( 49 | ArgSig( 50 | None, 51 | Some('y'), 52 | None, 53 | None, 54 | TokensReader.BooleanRead, 55 | positional = true, 56 | hidden = false 57 | ), 58 | ArgSig( 59 | None, 60 | Some('z'), 61 | None, 62 | None, 63 | TokensReader.BooleanRead, 64 | positional = false, 65 | hidden = false 66 | ) 67 | ), 68 | List("-y", "false", "-z", "false"), 69 | List(), 70 | None 71 | ) 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mainargs/test/src/OptionSeqTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object OptionSeqTests extends TestSuite { 5 | object Main { 6 | @main 7 | def runOpt(opt: Option[Int]) = opt 8 | 9 | @main 10 | def runSeq(seq: Seq[Int]) = seq 11 | 12 | @main 13 | def runVec(seq: Vector[Int]) = seq 14 | 15 | @main 16 | def runMap(map: Map[String, String]) = map 17 | 18 | @main 19 | def runInt(int: Int) = int 20 | } 21 | 22 | val tests = Tests { 23 | test("opt") { 24 | test { 25 | Parser(Main).runOrThrow(Array("runOpt")) ==> 26 | None 27 | } 28 | test { 29 | Parser(Main).runOrThrow(Array("runOpt", "--opt", "123")) ==> 30 | Some(123) 31 | } 32 | } 33 | test("seq") { 34 | 35 | test { 36 | Parser(Main).runOrThrow(Array("runSeq")) ==> 37 | Seq() 38 | } 39 | test { 40 | Parser(Main).runOrThrow(Array("runSeq", "--seq", "123")) ==> 41 | Seq(123) 42 | } 43 | } 44 | test("vec") { 45 | Parser(Main).runOrThrow(Array("runVec", "--seq", "123", "--seq", "456")) ==> 46 | Vector(123, 456) 47 | } 48 | 49 | test("map") { 50 | 51 | test { 52 | Parser(Main).runOrThrow(Array("runMap")) ==> 53 | Map() 54 | } 55 | test { 56 | Parser(Main).runOrThrow(Array("runMap", "--map", "abc=123")) ==> 57 | Map("abc" -> "123") 58 | } 59 | test { 60 | Parser(Main).runOrThrow( 61 | Array("runMap", "--map", "abc=123", "--map", "def=456") 62 | ) ==> 63 | Map("abc" -> "123", "def" -> "456") 64 | } 65 | } 66 | test("allowRepeats") { 67 | test("true") { 68 | Parser(Main) 69 | .runOrThrow(Array("runInt", "--int", "123", "--int", "456"), allowRepeats = true) ==> 70 | 456 71 | } 72 | test("false") { 73 | intercept[Exception] { 74 | Parser(Main) 75 | .runOrThrow(Array("runInt", "--int", "123", "--int", "456"), allowRepeats = false) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mainargs/src/Util.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import scala.annotation.{switch, tailrec} 4 | 5 | object Util { 6 | def nullNameMapper(s: String): Option[String] = None 7 | 8 | def kebabCaseNameMapper(s: String): Option[String] = { 9 | baseNameMapper(s, '-') 10 | } 11 | def snakeCaseNameMapper(s: String): Option[String] = { 12 | baseNameMapper(s, '_') 13 | } 14 | 15 | def baseNameMapper(s: String, sep: Char): Option[String] = { 16 | val chars = new collection.mutable.StringBuilder 17 | // 'D' -> digit 18 | // 'U' -> uppercase 19 | // 'L' -> lowercase 20 | // 'O' -> other 21 | var state = ' ' 22 | 23 | for (c <- s) { 24 | if (c.isDigit){ 25 | if (state == 'L' || state == 'U') chars.append(sep) 26 | chars.append(c) 27 | state = 'D' 28 | } else if (c.isUpper) { 29 | if (state == 'L' || state == 'D') chars.append(sep) 30 | chars.append(c.toLower) 31 | state = 'U' 32 | } else if (c.isLower){ 33 | chars.append(c) 34 | state = 'L' 35 | } else { 36 | state = 'O' 37 | chars.append(c) 38 | } 39 | } 40 | 41 | Some(chars.toString()) 42 | } 43 | 44 | def literalize(s: IndexedSeq[Char], unicode: Boolean = false) = { 45 | val sb = new StringBuilder 46 | sb.append('"') 47 | var i = 0 48 | val len = s.length 49 | while (i < len) { 50 | (s(i): @switch) match { 51 | case '"' => sb.append("\\\"") 52 | case '\\' => sb.append("\\\\") 53 | case '\b' => sb.append("\\b") 54 | case '\f' => sb.append("\\f") 55 | case '\n' => sb.append("\\n") 56 | case '\r' => sb.append("\\r") 57 | case '\t' => sb.append("\\t") 58 | case c => 59 | if (c < ' ' || (c > '~' && unicode)) sb.append("\\u%04x" format c.toInt) 60 | else sb.append(c) 61 | } 62 | i += 1 63 | } 64 | sb.append('"') 65 | 66 | sb.result() 67 | } 68 | 69 | def stripDashes(s: String) = { 70 | if (s.startsWith("--")) s.drop(2) 71 | else if (s.startsWith("-")) s.drop(1) 72 | else s 73 | } 74 | 75 | def appendMap[K, V](current: Map[K, Vector[V]], k: K, v: V): Map[K, Vector[V]] = { 76 | if (current.contains(k)) current + (k -> (current(k) :+ v)) 77 | else current + (k -> Vector(v)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /mainargs/test/src/ManyTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object ManyTests extends TestSuite { 5 | @main 6 | case class Config( 7 | a: String, 8 | b: Int, 9 | c: Boolean, 10 | d: String, 11 | e: Int, 12 | f: Boolean, 13 | g: String, 14 | h: Int, 15 | i: Boolean, 16 | j: String, 17 | k: Int, 18 | l: Boolean, 19 | m: Double, 20 | n: BigDecimal 21 | ) 22 | 23 | val parser = Parser[Config] 24 | val tests = Tests { 25 | test { 26 | parser.constructEither( 27 | Array( 28 | "--a", 29 | "A", 30 | "--b", 31 | "1", 32 | "--c", 33 | "true", 34 | "--d", 35 | "D", 36 | "--e", 37 | "2", 38 | "--f", 39 | "false", 40 | "--g", 41 | "G", 42 | "--h", 43 | "3", 44 | "--i", 45 | "true", 46 | "--j", 47 | "J", 48 | "--k", 49 | "4", 50 | "--l", 51 | "false", 52 | "--m", 53 | "5.50", 54 | "--n", 55 | "12345678901234567890.12345678901234567890" 56 | ), 57 | allowPositional = true 58 | ) 59 | } 60 | test { 61 | parser.constructEither( 62 | Array( 63 | "A", 64 | "--b", 65 | "1", 66 | "--c", 67 | "true", 68 | "D", 69 | "--e", 70 | "2", 71 | "--f", 72 | "false", 73 | "G", 74 | "--h", 75 | "3", 76 | "--i", 77 | "true", 78 | "J", 79 | "--k", 80 | "4", 81 | "--l", 82 | "false", 83 | "--m", 84 | "5.50", 85 | "--n", 86 | "12345678901234567890.12345678901234567890" 87 | ), 88 | allowPositional = true 89 | ) 90 | } 91 | test { 92 | parser.constructEither( 93 | Array( 94 | "A", 95 | "1", 96 | "--c", 97 | "true", 98 | "D", 99 | "2", 100 | "--f", 101 | "false", 102 | "G", 103 | "3", 104 | "--i", 105 | "true", 106 | "J", 107 | "4", 108 | "--l", 109 | "false" 110 | ), 111 | allowPositional = true 112 | ) 113 | } 114 | test { 115 | parser.constructEither( 116 | Array( 117 | "A", 118 | "1", 119 | "true", 120 | "D", 121 | "2", 122 | "false", 123 | "G", 124 | "3", 125 | "true", 126 | "J", 127 | "4", 128 | "false", 129 | "5.50", 130 | "12345678901234567890.12345678901234567890" 131 | ), 132 | allowPositional = true 133 | ) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /mainargs/src/Result.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | /** 4 | * Represents what comes out of an attempt to invoke an [[Main]]. 5 | * Could succeed with a value, but could fail in many different ways. 6 | */ 7 | sealed trait Result[+T] { 8 | def map[V](f: T => V): Result[V] = this match { 9 | case Result.Success(v) => Result.Success(f(v)) 10 | case e: Result.Failure => e 11 | } 12 | def flatMap[V](f: T => Result[V]): Result[V] = this match { 13 | case Result.Success(v) => f(v) 14 | case e: Result.Failure => e 15 | } 16 | } 17 | object Result { 18 | 19 | /** 20 | * Invoking the [[Main]] was totally successful, and returned a 21 | * result 22 | */ 23 | case class Success[T](value: T) extends Result[T] 24 | 25 | /** 26 | * Invoking the [[Main]] was not successful 27 | */ 28 | sealed trait Failure extends Result[Nothing] 29 | object Failure { 30 | sealed trait Early extends Failure 31 | object Early { 32 | 33 | case class NoMainMethodsDetected() extends Early 34 | case class SubcommandNotSpecified(options: Seq[String]) extends Early 35 | case class UnableToFindSubcommand(options: Seq[String], token: String) extends Early 36 | case class SubcommandSelectionDashes(token: String) extends Early 37 | } 38 | 39 | /** 40 | * Invoking the [[Main]] failed with an exception while executing 41 | * code within it. 42 | */ 43 | case class Exception(t: Throwable) extends Failure 44 | 45 | /** 46 | * Invoking the [[Main]] failed because the arguments provided 47 | * did not line up with the arguments expected 48 | */ 49 | case class MismatchedArguments( 50 | missing: Seq[ArgSig] = Nil, 51 | unknown: Seq[String] = Nil, 52 | duplicate: Seq[(ArgSig, Seq[String])] = Nil, 53 | incomplete: Option[ArgSig] = None 54 | ) extends Failure 55 | 56 | /** 57 | * Invoking the [[Main]] failed because there were problems 58 | * deserializing/parsing individual arguments 59 | */ 60 | case class InvalidArguments(values: Seq[ParamError]) extends Failure 61 | } 62 | 63 | sealed trait ParamError 64 | object ParamError { 65 | 66 | /** 67 | * Something went wrong trying to de-serialize the input parameter 68 | */ 69 | case class Failed(arg: ArgSig, tokens: Seq[String], errMsg: String) 70 | extends ParamError 71 | 72 | /** 73 | * Something went wrong trying to de-serialize the input parameter; 74 | * the thrown exception is stored in [[ex]] 75 | */ 76 | case class Exception(arg: ArgSig, tokens: Seq[String], ex: Throwable) 77 | extends ParamError 78 | 79 | /** 80 | * Something went wrong trying to evaluate the default value 81 | * for this input parameter 82 | */ 83 | case class DefaultFailed(arg: ArgSig, ex: Throwable) extends ParamError 84 | } 85 | } 86 | 87 | sealed trait ParamResult[+T] { 88 | def map[V](f: T => V): ParamResult[V] = this match { 89 | case ParamResult.Success(v) => ParamResult.Success(f(v)) 90 | case e: ParamResult.Failure => e 91 | } 92 | def flatMap[V](f: T => ParamResult[V]): ParamResult[V] = this match { 93 | case ParamResult.Success(v) => f(v) 94 | case e: ParamResult.Failure => e 95 | } 96 | } 97 | object ParamResult { 98 | case class Failure(errors: Seq[Result.ParamError]) extends ParamResult[Nothing] 99 | case class Success[T](value: T) extends ParamResult[T] 100 | } 101 | -------------------------------------------------------------------------------- /mainargs/test/src/FlagTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object FlagTests extends TestSuite { 5 | 6 | object Base { 7 | @main 8 | def bool(a: Flag, b: Boolean, c: Flag) = Seq(a.value, b, c.value) 9 | @main 10 | def str(a: Flag, b: String) = Seq(a.value, b) 11 | } 12 | 13 | val check = new Checker(Parser(Base), allowPositional = true) 14 | 15 | val tests = Tests { 16 | test - check( 17 | List("bool", "-b", "true"), 18 | Result.Success(Seq(false, true, false)) 19 | ) 20 | test - check( 21 | List("bool", "-b", "false"), 22 | Result.Success(Seq(false, false, false)) 23 | ) 24 | 25 | test - check( 26 | List("bool", "-a", "-b", "false"), 27 | Result.Success(Seq(true, false, false)) 28 | ) 29 | 30 | test - check( 31 | List("bool", "-c", "-b", "false"), 32 | Result.Success(Seq(false, false, true)) 33 | ) 34 | 35 | test - check( 36 | List("bool", "-a", "-c", "-b", "false"), 37 | Result.Success(Seq(true, false, true)) 38 | ) 39 | 40 | test("combined"){ 41 | test - check( 42 | List("bool", "-bfalse"), 43 | Result.Success(List(false, false, false)) 44 | ) 45 | test - check( 46 | List("bool", "-btrue"), 47 | Result.Success(List(false, true, false)) 48 | ) 49 | 50 | test - check( 51 | List("bool", "-abtrue"), 52 | Result.Success(List(true, true, false)) 53 | ) 54 | test - check( 55 | List("bool", "-abfalse"), 56 | Result.Success(List(true, false, false)) 57 | ) 58 | 59 | test - check( 60 | List("bool", "-a", "-btrue"), 61 | Result.Success(List(true, true, false)) 62 | ) 63 | 64 | test - check( 65 | List("bool", "-a", "-bfalse"), 66 | Result.Success(List(true, false, false)) 67 | ) 68 | 69 | test - check( 70 | List("bool", "-acbtrue"), 71 | Result.Success(List(true, true, true)) 72 | ) 73 | 74 | test - check( 75 | List("bool", "-acbfalse"), 76 | Result.Success(List(true, false, true)) 77 | ) 78 | 79 | test - check( 80 | List("bool", "-a", "-c", "-btrue"), 81 | Result.Success(List(true, true, true)) 82 | ) 83 | 84 | test - check( 85 | List("bool", "-a", "-c", "-bfalse"), 86 | Result.Success(List(true, false, true)) 87 | ) 88 | 89 | test - check( 90 | List("bool", "-a", "-btrue", "-c"), 91 | Result.Success(List(true, true, true)) 92 | ) 93 | 94 | test - check( 95 | List("bool", "-a", "-bfalse", "-c"), 96 | Result.Success(List(true, false, true)) 97 | ) 98 | 99 | test - check( 100 | List("bool", "-ba"), 101 | Result.Failure.InvalidArguments( 102 | List( 103 | Result.ParamError.Failed( 104 | new ArgSig(None, Some('b'), None, None, mainargs.TokensReader.BooleanRead, false, false), 105 | Vector("a"), 106 | "java.lang.IllegalArgumentException: For input string: \"a\"" 107 | ) 108 | ) 109 | ) 110 | ) 111 | 112 | test - check( 113 | List("bool", "-ab"), 114 | Result.Failure.MismatchedArguments( 115 | Nil, 116 | Nil, 117 | Nil, 118 | Some(new ArgSig(None, Some('b'), None, None, TokensReader.BooleanRead, false, false)) 119 | ) 120 | ) 121 | 122 | test - check(List("str", "-b=value", "-a"), Result.Success(List(true, "value"))) 123 | test - check(List("str", "-b=", "-a"), Result.Success(List(true, ""))) 124 | 125 | test - check(List("str", "-ab=value"), Result.Success(List(true, "value"))) 126 | 127 | test - check( 128 | List("str", "-bvalue", "-akey=value"), 129 | Result.Failure.MismatchedArguments(Nil, List("-akey=value"), Nil, None) 130 | ) 131 | } 132 | 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /mainargs/test/src/DashedArgumentName.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object DashedArgumentName extends TestSuite { 5 | 6 | object Base { 7 | @main 8 | def `opt-for-18+name`(`opt-for-18+arg`: Boolean) = `opt-for-18+arg` 9 | 10 | @main 11 | def `opt-for-29+name`(`opt-for-29+arg`: Boolean) = `opt-for-29+arg` 12 | 13 | @main 14 | def camelOptFor29Name(camelOptFor29Arg: Boolean) = camelOptFor29Arg 15 | 16 | @main(name = "camelOptFor29NameForce") 17 | def camelOptFor29NameForce(@arg(name = "camelOptFor29ArgForce") camelOptFor29ArgForce: Boolean) = camelOptFor29ArgForce 18 | } 19 | val check = new Checker(Parser(Base), allowPositional = true) 20 | val snakeCaseCheck = new Checker(Parser(Base), allowPositional = true, nameMapper = Util.snakeCaseNameMapper) 21 | 22 | val tests = Tests { 23 | test("backticked") { 24 | test - check( 25 | List("opt-for-18+name", "--opt-for-18+arg", "true"), 26 | Result.Success(true) 27 | ) 28 | test - check( 29 | List("opt-for-18+name", "--opt-for-18+arg", "false"), 30 | Result.Success(false) 31 | ) 32 | test - check( 33 | List("opt-for-29+name", "--opt-for-29+arg", "true"), 34 | Result.Success(true) 35 | ) 36 | test - check( 37 | List("opt-for-29+name", "--opt-for-29+arg", "false"), 38 | Result.Success(false) 39 | ) 40 | } 41 | test("camelKebabNameMapped") { 42 | test("mapped") - check( 43 | List("camel-opt-for-29-name", "--camel-opt-for-29-arg", "false"), 44 | Result.Success(false) 45 | ) 46 | 47 | // Make sure we continue to support un-mapped names for backwards compatibility 48 | test("backwardsCompatUnmapped") - check( 49 | List("camelOptFor29Name", "--camelOptFor29Arg", "false"), 50 | Result.Success(false) 51 | ) 52 | 53 | test("explicitNameUnmapped") - check( 54 | List("camelOptFor29NameForce", "--camelOptFor29ArgForce", "false"), 55 | Result.Success(false) 56 | ) 57 | 58 | // For names given explicitly via `main(name = ...)` or `arg(name = ...)`, we 59 | // do not use a name mapper, since we assume the user would provide the exact 60 | // name they want. 61 | test("explicitMainNameMappedFails") - check( 62 | List("camel-opt-for-29-name-force", "--camel-opt-for-29-arg-force", "false"), 63 | Result.Failure.Early.UnableToFindSubcommand( 64 | List("opt-for-18+name", "opt-for-29+name", "camel-opt-for-29-name", "camelOptFor29NameForce"), 65 | "camel-opt-for-29-name-force" 66 | ) 67 | ) 68 | test("explicitArgNameMappedFails") - check( 69 | List("camelOptFor29NameForce", "--camel-opt-for-29-arg-force", "false"), 70 | Result.Failure.MismatchedArguments( 71 | Vector( 72 | new ArgSig( 73 | Some("camelOptFor29ArgForce"), 74 | Some("camelOptFor29ArgForce"), 75 | None, 76 | None, 77 | None, 78 | mainargs.TokensReader.BooleanRead, 79 | positional = false, 80 | hidden = false 81 | ) 82 | ), 83 | List("--camel-opt-for-29-arg-force", "false"), 84 | List(), 85 | None 86 | ) 87 | 88 | ) 89 | } 90 | test("camelSnakeNameMapped") { 91 | test("mapped") - snakeCaseCheck( 92 | List("camel_opt_for_29_name", "--camel_opt_for_29_arg", "false"), 93 | Result.Success(false) 94 | ) 95 | 96 | // Make sure we continue to support un-mapped names for backwards compatibility 97 | test("backwardsCompatUnmapped") - check( 98 | List("camelOptFor29Name", "--camelOptFor29Arg", "false"), 99 | Result.Success(false) 100 | ) 101 | 102 | test("explicitNameUnmapped") - check( 103 | List("camelOptFor29NameForce", "--camelOptFor29ArgForce", "false"), 104 | Result.Success(false) 105 | ) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /mainargs/test/src/VarargsBaseTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | trait VarargsBaseTests extends TestSuite { 5 | def check: Checker[_] 6 | def isNewVarargsTests: Boolean 7 | val tests = Tests { 8 | 9 | test("happyPathPasses") { 10 | test - check( 11 | List("pureVariadic", "1", "2", "3"), 12 | Result.Success(6) 13 | ) 14 | test - check( 15 | List("mixedVariadic", "1", "2", "3", "4", "5"), 16 | Result.Success("12345") 17 | ) 18 | test - { 19 | if (isNewVarargsTests) 20 | check( 21 | List("mixedVariadicWithDefault"), 22 | Result.Success("1337") 23 | ) 24 | } 25 | } 26 | test("emptyVarargsPasses") { 27 | test - check(List("pureVariadic"), Result.Success(0)) 28 | test - check( 29 | List("mixedVariadic", "-f", "1"), 30 | Result.Success("1") 31 | ) 32 | test - check( 33 | List("mixedVariadic", "1"), 34 | Result.Success("1") 35 | ) 36 | } 37 | test("varargsAreAlwaysPositional") { 38 | val invoked = check.parseInvoke( 39 | List("pureVariadic", "--nums", "31337") 40 | ) 41 | test - assertMatch(invoked) { 42 | case Result.Failure.InvalidArguments( 43 | List( 44 | Result.ParamError.Failed( 45 | ArgSig(Some("nums"), _, _, _, _, _, _), 46 | Seq("--nums", "31337"), 47 | """java.lang.NumberFormatException: For input string: "--nums"""" | 48 | """java.lang.NumberFormatException: --nums""" 49 | ) 50 | ) 51 | ) => 52 | } 53 | 54 | test - assertMatch( 55 | check.parseInvoke(List("pureVariadic", "1", "2", "3", "--nums", "4")) 56 | ) { 57 | case Result.Failure.InvalidArguments( 58 | List( 59 | Result.ParamError.Failed( 60 | ArgSig(Some("nums"), _, _, _, _, _, _), 61 | Seq("1", "2", "3", "--nums", "4"), 62 | "java.lang.NumberFormatException: For input string: \"--nums\"" | 63 | "java.lang.NumberFormatException: --nums" 64 | ) 65 | ) 66 | ) => 67 | } 68 | test - check( 69 | List("mixedVariadic", "1", "--args", "foo"), 70 | Result.Success("1--argsfoo") 71 | ) 72 | 73 | } 74 | 75 | test("notEnoughNormalArgsStillFails") { 76 | assertMatch(check.parseInvoke(List("mixedVariadic"))) { 77 | case Result.Failure.MismatchedArguments( 78 | Seq(ArgSig(Some("first"), _, _, _, _, _, _)), 79 | Nil, 80 | Nil, 81 | None 82 | ) => 83 | } 84 | } 85 | test("multipleVarargParseFailures") { 86 | test - assertMatch( 87 | check.parseInvoke(List("pureVariadic", "aa", "bb", "3")) 88 | ) { 89 | case Result.Failure.InvalidArguments( 90 | List( 91 | Result.ParamError.Failed( 92 | ArgSig(Some("nums"), _, _, _, _, _, _), 93 | Seq("aa", "bb", "3"), 94 | "java.lang.NumberFormatException: For input string: \"aa\"" | 95 | "java.lang.NumberFormatException: aa" 96 | ) 97 | ) 98 | ) => 99 | } 100 | 101 | test - assertMatch( 102 | check.parseInvoke(List("mixedVariadic", "aa", "bb", "3")) 103 | ) { 104 | case Result.Failure.InvalidArguments( 105 | List( 106 | Result.ParamError.Failed( 107 | ArgSig(Some("first"), _, _, _, _, _, _), 108 | Seq("aa"), 109 | "java.lang.NumberFormatException: For input string: \"aa\"" | 110 | "java.lang.NumberFormatException: aa" 111 | ) 112 | ) 113 | ) => 114 | } 115 | } 116 | 117 | test("failedCombinedShortArgsGoToLeftover"){ 118 | test - check( 119 | List("mixedVariadic", "-f", "123", "abc", "xyz"), 120 | Result.Success("123abcxyz") 121 | ) 122 | test - check( 123 | List("mixedVariadic", "-f123", "456", "abc", "xyz"), 124 | Result.Success("123456abcxyz") 125 | ) 126 | test - check( 127 | List("mixedVariadic", "-f123", "-unknown", "456", "abc", "xyz"), 128 | Result.Success("123-unknown456abcxyz") 129 | ) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /mainargs/test/src-jvm-2/MillTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import utest._ 4 | 5 | object MillTests extends TestSuite { 6 | implicit object PathRead extends TokensReader.Simple[os.Path] { 7 | def shortName = "path" 8 | def read(strs: Seq[String]) = Right(os.Path(strs.head, os.pwd)) 9 | } 10 | 11 | @main( 12 | name = "Mill Build Tool", 13 | doc = "usage: mill [mill-options] [target [target-options]]" 14 | ) 15 | case class Config( 16 | @arg( 17 | doc = 18 | "Run Mill in interactive mode and start a build REPL. In this mode, no mill server will be used. Must be the first argument." 19 | ) 20 | repl: Flag = Flag(), 21 | @arg( 22 | name = "no-server", 23 | doc = 24 | "Run Mill in interactive mode, suitable for opening REPLs and taking user input. In this mode, no mill server will be used. Must be the first argument." 25 | ) 26 | noServer: Flag = Flag(), 27 | @arg( 28 | short = 'i', 29 | doc = 30 | "Run Mill in interactive mode, suitable for opening REPLs and taking user input. In this mode, no mill server will be used. Must be the first argument." 31 | ) 32 | interactive: Flag = Flag(), 33 | @arg( 34 | short = 'v', 35 | doc = "Show mill version and exit." 36 | ) 37 | version: Flag = Flag(), 38 | @arg( 39 | name = "bell", 40 | short = 'b', 41 | doc = "Ring the bell once if the run completes successfully, twice if it fails." 42 | ) 43 | ringBell: Flag = Flag(), 44 | @arg( 45 | name = "disable-ticker", 46 | doc = "Disable ticker log (e.g. short-lived prints of stages and progress bars)" 47 | ) 48 | disableTicker: Flag = Flag(), 49 | @arg( 50 | short = 'd', 51 | doc = "Show debug output on STDOUT" 52 | ) 53 | debug: Flag = Flag(), 54 | @arg( 55 | name = "keep-going", 56 | short = 'k', 57 | doc = "Continue build, even after build failures" 58 | ) 59 | keepGoing: Flag = Flag(), 60 | @arg( 61 | name = "define", 62 | short = 'D', 63 | doc = "Define (or overwrite) a system property" 64 | ) 65 | extraSystemProperties: Map[String, String] = Map(), 66 | @arg( 67 | name = "jobs", 68 | short = 'j', 69 | doc = 70 | "Allow processing N targets in parallel. Use 1 to disable parallel and 0 to use as much threads as available processors." 71 | ) 72 | threadCount: Int = 1, 73 | ammoniteConfig: AmmoniteConfig.Core = AmmoniteConfig.Core( 74 | injectedConstant = AmmoniteConfig.InjectedConstant(), 75 | noDefaultPredef = Flag(), 76 | silent = Flag(), 77 | watch = Flag(), 78 | bsp = Flag(), 79 | thin = Flag(), 80 | help = Flag() 81 | ), 82 | @arg( 83 | name = "hidden-dummy", 84 | hidden = true 85 | ) 86 | hiddenDummy: String = "" 87 | ) 88 | 89 | val tests = Tests { 90 | 91 | val parser = Parser[Config] 92 | 93 | test("formatMainMethods") { 94 | val rendered = parser.helpText(sorted = false) 95 | val expected = { 96 | """Mill Build Tool 97 | |usage: mill [mill-options] [target [target-options]] 98 | | --repl Run Mill in interactive mode and start a build REPL. In this mode, no mill 99 | | server will be used. Must be the first argument. 100 | | --no-server Run Mill in interactive mode, suitable for opening REPLs and taking user 101 | | input. In this mode, no mill server will be used. Must be the first argument. 102 | | -i --interactive Run Mill in interactive mode, suitable for opening REPLs and taking user 103 | | input. In this mode, no mill server will be used. Must be the first argument. 104 | | -v --version Show mill version and exit. 105 | | -b --bell Ring the bell once if the run completes successfully, twice if it fails. 106 | | --disable-ticker Disable ticker log (e.g. short-lived prints of stages and progress bars) 107 | | -d --debug Show debug output on STDOUT 108 | | -k --keep-going Continue build, even after build failures 109 | | -D --define Define (or overwrite) a system property 110 | | -j --jobs Allow processing N targets in parallel. Use 1 to disable parallel and 0 to 111 | | use as much threads as available processors. 112 | | --no-default-predef Disable the default predef and run Ammonite with the minimal predef possible 113 | | -s --silent Make ivy logs go silent instead of printing though failures will still throw 114 | | exception 115 | | -w --watch Watch and re-run your scripts when they change 116 | | --bsp Run a BSP server against the passed scripts 117 | | -c --code Pass in code to be run immediately in the REPL 118 | | -h --home The home directory of the REPL; where it looks for config and caches 119 | | -p --predef Lets you load your predef from a custom location, rather than the default 120 | | location in your Ammonite home 121 | | --color Enable or disable colored output; by default colors are enabled in both REPL 122 | | and scripts if the console is interactive, and disabled otherwise 123 | | --thin Hide parts of the core of Ammonite and some of its dependencies. By default, 124 | | the core of Ammonite and all of its dependencies can be seen by users from 125 | | the Ammonite session. This option mitigates that via class loader isolation. 126 | | --help Print this message 127 | |""".stripMargin 128 | } 129 | assert(rendered == expected) 130 | } 131 | test("parseInvoke") { 132 | parser.constructEither(Array("--jobs", "12").toIndexedSeq) ==> 133 | Right(Config(threadCount = 12)) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /mainargs/src-2/Macros.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import scala.language.experimental.macros 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | /** 6 | * More or less a minimal version of Autowire's Server that lets you generate 7 | * a set of "routes" from the methods defined in an object, and call them 8 | * using passing in name/args/kwargs via Java reflection, without having to 9 | * generate/compile code or use Scala reflection. This saves us spinning up 10 | * the Scala compiler and greatly reduces the startup time of cached scripts. 11 | */ 12 | class Macros(val c: Context) { 13 | import c.universe._ 14 | 15 | def parserForMethods[B: c.WeakTypeTag](base: c.Expr[B]): c.Tree = { 16 | val allRoutes = getAllRoutesForClass(weakTypeOf[B]) 17 | q""" 18 | new _root_.mainargs.ParserForMethods( 19 | _root_.mainargs.MethodMains[${weakTypeOf[B]}](_root_.scala.List(..$allRoutes), () => $base) 20 | ) 21 | """ 22 | } 23 | 24 | def parserForClass[T: c.WeakTypeTag]: c.Tree = { 25 | 26 | val cls = weakTypeOf[T].typeSymbol.asClass 27 | val companionObj = weakTypeOf[T].typeSymbol.companion 28 | val constructor = cls.primaryConstructor.asMethod 29 | val route = extractMethod( 30 | TermName("apply"), 31 | constructor.paramLists.flatten, 32 | constructor.pos, 33 | cls.annotations.find(_.tpe =:= typeOf[main]), 34 | companionObj.typeSignature, 35 | weakTypeOf[T] 36 | ) 37 | 38 | q""" 39 | new _root_.mainargs.ParserForClass( 40 | $route.asInstanceOf[_root_.mainargs.MainData[${weakTypeOf[T]}, Any]], 41 | () => $companionObj 42 | ) 43 | """ 44 | } 45 | def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = { 46 | def isAMemberOfAnyRef(member: Symbol) = { 47 | // AnyRef is an alias symbol, we go to the real "owner" of these methods 48 | val anyRefSym = c.mirror.universe.definitions.ObjectClass 49 | member.owner == anyRefSym 50 | } 51 | val extractableMembers = for { 52 | member <- curCls.members.toList.reverse 53 | if !isAMemberOfAnyRef(member) 54 | if !member.isSynthetic 55 | if member.isPublic 56 | if member.isTerm 57 | memTerm = member.asTerm 58 | if memTerm.isMethod 59 | if !memTerm.isModule 60 | } yield memTerm.asMethod 61 | 62 | extractableMembers flatMap { case memTerm => 63 | if (memTerm.isSetter || memTerm.isConstructor || memTerm.isGetter) Nil 64 | else Seq(memTerm) 65 | } 66 | } 67 | 68 | def unwrapVarargType(arg: Symbol) = { 69 | val vararg = arg.typeSignature.typeSymbol == definitions.RepeatedParamClass 70 | val unwrappedType = 71 | if (!vararg) arg.typeSignature 72 | else arg.typeSignature.asInstanceOf[TypeRef].args(0) 73 | 74 | (vararg, unwrappedType) 75 | } 76 | 77 | def extractMethod( 78 | methodName: TermName, 79 | flattenedArgLists: Seq[Symbol], 80 | methodPos: Position, 81 | mainAnnotation: Option[Annotation], 82 | curCls: c.universe.Type, 83 | returnType: c.universe.Type 84 | ): c.universe.Tree = { 85 | 86 | val baseArgSym = TermName(c.freshName()) 87 | 88 | // Somehow this is necessary to make default 89 | // method discovery work on Scala 2.12.1 -> 2.12.7 90 | curCls.members.foreach(_.name) 91 | def hasDefault(i: Int) = { 92 | val defaultName = s"${methodName}$$default$$${i + 1}" 93 | if (curCls.members.exists(_.name.toString == defaultName)) Some(defaultName) 94 | else None 95 | } 96 | 97 | val argListSymbol = q"${c.fresh[TermName](TermName("argsList"))}" 98 | 99 | val defaults = for ((arg0, i) <- flattenedArgLists.zipWithIndex) yield { 100 | val arg = TermName(c.freshName()) 101 | hasDefault(i) match{ 102 | case None => q"_root_.scala.None" 103 | case Some(defaultName) => 104 | q"_root_.scala.Some[$curCls => ${arg0.info}](($arg: $curCls) => $arg.${newTermName(defaultName)}: ${arg0.info})" 105 | } 106 | } 107 | 108 | val argSigs = for ((arg, defaultOpt) <- flattenedArgLists.zip(defaults)) yield { 109 | 110 | val (vararg, varargUnwrappedType) = unwrapVarargType(arg) 111 | val argAnnotation = arg.annotations.find(_.tpe =:= typeOf[arg]) 112 | 113 | val instantiateArg = argAnnotation match { 114 | case Some(annot) => q"new ${annot.tree.tpe}(..${annot.tree.children.tail})" 115 | case _ => q"new _root_.mainargs.arg()" 116 | } 117 | val argSig = if (vararg) q""" 118 | _root_.mainargs.ArgSig.create[_root_.mainargs.Leftover[$varargUnwrappedType], $curCls]( 119 | ${arg.name.decoded}, 120 | $instantiateArg, 121 | $defaultOpt 122 | ) 123 | """ else q""" 124 | _root_.mainargs.ArgSig.create[$varargUnwrappedType, $curCls]( 125 | ${arg.name.decoded}, 126 | $instantiateArg, 127 | $defaultOpt 128 | ) 129 | """ 130 | 131 | c.internal.setPos(argSig, methodPos) 132 | argSig 133 | } 134 | 135 | val argNameCasts = flattenedArgLists.zipWithIndex.map { case (arg, i) => 136 | val (vararg, unwrappedType) = unwrapVarargType(arg) 137 | val baseTree = q"$argListSymbol($i)" 138 | if (!vararg) q"$baseTree.asInstanceOf[$unwrappedType]" 139 | else q"$baseTree.asInstanceOf[_root_.mainargs.Leftover[$unwrappedType]].value: _*" 140 | } 141 | 142 | val mainInstance = mainAnnotation match { 143 | case Some(m) => q"new ${m.tree.tpe}(..${m.tree.children.tail})" 144 | case None => q"new _root_.mainargs.main()" 145 | } 146 | 147 | val res = q"""{ 148 | _root_.mainargs.MainData.create[$returnType, $curCls]( 149 | ${methodName.decoded}, 150 | $mainInstance, 151 | _root_.scala.Seq(..$argSigs), 152 | ($baseArgSym: $curCls, $argListSymbol: _root_.scala.Seq[_root_.scala.Any]) => { 153 | $baseArgSym.$methodName(..$argNameCasts) 154 | } 155 | ) 156 | }""" 157 | // println(res) 158 | res 159 | } 160 | 161 | def hasmain(t: MethodSymbol) = t.annotations.exists(_.tpe =:= typeOf[main]) 162 | def getAllRoutesForClass( 163 | curCls: Type, 164 | pred: MethodSymbol => Boolean = hasmain 165 | ): Iterable[c.universe.Tree] = { 166 | for (t <- getValsOrMeths(curCls) if pred(t)) 167 | yield { 168 | extractMethod( 169 | t.name, 170 | t.paramss.flatten, 171 | t.pos, 172 | t.annotations.find(_.tpe =:= typeOf[main]), 173 | curCls, 174 | weakTypeOf[Any] 175 | ) 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /mainargs/test/src/ClassTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import utest._ 3 | 4 | object ClassTests extends TestSuite { 5 | 6 | @main 7 | case class Foo(x: Int, y: Int) 8 | 9 | @main 10 | case class Bar(w: Flag = Flag(), f: Foo, @arg(short = 'z') zzzz: String) 11 | 12 | @main 13 | case class Qux(moo: String, b: Bar) 14 | 15 | case class Cli(@arg(short = 'd') debug: Flag) 16 | 17 | @main 18 | class Compat( 19 | @arg(short = 'h') val home: String, 20 | @arg(short = 's') val silent: Flag, 21 | val leftoverArgs: Leftover[String] 22 | ) { 23 | override def equals(obj: Any): Boolean = 24 | obj match { 25 | case c: Compat => 26 | home == c.home && silent == c.silent && leftoverArgs == c.leftoverArgs 27 | case _ => false 28 | } 29 | } 30 | object Compat { 31 | def apply( 32 | home: String = "/home", 33 | silent: Flag = Flag(), 34 | leftoverArgs: Leftover[String] = Leftover() 35 | ) = new Compat(home, silent, leftoverArgs) 36 | 37 | @deprecated("bin-compat shim", "0.1.0") 38 | private[mainargs] def apply( 39 | home: String, 40 | silent: Flag, 41 | noDefaultPredef: Flag, 42 | leftoverArgs: Leftover[String] 43 | ) = new Compat(home, silent, leftoverArgs) 44 | } 45 | 46 | implicit val fooParser: ParserForClass[Foo] = Parser[Foo] 47 | implicit val barParser: ParserForClass[Bar] = Parser[Bar] 48 | implicit val quxParser: ParserForClass[Qux] = Parser[Qux] 49 | implicit val cliParser: ParserForClass[Cli] = Parser[Cli] 50 | implicit val compatParser: ParserForClass[Compat] = Parser[Compat] 51 | 52 | class PathWrap { 53 | @main 54 | case class Foo(x: Int = 23, y: Int = 47) 55 | 56 | object Main { 57 | @main 58 | def run(bar: Bar, bool: Boolean = false) = { 59 | s"${bar.w.value} ${bar.f.x} ${bar.f.y} ${bar.zzzz} $bool" 60 | } 61 | } 62 | 63 | implicit val fooParser: ParserForClass[Foo] = Parser[Foo] 64 | } 65 | 66 | object Main { 67 | @main 68 | def run(bar: Bar, bool: Boolean = false) = { 69 | s"${bar.w.value} ${bar.f.x} ${bar.f.y} ${bar.zzzz} $bool" 70 | } 71 | } 72 | 73 | val tests = Tests { 74 | test("simple") { 75 | test("success") { 76 | fooParser.constructOrThrow(Seq("-x", "1", "-y", "2")) ==> Foo(1, 2) 77 | } 78 | test("missing") { 79 | fooParser.constructRaw(Seq("-x", "1")) ==> 80 | Result.Failure.MismatchedArguments( 81 | Seq( 82 | ArgSig( 83 | None, 84 | Some('y'), 85 | None, 86 | None, 87 | mainargs.TokensReader.IntRead, 88 | positional = false, 89 | hidden = false 90 | ) 91 | ), 92 | List(), 93 | List(), 94 | None 95 | ) 96 | 97 | } 98 | } 99 | 100 | test("nested") { 101 | test("success") { 102 | barParser.constructOrThrow( 103 | Seq("-w", "-x", "1", "-y", "2", "--zzzz", "xxx") 104 | ) ==> 105 | Bar(Flag(true), Foo(1, 2), "xxx") 106 | } 107 | test("missingInner") { 108 | barParser.constructRaw(Seq("-w", "-x", "1", "-z", "xxx")) ==> 109 | Result.Failure.MismatchedArguments( 110 | Seq( 111 | ArgSig( 112 | None, 113 | Some('y'), 114 | None, 115 | None, 116 | mainargs.TokensReader.IntRead, 117 | positional = false, 118 | hidden = false 119 | ) 120 | ), 121 | List(), 122 | List(), 123 | None 124 | ) 125 | } 126 | test("missingOuter") { 127 | barParser.constructRaw(Seq("-w", "-x", "1", "-y", "2")) ==> 128 | Result.Failure.MismatchedArguments( 129 | Seq( 130 | ArgSig( 131 | Some("zzzz"), 132 | Some('z'), 133 | None, 134 | None, 135 | mainargs.TokensReader.StringRead, 136 | positional = false, 137 | hidden = false 138 | ) 139 | ), 140 | List(), 141 | List(), 142 | None 143 | ) 144 | } 145 | 146 | test("missingInnerOuter") { 147 | barParser.constructRaw(Seq("-w", "-x", "1")) ==> 148 | Result.Failure.MismatchedArguments( 149 | Seq( 150 | ArgSig( 151 | None, 152 | Some('y'), 153 | None, 154 | None, 155 | mainargs.TokensReader.IntRead, 156 | positional = false, 157 | hidden = false 158 | ), 159 | ArgSig( 160 | Some("zzzz"), 161 | Some('z'), 162 | None, 163 | None, 164 | mainargs.TokensReader.StringRead, 165 | positional = false, 166 | hidden = false 167 | ) 168 | ), 169 | List(), 170 | List(), 171 | None 172 | ) 173 | } 174 | 175 | test("failedInnerOuter") { 176 | assertMatch( 177 | barParser.constructRaw( 178 | Seq("-w", "-x", "xxx", "-y", "hohoho", "-z", "xxx") 179 | ) 180 | ) { 181 | case Result.Failure.InvalidArguments( 182 | Seq( 183 | Result.ParamError.Failed( 184 | ArgSig(None, Some('x'), None, None, _, false, _), 185 | Seq("xxx"), 186 | _ 187 | ), 188 | Result.ParamError.Failed( 189 | ArgSig(None, Some('y'), None, None, _, false, _), 190 | Seq("hohoho"), 191 | _ 192 | ) 193 | ) 194 | ) => 195 | 196 | } 197 | } 198 | } 199 | 200 | test("doubleNested") { 201 | quxParser.constructOrThrow( 202 | Seq("-w", "-x", "1", "-y", "2", "-z", "xxx", "--moo", "cow") 203 | ) ==> 204 | Qux("cow", Bar(Flag(true), Foo(1, 2), "xxx")) 205 | } 206 | test("success") { 207 | Parser(Main).runOrThrow( 208 | Seq("-x", "1", "-y", "2", "-z", "hello") 209 | ) ==> "false 1 2 hello false" 210 | } 211 | test("mill-compat") { 212 | test("apply-overload-class") { 213 | compatParser.constructOrThrow(Seq("foo")) ==> Compat( 214 | home = "/home", 215 | silent = Flag(false), 216 | leftoverArgs = Leftover("foo") 217 | ) 218 | } 219 | test("no-main-on-class") { 220 | cliParser.constructOrThrow(Seq("-d")) ==> Cli(Flag(true)) 221 | } 222 | test("path-dependent-default") { 223 | val p = new PathWrap 224 | p.fooParser.constructOrThrow(Seq()) ==> p.Foo(23, 47) 225 | } 226 | test("path-dependent-default-method") { 227 | val p = new PathWrap 228 | Parser(p.Main).runOrThrow( 229 | Seq("-x", "1", "-y", "2", "-z", "hello") 230 | ) ==> "false 1 2 hello false" 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /mainargs/src/Invoker.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | object Invoker { 4 | def construct[T]( 5 | cep: TokensReader.Class[T], 6 | args: Seq[String], 7 | allowPositional: Boolean, 8 | allowRepeats: Boolean, 9 | ): Result[T] = construct(cep, args, allowPositional, allowRepeats, Util.nullNameMapper) 10 | 11 | def construct[T]( 12 | cep: TokensReader.Class[T], 13 | args: Seq[String], 14 | allowPositional: Boolean, 15 | allowRepeats: Boolean, 16 | nameMapper: String => Option[String] 17 | ): Result[T] = { 18 | TokenGrouping 19 | .groupArgs( 20 | args, 21 | cep.main.flattenedArgSigs, 22 | allowPositional, 23 | allowRepeats, 24 | cep.main.argSigs0.exists(_.reader.isLeftover), 25 | nameMapper 26 | ) 27 | .flatMap((group: TokenGrouping[Any]) => invoke(cep.companion(), cep.main, group)) 28 | } 29 | 30 | def invoke0[T, B]( 31 | base: B, 32 | mainData: MainData[T, B], 33 | kvs: Map[ArgSig, Seq[String]], 34 | extras: Seq[String] 35 | ): Result[T] = { 36 | val readArgValues: Seq[Either[Result[Any], ParamResult[_]]] = 37 | for (a <- mainData.argSigs0) yield { 38 | a.reader match { 39 | case r: TokensReader.Flag => 40 | Right(ParamResult.Success(Flag(kvs.contains(a)).asInstanceOf[T])) 41 | case r: TokensReader.Simple[T] => Right(makeReadCall(kvs, base, a, r)) 42 | case r: TokensReader.Constant[T] => Right(r.read() match { 43 | case Left(s) => ParamResult.Failure(Seq(Result.ParamError.Failed(a, Nil, s))) 44 | case Right(v) => ParamResult.Success(v) 45 | }) 46 | case r: TokensReader.Leftover[T, _] => Right(makeReadVarargsCall(a, extras, r)) 47 | case r: TokensReader.Class[T] => 48 | Left( 49 | invoke0[T, B]( 50 | r.companion().asInstanceOf[B], 51 | r.main.asInstanceOf[MainData[T, B]], 52 | kvs, 53 | extras 54 | ) 55 | ) 56 | 57 | } 58 | } 59 | 60 | val validated = { 61 | val lefts = readArgValues 62 | .collect { 63 | case Left(Result.Failure.InvalidArguments(lefts)) => lefts 64 | case Right(ParamResult.Failure(failure)) => failure 65 | } 66 | .flatten 67 | if (lefts.nonEmpty) Result.Failure.InvalidArguments(lefts) 68 | else Result.Success( 69 | readArgValues.collect { 70 | case Left(Result.Success(x)) => x 71 | case Right(ParamResult.Success(x)) => x 72 | } 73 | ) 74 | } 75 | 76 | val res = validated.flatMap { validated => 77 | Result.Success(mainData.invokeRaw(base, validated)) 78 | } 79 | res 80 | } 81 | def invoke[T, B](target: B, main: MainData[T, B], grouping: TokenGrouping[B]): Result[T] = { 82 | try invoke0( 83 | target, 84 | main, 85 | grouping.grouped, 86 | grouping.remaining 87 | ) 88 | catch { case e: Throwable => Result.Failure.Exception(e) } 89 | } 90 | def runMains[B]( 91 | mains: MethodMains[B], 92 | args: Seq[String], 93 | allowPositional: Boolean, 94 | allowRepeats: Boolean): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = { 95 | runMains(mains, args, allowPositional, allowRepeats, Util.nullNameMapper) 96 | } 97 | def runMains[B]( 98 | mains: MethodMains[B], 99 | args: Seq[String], 100 | allowPositional: Boolean, 101 | allowRepeats: Boolean, 102 | nameMapper: String => Option[String] 103 | ): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = { 104 | def groupArgs(main: MainData[Any, B], argsList: Seq[String]) = { 105 | def invokeLocal(group: TokenGrouping[Any]) = 106 | invoke(mains.base(), main.asInstanceOf[MainData[Any, Any]], group) 107 | Right( 108 | main, 109 | TokenGrouping 110 | .groupArgs( 111 | argsList, 112 | main.flattenedArgSigs, 113 | allowPositional, 114 | allowRepeats, 115 | main.argSigs0.exists { 116 | case x: ArgSig => x.reader.isLeftover 117 | case _ => false 118 | }, 119 | nameMapper 120 | ) 121 | .flatMap(invokeLocal) 122 | ) 123 | } 124 | mains.value match { 125 | case Seq() => Left(Result.Failure.Early.NoMainMethodsDetected()) 126 | case Seq(main) => groupArgs(main, args) 127 | case multiple => 128 | args.toList match { 129 | case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name(nameMapper)))) 130 | case head :: tail => 131 | if (head.startsWith("-")) { 132 | Left(Result.Failure.Early.SubcommandSelectionDashes(head)) 133 | } else { 134 | multiple.find{ m => 135 | val name = m.name(nameMapper) 136 | name == head || (m.mainName.isEmpty && m.defaultName == head) 137 | } match { 138 | case None => Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name(nameMapper)), head)) 139 | case Some(main) => groupArgs(main, tail) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | def tryEither[T](t: => T, error: Throwable => Result.ParamError): Either[Result.ParamError, T] = { 147 | try Right(t) 148 | catch { case e: Throwable => Left(error(e)) } 149 | } 150 | def makeReadCall[T]( 151 | dict: Map[ArgSig, Seq[String]], 152 | base: Any, 153 | arg: ArgSig, 154 | reader: TokensReader.Simple[_] 155 | ): ParamResult[T] = { 156 | def prioritizedDefault = tryEither( 157 | arg.default.map(_(base)), 158 | Result.ParamError.DefaultFailed(arg, _) 159 | ) match { 160 | case Left(ex) => ParamResult.Failure(Seq(ex)) 161 | case Right(v) => ParamResult.Success(v) 162 | } 163 | val tokens = dict.get(arg) match { 164 | case None => if (reader.allowEmpty) Some(Nil) else None 165 | case Some(tokens) => Some(tokens) 166 | } 167 | val optResult = tokens match { 168 | case None => prioritizedDefault 169 | case Some(tokens) => 170 | tryEither( 171 | reader.read(tokens), 172 | Result.ParamError.Exception(arg, tokens, _) 173 | ) match { 174 | case Left(ex) => ParamResult.Failure(Seq(ex)) 175 | case Right(Left(errMsg)) => 176 | ParamResult.Failure(Seq(Result.ParamError.Failed(arg, tokens, errMsg))) 177 | case Right(Right(v)) => ParamResult.Success(Some(v)) 178 | } 179 | } 180 | optResult.map(_.get.asInstanceOf[T]) 181 | } 182 | 183 | def makeReadVarargsCall[T]( 184 | arg: ArgSig, 185 | values: Seq[String], 186 | reader: TokensReader.Leftover[_, _] 187 | ): ParamResult[T] = { 188 | val eithers = 189 | tryEither( 190 | reader.read(values), 191 | Result.ParamError.Exception(arg, values, _) 192 | ) match { 193 | case Left(x) => Left(x) 194 | case Right(Left(errMsg)) => Left(Result.ParamError.Failed(arg, values, errMsg)) 195 | case Right(Right(v)) => Right(v) 196 | } 197 | 198 | eithers match { 199 | case Left(s) => ParamResult.Failure(Seq(s)) 200 | case Right(v) => ParamResult.Success(v.asInstanceOf[T]) 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /mainargs/src/TokenGrouping.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import scala.annotation.tailrec 4 | 5 | case class TokenGrouping[B](remaining: List[String], grouped: Map[ArgSig, Seq[String]]) 6 | 7 | object TokenGrouping { 8 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 9 | def groupArgs[B]( 10 | flatArgs0: Seq[String], 11 | argSigs: Seq[(ArgSig, TokensReader.Terminal[_])], 12 | allowPositional: Boolean, 13 | allowRepeats: Boolean, 14 | allowLeftover: Boolean, 15 | ): Result[TokenGrouping[B]] = { 16 | groupArgs(flatArgs0, argSigs, allowPositional, allowRepeats, allowLeftover, _ => None) 17 | } 18 | 19 | def groupArgs[B]( 20 | flatArgs0: Seq[String], 21 | argSigs: Seq[(ArgSig, TokensReader.Terminal[_])], 22 | allowPositional: Boolean, 23 | allowRepeats: Boolean, 24 | allowLeftover: Boolean, 25 | nameMapper: String => Option[String] 26 | ): Result[TokenGrouping[B]] = { 27 | val positionalArgSigs: Seq[ArgSig] = argSigs.collect { 28 | case (a, r: TokensReader.Simple[_]) if allowPositional | a.positional => 29 | a 30 | } 31 | 32 | val flatArgs = flatArgs0.toList 33 | def makeKeywordArgMap(getNames: ArgSig => Iterable[String]): Map[String, ArgSig] = argSigs 34 | .collect { 35 | case (a, r: TokensReader.Simple[_]) if !a.positional => a 36 | case (a, r: TokensReader.Flag) => a 37 | } 38 | .flatMap { x => getNames(x).map(_ -> x) } 39 | .toMap[String, ArgSig] 40 | 41 | lazy val shortArgMap: Map[Char, ArgSig] = argSigs 42 | .collect{case (a, _) if !a.positional => a.shortName.map(_ -> a)} 43 | .flatten 44 | .toMap[Char, ArgSig] 45 | 46 | lazy val shortFlagArgMap: Map[Char, ArgSig] = argSigs 47 | .collect{case (a, r: TokensReader.Flag) if !a.positional => a.shortName.map(_ -> a)} 48 | .flatten 49 | .toMap[Char, ArgSig] 50 | 51 | lazy val longKeywordArgMap = makeKeywordArgMap(x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.longName(Util.nullNameMapper).map("--" + _)) 52 | 53 | def parseCombinedShortTokens(current: Map[ArgSig, Vector[String]], 54 | head: String, 55 | rest: List[String]) = { 56 | val chars = head.drop(1) 57 | var rest2 = rest 58 | var i = 0 59 | var currentMap = current 60 | var failure = false 61 | var incomplete: Option[ArgSig] = None 62 | 63 | while (i < chars.length) { 64 | val c = chars(i) 65 | shortFlagArgMap.get(c) match { 66 | case Some(a) => 67 | // For `Flag`s in chars, we consume the char, set it to `true`, and continue 68 | currentMap = Util.appendMap(currentMap, a, "") 69 | i += 1 70 | case None => 71 | // For other kinds of short arguments, we consume the char, set the value to 72 | // the remaining characters, and exit 73 | shortArgMap.get(c) match { 74 | case Some(a) => 75 | if (i < chars.length - 1) { 76 | currentMap = Util.appendMap(currentMap, a, chars.drop(i + 1).stripPrefix("=")) 77 | } else { 78 | // If the non-flag argument is the last in the combined token, we look 79 | // ahead to grab the next token and assign it as this argument's value 80 | rest2 match { 81 | case Nil => 82 | // If there is no next token, it is an error 83 | incomplete = Some(a) 84 | failure = true 85 | case next :: remaining => 86 | currentMap = Util.appendMap(currentMap, a, next) 87 | rest2 = remaining 88 | } 89 | } 90 | case None => 91 | // If we encounter a character that is neither a short flag or a 92 | // short argument, it is an error 93 | failure = true 94 | } 95 | i = chars.length 96 | } 97 | 98 | } 99 | 100 | if (failure) Left(incomplete) else Right((rest2, currentMap)) 101 | } 102 | 103 | def lookupArgMap(k: String, m: Map[String, ArgSig]): Option[(ArgSig, mainargs.TokensReader[_])] = { 104 | m.get(k).map(a => (a, a.reader)) 105 | } 106 | 107 | @tailrec def rec( 108 | remaining: List[String], 109 | current: Map[ArgSig, Vector[String]] 110 | ): Result[TokenGrouping[B]] = { 111 | remaining match { 112 | case head :: rest => 113 | // special handling for combined short args of the style "-xvf" or "-j10" 114 | if (head.startsWith("-") && head.lift(1).exists(c => c != '-')){ 115 | parseCombinedShortTokens(current, head, rest) match{ 116 | case Left(Some(incompleteArg)) => 117 | Result.Failure.MismatchedArguments(Nil, Nil, Nil, incomplete = Some(incompleteArg)) 118 | case Left(None) => complete(remaining, current) 119 | case Right((rest2, currentMap)) => rec(rest2, currentMap) 120 | } 121 | 122 | } else if (head.startsWith("-") && head.exists(_ != '-')) { 123 | head.split("=", 2) match{ 124 | case Array(first, second) => 125 | lookupArgMap(first, longKeywordArgMap) match { 126 | case Some((cliArg, _: TokensReader.Simple[_])) => 127 | rec(rest, Util.appendMap(current, cliArg, second)) 128 | 129 | case _ => complete(remaining, current) 130 | } 131 | 132 | case _ => 133 | lookupArgMap(head, longKeywordArgMap) match { 134 | case Some((cliArg, _: TokensReader.Flag)) => 135 | rec(rest, Util.appendMap(current, cliArg, "")) 136 | case Some((cliArg, _: TokensReader.Simple[_])) => 137 | rest match { 138 | case next :: rest2 => rec(rest2, Util.appendMap(current, cliArg, next)) 139 | case Nil => 140 | Result.Failure.MismatchedArguments(Nil, Nil, Nil, incomplete = Some(cliArg)) 141 | } 142 | case _ => complete(remaining, current) 143 | } 144 | } 145 | } else { 146 | positionalArgSigs.find(!current.contains(_)) match { 147 | case Some(nextInLine) => rec(rest, Util.appendMap(current, nextInLine, head)) 148 | case None => complete(remaining, current) 149 | } 150 | } 151 | 152 | case _ => complete(remaining, current) 153 | } 154 | } 155 | 156 | def complete( 157 | remaining: List[String], 158 | current: Map[ArgSig, Vector[String]] 159 | ): Result[TokenGrouping[B]] = { 160 | 161 | val duplicates = current 162 | .filter { 163 | case (a: ArgSig, vs) => 164 | a.reader match { 165 | case r: TokensReader.Flag => vs.size > 1 && !allowRepeats 166 | case r: TokensReader.Simple[_] => vs.size > 1 && !r.alwaysRepeatable && !allowRepeats 167 | case r: TokensReader.Leftover[_, _] => false 168 | case r: TokensReader.Constant[_] => false 169 | } 170 | 171 | } 172 | .toSeq 173 | 174 | val missing = argSigs.collect { 175 | case (a, r: TokensReader.Simple[_]) 176 | if !r.allowEmpty 177 | && a.default.isEmpty 178 | && !current.contains(a) => 179 | a 180 | } 181 | 182 | val unknown = if (allowLeftover) Nil else remaining 183 | if (missing.nonEmpty || duplicates.nonEmpty || unknown.nonEmpty) { 184 | Result.Failure.MismatchedArguments( 185 | missing = missing, 186 | unknown = unknown, 187 | duplicate = duplicates, 188 | incomplete = None 189 | ) 190 | } else Result.Success(TokenGrouping(remaining, current)) 191 | 192 | } 193 | rec(flatArgs, Map()) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /mainargs/test/src/CoreTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import mainargs.Result.Failure.MismatchedArguments 4 | import utest._ 5 | 6 | object CoreBase { 7 | case object MyException extends Exception 8 | @main 9 | def foo() = 1 10 | @main 11 | def bar(i: Int) = i 12 | 13 | @main(doc = "Qux is a function that does stuff") 14 | def qux( 15 | i: Int, 16 | @arg(doc = "Pass in a custom `s` to override it") 17 | s: String = "lols" 18 | ) 19 | = s * i 20 | @main 21 | def baz(arg: Int) = arg 22 | 23 | @main 24 | def ex() = throw MyException 25 | 26 | def notExported(nonParseable: java.io.InputStream) = ??? 27 | 28 | val alsoNotExported = "bazzz" 29 | } 30 | 31 | object CorePositionalEnabledTests extends CoreTests(true) 32 | object CorePositionalDisabledTests extends CoreTests(false) 33 | 34 | class CoreTests(allowPositional: Boolean) extends TestSuite { 35 | val check = new Checker(ParserForMethods(CoreBase), allowPositional = allowPositional) 36 | 37 | val tests = Tests { 38 | test("formatMainMethods") { 39 | val parsed = check.parser.helpText() 40 | val expected = 41 | """Available subcommands: 42 | | 43 | | foo 44 | | 45 | | bar 46 | | -i 47 | | 48 | | qux 49 | | Qux is a function that does stuff 50 | | -i 51 | | -s Pass in a custom `s` to override it 52 | | 53 | | baz 54 | | --arg 55 | | 56 | | ex 57 | |""".stripMargin 58 | 59 | parsed ==> expected 60 | } 61 | test("basicModelling") { 62 | val names = check.mains.value.map(_.name(Util.nullNameMapper)) 63 | 64 | assert( 65 | names == 66 | List("foo", "bar", "qux", "baz", "ex") 67 | ) 68 | val evaledArgs = check.mains.value.map(_.flattenedArgSigs.map { 69 | case (ArgSig(name, s, docs, None, parser, _, _), _) => (name, s, docs, None, parser) 70 | case (ArgSig(name, s, docs, Some(default), parser, _, _), _) => 71 | (name, s, docs, Some(default(CoreBase)), parser) 72 | }) 73 | 74 | assert( 75 | evaledArgs == List( 76 | List(), 77 | List((None, Some('i'), None, None, TokensReader.IntRead)), 78 | List( 79 | (None, Some('i'), None, None, TokensReader.IntRead), 80 | ( 81 | None, 82 | Some('s'), 83 | Some("Pass in a custom `s` to override it"), 84 | Some("lols"), 85 | TokensReader.StringRead 86 | ) 87 | ), 88 | List( 89 | (Some("arg"), None, None, None, TokensReader.IntRead), 90 | ), 91 | List() 92 | ) 93 | ) 94 | } 95 | 96 | test("invoke") { 97 | test - check( 98 | List("foo"), 99 | Result.Success(1) 100 | ) 101 | test - check( 102 | List("bar", "-i", "2"), 103 | Result.Success(2) 104 | ) 105 | test - check( 106 | List("qux", "-i", "2"), 107 | Result.Success("lolslols") 108 | ) 109 | test - check( 110 | List("qux", "-i", "3", "-s", "x"), 111 | Result.Success("xxx") 112 | ) 113 | test - check( 114 | List("qux", "-i", "3", "-s", "-"), 115 | Result.Success("---") 116 | ) 117 | test - check( 118 | List("qux", "-i", "3", "-s", "--"), 119 | Result.Success("------") 120 | ) 121 | } 122 | 123 | test("failures") { 124 | test("missingParams") { 125 | test - assertMatch(check.parseInvoke(List("bar"))) { 126 | case Result.Failure.MismatchedArguments( 127 | Seq(ArgSig(None, Some('i'), _, _, _, _, _)), 128 | Nil, 129 | Nil, 130 | None 131 | ) => 132 | } 133 | test - assertMatch(check.parseInvoke(List("qux", "-s", "omg"))) { 134 | case Result.Failure.MismatchedArguments( 135 | Seq(ArgSig(None, Some('i'), _, _, _, _, _)), 136 | Nil, 137 | Nil, 138 | None 139 | ) => 140 | } 141 | test("incomplete") { 142 | // Make sure both long args and short args properly report 143 | // incomplete arguments as distinct from other mismatches 144 | test - assertMatch(check.parseInvoke(List("qux", "-s"))) { 145 | case Result.Failure.MismatchedArguments( 146 | Nil, 147 | Nil, 148 | Nil, 149 | Some(_) 150 | ) => 151 | } 152 | test - assertMatch(check.parseInvoke(List("baz", "--arg"))) { 153 | case Result.Failure.MismatchedArguments( 154 | Nil, 155 | Nil, 156 | Nil, 157 | Some(_) 158 | ) => 159 | } 160 | } 161 | } 162 | 163 | test("tooManyParams") - check( 164 | List("foo", "1", "2"), 165 | Result.Failure.MismatchedArguments(Nil, List("1", "2"), Nil, None) 166 | ) 167 | 168 | test("failing") - check( 169 | List("ex"), 170 | Result.Failure.Exception(CoreBase.MyException) 171 | ) 172 | } 173 | } 174 | } 175 | 176 | object CorePositionalDisabledOnlyTests extends TestSuite { 177 | val check = new Checker(ParserForMethods(CoreBase), allowPositional = false) 178 | 179 | val tests = Tests { 180 | test("invoke") { 181 | test - check( 182 | List("bar", "2"), 183 | MismatchedArguments( 184 | missing = List(ArgSig( 185 | None, 186 | Some('i'), 187 | None, 188 | None, 189 | TokensReader.IntRead, 190 | positional = false, 191 | hidden = false 192 | )), 193 | unknown = List("2") 194 | ) 195 | ) 196 | test - check( 197 | List("qux", "2"), 198 | MismatchedArguments( 199 | missing = List(ArgSig( 200 | None, 201 | Some('i'), 202 | None, 203 | None, 204 | TokensReader.IntRead, 205 | positional = false, 206 | hidden = false 207 | )), 208 | unknown = List("2") 209 | ) 210 | ) 211 | test - check( 212 | List("qux", "3", "x"), 213 | MismatchedArguments( 214 | missing = List(ArgSig( 215 | None, 216 | Some('i'), 217 | None, 218 | None, 219 | TokensReader.IntRead, 220 | positional = false, 221 | hidden = false 222 | )), 223 | unknown = List("3", "x") 224 | ) 225 | ) 226 | test - check( 227 | List("qux", "-i", "3", "x"), 228 | MismatchedArguments(List(), List("x"), List(), None) 229 | ) 230 | } 231 | 232 | test("failures") { 233 | test("invalidParams") - check( 234 | List("bar", "lol"), 235 | MismatchedArguments( 236 | missing = List(ArgSig( 237 | None, 238 | Some('i'), 239 | None, 240 | None, 241 | TokensReader.IntRead, 242 | positional = false, 243 | hidden = false 244 | )), 245 | unknown = List("lol") 246 | ) 247 | ) 248 | } 249 | 250 | test("redundantParams") - check( 251 | List("qux", "1", "-i", "2"), 252 | MismatchedArguments( 253 | missing = List(ArgSig(None, Some('i'), None, None, TokensReader.IntRead, positional = false, hidden = false)), 254 | unknown = List("1", "-i", "2") 255 | ) 256 | ) 257 | } 258 | } 259 | 260 | object CorePositionalEnabledOnlyTests extends TestSuite { 261 | val check = new Checker(Parser(CoreBase), allowPositional = true) 262 | 263 | val tests = Tests { 264 | test("invoke") { 265 | test - check(List("bar", "2"), Result.Success(2)) 266 | test - check(List("qux", "2"), Result.Success("lolslols")) 267 | test - check(List("qux", "3", "x"), Result.Success("xxx")) 268 | test - check(List("qux", "2", "-"), Result.Success("--")) 269 | test - check(List("qux", "2", "--"), Result.Success("----")) 270 | test - check(List("qux", "1", "---"), Result.Success("---")) 271 | test - check( 272 | List("qux", "-i", "3", "x"), 273 | Result.Success("xxx") 274 | ) 275 | } 276 | 277 | test("failures") { 278 | test("invalidParams") - assertMatch( 279 | check.parseInvoke(List("bar", "lol")) 280 | ) { 281 | case Result.Failure.InvalidArguments( 282 | List(Result.ParamError.Failed( 283 | ArgSig(None, Some('i'), _, _, _, _, _), 284 | Seq("lol"), 285 | _ 286 | )) 287 | ) => 288 | } 289 | 290 | test("redundantParams") { 291 | val parsed = check.parseInvoke(List("qux", "1", "-i", "2")) 292 | assertMatch(parsed) { 293 | case Result.Failure.MismatchedArguments( 294 | Nil, 295 | Nil, 296 | Seq((ArgSig(None, Some('i'), _, _, _, _, _), Seq("1", "2"))), 297 | None 298 | ) => 299 | } 300 | } 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /mainargs/src-3/Macros.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import scala.quoted._ 4 | 5 | object Macros { 6 | private def mainAnnotation(using Quotes) = quotes.reflect.Symbol.requiredClass("mainargs.main") 7 | private def argAnnotation(using Quotes) = quotes.reflect.Symbol.requiredClass("mainargs.arg") 8 | def parserForMethods[B](base: Expr[B])(using Quotes, Type[B]): Expr[ParserForMethods[B]] = { 9 | import quotes.reflect._ 10 | val allMethods = TypeRepr.of[B].typeSymbol.memberMethods 11 | val annotatedMethodsWithMainAnnotations = allMethods.flatMap { methodSymbol => 12 | methodSymbol.getAnnotation(mainAnnotation).map(methodSymbol -> _) 13 | }.sortBy(_._1.pos.map(_.start)) 14 | val mainDatas = Expr.ofList(annotatedMethodsWithMainAnnotations.map { (annotatedMethod, mainAnnotationInstance) => 15 | createMainData[Any, B](annotatedMethod, mainAnnotationInstance) 16 | }) 17 | 18 | '{ 19 | new ParserForMethods[B]( 20 | MethodMains[B]($mainDatas, () => $base) 21 | ) 22 | } 23 | } 24 | 25 | def parserForClass[B](using Quotes, Type[B]): Expr[ParserForClass[B]] = { 26 | import quotes.reflect._ 27 | val typeReprOfB = TypeRepr.of[B] 28 | val companionModule = typeReprOfB match { 29 | case TypeRef(a,b) => TermRef(a,b) 30 | } 31 | val typeSymbolOfB = typeReprOfB.typeSymbol 32 | val companionModuleType = typeSymbolOfB.companionModule.tree.asInstanceOf[ValDef].tpt.tpe.asType 33 | val companionModuleExpr = Ident(companionModule).asExpr 34 | val mainAnnotationInstance = typeSymbolOfB.getAnnotation(mainAnnotation).getOrElse { 35 | '{new mainargs.main()}.asTerm // construct a default if not found. 36 | } 37 | val ctor = typeSymbolOfB.primaryConstructor 38 | val ctorParams = ctor.paramSymss.flatten 39 | // try to match the apply method with the constructor parameters, this is a good heuristic 40 | // for if the apply method is overloaded. 41 | val annotatedMethod = typeSymbolOfB.companionModule.memberMethod("apply").filter(p => 42 | p.paramSymss.flatten.corresponds(ctorParams) { (p1, p2) => 43 | p1.name == p2.name 44 | } 45 | ).headOption.getOrElse { 46 | report.errorAndAbort( 47 | s"Cannot find apply method in companion object of ${typeReprOfB.show}", 48 | typeSymbolOfB.companionModule.pos.getOrElse(Position.ofMacroExpansion) 49 | ) 50 | } 51 | companionModuleType match 52 | case '[bCompanion] => 53 | val mainData = createMainData[B, bCompanion]( 54 | annotatedMethod, 55 | mainAnnotationInstance, 56 | // Somehow the `apply` method parameter annotations don't end up on 57 | // the `apply` method parameters, but end up in the `` method 58 | // parameters, so use those for getting the annotations instead 59 | TypeRepr.of[B].typeSymbol.primaryConstructor.paramSymss 60 | ) 61 | val erasedMainData = '{$mainData.asInstanceOf[MainData[B, Any]]} 62 | '{ new ParserForClass[B]($erasedMainData, () => ${ Ident(companionModule).asExpr }) } 63 | } 64 | 65 | def createMainData[T: Type, B: Type](using Quotes) 66 | (method: quotes.reflect.Symbol, 67 | mainAnnotation: quotes.reflect.Term): Expr[MainData[T, B]] = { 68 | createMainData[T, B](method, mainAnnotation, method.paramSymss) 69 | } 70 | 71 | private object VarargTypeRepr { 72 | def unapply(using Quotes)(tpe: quotes.reflect.TypeRepr): Option[quotes.reflect.TypeRepr] = { 73 | import quotes.reflect.* 74 | tpe match { 75 | case AnnotatedType(AppliedType(_, Seq(arg)), x) // Scala 3 repeated parameter 76 | if x.tpe =:= defn.RepeatedAnnot.typeRef => Some(arg) 77 | case AppliedType(TypeRef(pre, ""), Seq(arg)) // Scala 2 repeated parameter, can be read from Scala 3 78 | if pre =:= defn.ScalaPackage.termRef => Some(arg) 79 | case _ => None 80 | } 81 | } 82 | } 83 | 84 | private object AsType { 85 | def unapply(using Quotes)(tpe: quotes.reflect.TypeRepr): Some[Type[?]] = { 86 | Some(tpe.asType) 87 | } 88 | } 89 | 90 | def createMainData[T: Type, B: Type](using Quotes) 91 | (method: quotes.reflect.Symbol, 92 | mainAnnotation: quotes.reflect.Term, 93 | annotatedParamsLists: List[List[quotes.reflect.Symbol]]): Expr[MainData[T, B]] = { 94 | 95 | import quotes.reflect.* 96 | val params = method.paramSymss.headOption.getOrElse(report.throwError("Multiple parameter lists not supported")) 97 | val defaultParams = if (params.exists(_.flags.is(Flags.HasDefault))) getDefaultParams(method) else Map.empty 98 | val argSigsExprs = params.zip(annotatedParamsLists.flatten).map { paramAndAnnotParam => 99 | val param = paramAndAnnotParam._1 100 | val annotParam = paramAndAnnotParam._2 101 | val paramTree = param.tree.asInstanceOf[ValDef] 102 | val paramTpe = paramTree.tpt.tpe 103 | val readerTpe = paramTpe match { 104 | case VarargTypeRepr(AsType('[t])) => TypeRepr.of[Leftover[t]] 105 | case _ => paramTpe 106 | } 107 | val arg = annotParam.getAnnotation(argAnnotation).map(_.asExprOf[mainargs.arg]).getOrElse('{ new mainargs.arg() }) 108 | readerTpe.asType match { 109 | case '[t] => 110 | def applyAndCast(f: Expr[Any] => Expr[Any], arg: Expr[B]): Expr[t] = { 111 | f(arg) match { 112 | case '{ $v: `t` } => v 113 | case expr => { 114 | // this case will be activated when the found default parameter is not of type `t` 115 | val recoveredType = 116 | try 117 | expr.asExprOf[t] 118 | catch 119 | case err: Exception => 120 | report.errorAndAbort( 121 | s"""Failed to convert default value for parameter ${param.name}, 122 | |expected type: ${paramTpe.show}, 123 | |but default value ${expr.show} is of type: ${expr.asTerm.tpe.widen.show} 124 | |while converting type caught an exception with message: ${err.getMessage} 125 | |There might be a bug in mainargs.""".stripMargin, 126 | param.pos.getOrElse(Position.ofMacroExpansion) 127 | ) 128 | recoveredType 129 | } 130 | } 131 | } 132 | val defaultParam: Expr[Option[B => t]] = defaultParams.get(param) match { 133 | case Some(f) => '{ Some((b: B) => ${ applyAndCast(f, 'b) }) } 134 | case None => '{ None } 135 | } 136 | val tokensReader = Expr.summon[mainargs.TokensReader[t]].getOrElse { 137 | report.errorAndAbort( 138 | s"No mainargs.TokensReader[${Type.show[t]}] found for parameter ${param.name} of method ${method.name} in ${method.owner.fullName}", 139 | method.pos.getOrElse(Position.ofMacroExpansion) 140 | ) 141 | } 142 | '{ (ArgSig.create[t, B](${ Expr(param.name) }, ${ arg }, ${ defaultParam })(using ${ tokensReader })) } 143 | } 144 | } 145 | val argSigs = Expr.ofList(argSigsExprs) 146 | 147 | val invokeRaw: Expr[(B, Seq[Any]) => T] = { 148 | 149 | def callOf(methodOwner: Expr[Any], args: Expr[Seq[Any]]) = 150 | call(methodOwner, method, args).asExprOf[T] 151 | 152 | '{ (b: B, params: Seq[Any]) => ${ callOf('b, 'params) } } 153 | } 154 | '{ MainData.create[T, B](${ Expr(method.name) }, ${ mainAnnotation.asExprOf[mainargs.main] }, ${ argSigs }, ${ invokeRaw }) } 155 | } 156 | 157 | /** Call a method given by its symbol. 158 | * 159 | * E.g. 160 | * 161 | * assuming: 162 | * 163 | * def foo(x: Int, y: String)(z: Int) 164 | * 165 | * val argss: List[List[Any]] = ??? 166 | * 167 | * then: 168 | * 169 | * call(, '{argss}) 170 | * 171 | * will expand to: 172 | * 173 | * foo(argss(0)(0), argss(0)(1))(argss(1)(0)) 174 | * 175 | */ 176 | private def call(using Quotes)( 177 | methodOwner: Expr[Any], 178 | method: quotes.reflect.Symbol, 179 | args: Expr[Seq[Any]] 180 | ): Expr[_] = { 181 | // Copy pasted from Cask. 182 | // https://github.com/com-lihaoyi/cask/blob/65b9c8e4fd528feb71575f6e5ef7b5e2e16abbd9/cask/src-3/cask/router/Macros.scala#L106 183 | import quotes.reflect._ 184 | val paramss = method.paramSymss 185 | 186 | if (paramss.isEmpty) { 187 | report.errorAndAbort("At least one parameter list must be declared.", method.pos.get) 188 | } 189 | if (paramss.sizeIs > 1) { 190 | report.errorAndAbort("Multiple parameter lists are not supported.", method.pos.get) 191 | } 192 | val params = paramss.head 193 | 194 | val methodType = methodOwner.asTerm.tpe.memberType(method) 195 | 196 | def accesses(ref: Expr[Seq[Any]]): List[Term] = 197 | for (i <- params.indices.toList) yield { 198 | val param = params(i) 199 | val tpe = methodType.memberType(param) 200 | val untypedRef = '{ $ref(${Expr(i)}) } 201 | tpe match { 202 | case VarargTypeRepr(AsType('[t])) => 203 | Typed( 204 | '{ $untypedRef.asInstanceOf[Leftover[t]].value }.asTerm, 205 | Inferred(AppliedType(defn.RepeatedParamClass.typeRef, List(TypeRepr.of[t]))) 206 | ) 207 | case _ => tpe.asType match 208 | case '[t] => '{ $untypedRef.asInstanceOf[t] }.asTerm 209 | } 210 | } 211 | 212 | methodOwner.asTerm.select(method).appliedToArgs(accesses(args)).asExpr 213 | } 214 | 215 | /** Lookup default values for a method's parameters. */ 216 | private def getDefaultParams(using Quotes)(method: quotes.reflect.Symbol): Map[quotes.reflect.Symbol, Expr[Any] => Expr[Any]] = { 217 | // Copy pasted from Cask. 218 | // https://github.com/com-lihaoyi/cask/blob/65b9c8e4fd528feb71575f6e5ef7b5e2e16abbd9/cask/src-3/cask/router/Macros.scala#L38 219 | import quotes.reflect._ 220 | 221 | val params = method.paramSymss.flatten 222 | val defaults = collection.mutable.Map.empty[Symbol, Expr[Any] => Expr[Any]] 223 | 224 | val Name = (method.name + """\$default\$(\d+)""").r 225 | val InitName = """\$lessinit\$greater\$default\$(\d+)""".r 226 | 227 | val idents = method.owner.tree.asInstanceOf[ClassDef].body 228 | 229 | idents.foreach{ 230 | case deff @ DefDef(Name(idx), _, _, _) => 231 | val expr = (owner: Expr[Any]) => Select(owner.asTerm, deff.symbol).asExpr 232 | defaults += (params(idx.toInt - 1) -> expr) 233 | 234 | // The `apply` method re-uses the default param factory methods from ``, 235 | // so make sure to check if those exist too 236 | case deff @ DefDef(InitName(idx), _, _, _) if method.name == "apply" => 237 | val expr = (owner: Expr[Any]) => Select(owner.asTerm, deff.symbol).asExpr 238 | defaults += (params(idx.toInt - 1) -> expr) 239 | 240 | case _ => 241 | } 242 | 243 | defaults.toMap 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /mainargs/test/src-jvm-2/AmmoniteTests.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import utest._ 4 | 5 | object AmmoniteDefaults { 6 | val welcomeBanner = { 7 | def scalaVersion = scala.util.Properties.versionNumberString 8 | 9 | def javaVersion = System.getProperty("java.version") 10 | 11 | s"Welcome to the Ammonite Repl (Scala $scalaVersion Java $javaVersion)" 12 | } 13 | 14 | def ammoniteHome = os.Path(System.getProperty("user.home")) / ".ammonite" 15 | } 16 | 17 | @main 18 | case class AmmoniteConfig( 19 | core: AmmoniteConfig.Core, 20 | predef: AmmoniteConfig.Predef, 21 | repl: AmmoniteConfig.Repl, 22 | rest: Leftover[String] 23 | ) 24 | 25 | object AmmoniteConfig { 26 | implicit object PathRead extends TokensReader.Simple[os.Path] { 27 | def shortName = "path" 28 | def read(strs: Seq[String]) = Right(os.Path(strs.head, os.pwd)) 29 | } 30 | 31 | case class InjectedConstant() 32 | 33 | implicit object InjectedTokensReader extends TokensReader.Constant[InjectedConstant] { 34 | def read() = Right(new InjectedConstant()) 35 | } 36 | @main 37 | case class Core( 38 | injectedConstant: InjectedConstant, 39 | @arg( 40 | name = "no-default-predef", 41 | doc = "Disable the default predef and run Ammonite with the minimal predef possible" 42 | ) 43 | noDefaultPredef: Flag, 44 | @arg( 45 | short = 's', 46 | doc = 47 | "Make ivy logs go silent instead of printing though failures will " + 48 | "still throw exception" 49 | ) 50 | silent: Flag, 51 | @arg( 52 | short = 'w', 53 | doc = "Watch and re-run your scripts when they change" 54 | ) 55 | watch: Flag, 56 | @arg(doc = "Run a BSP server against the passed scripts") 57 | bsp: Flag, 58 | @arg( 59 | short = 'c', 60 | doc = "Pass in code to be run immediately in the REPL" 61 | ) 62 | code: Option[String] = None, 63 | @arg( 64 | short = 'h', 65 | doc = "The home directory of the REPL; where it looks for config and caches" 66 | ) 67 | home: os.Path = AmmoniteDefaults.ammoniteHome, 68 | @arg( 69 | name = "predef", 70 | short = 'p', 71 | doc = 72 | "Lets you load your predef from a custom location, rather than the " + 73 | "default location in your Ammonite home" 74 | ) 75 | predefFile: Option[os.Path] = None, 76 | @arg( 77 | doc = 78 | "Enable or disable colored output; by default colors are enabled " + 79 | "in both REPL and scripts if the console is interactive, and disabled " + 80 | "otherwise" 81 | ) 82 | color: Option[Boolean] = None, 83 | @arg( 84 | doc = 85 | "Hide parts of the core of Ammonite and some of its dependencies. By default, " + 86 | "the core of Ammonite and all of its dependencies can be seen by users from the " + 87 | "Ammonite session. This option mitigates that via class loader isolation." 88 | ) 89 | thin: Flag, 90 | @arg(doc = "Print this message") 91 | help: Flag 92 | ) 93 | implicit val coreParser = Parser[Core] 94 | 95 | @main 96 | case class Predef( 97 | @arg( 98 | name = "predef-code", 99 | doc = "Any commands you want to execute at the start of the REPL session" 100 | ) 101 | predefCode: String = "", 102 | @arg( 103 | name = "no-home-predef", 104 | doc = 105 | "Disables the default behavior of loading predef files from your " + 106 | "~/.ammonite/predef.sc, predefScript.sc, or predefShared.sc. You can " + 107 | "choose an additional predef to use using `--predef" 108 | ) 109 | noHomePredef: Flag 110 | ) 111 | implicit val predefParser = Parser[Predef] 112 | 113 | @main 114 | case class Repl( 115 | @arg( 116 | short = 'b', 117 | doc = "Customize the welcome banner that gets shown when Ammonite starts" 118 | ) 119 | banner: String = AmmoniteDefaults.welcomeBanner, 120 | @arg( 121 | name = "no-remote-logging", 122 | doc = 123 | "(deprecated) Disable remote logging of the number of times a REPL starts and runs commands" 124 | ) 125 | noRemoteLogging: Flag, 126 | @arg( 127 | doc = 128 | "Wrap user code in classes rather than singletons, typically for Java serialization " + 129 | "friendliness." 130 | ) 131 | classBased: Flag 132 | ) 133 | implicit val replParser = Parser[Repl] 134 | } 135 | 136 | object AmmoniteTests extends TestSuite { 137 | val parser = Parser[AmmoniteConfig] 138 | val tests = Tests { 139 | 140 | test("formatMainMethods.unsorted") { 141 | val customName = s"Ammonite REPL & Script-Runner" 142 | val customDoc = "usage: amm [ammonite-options] [script-file [script-options]]" 143 | val rendered = 144 | parser.helpText(customName = customName, customDoc = customDoc, sorted = false).trim 145 | val expected = 146 | """Ammonite REPL & Script-Runner 147 | |usage: amm [ammonite-options] [script-file [script-options]] 148 | | --no-default-predef Disable the default predef and run Ammonite with the minimal predef possible 149 | | -s --silent Make ivy logs go silent instead of printing though failures will still throw 150 | | exception 151 | | -w --watch Watch and re-run your scripts when they change 152 | | --bsp Run a BSP server against the passed scripts 153 | | -c --code Pass in code to be run immediately in the REPL 154 | | -h --home The home directory of the REPL; where it looks for config and caches 155 | | -p --predef Lets you load your predef from a custom location, rather than the default 156 | | location in your Ammonite home 157 | | --color Enable or disable colored output; by default colors are enabled in both REPL 158 | | and scripts if the console is interactive, and disabled otherwise 159 | | --thin Hide parts of the core of Ammonite and some of its dependencies. By default, 160 | | the core of Ammonite and all of its dependencies can be seen by users from 161 | | the Ammonite session. This option mitigates that via class loader isolation. 162 | | --help Print this message 163 | | --predef-code Any commands you want to execute at the start of the REPL session 164 | | --no-home-predef Disables the default behavior of loading predef files from your 165 | | ~/.ammonite/predef.sc, predefScript.sc, or predefShared.sc. You can choose an 166 | | additional predef to use using `--predef 167 | | -b --banner Customize the welcome banner that gets shown when Ammonite starts 168 | | --no-remote-logging (deprecated) Disable remote logging of the number of times a REPL starts and 169 | | runs commands 170 | | --classBased Wrap user code in classes rather than singletons, typically for Java 171 | | serialization friendliness. 172 | | rest ... 173 | |""".stripMargin.trim 174 | 175 | assert(rendered == expected) 176 | 177 | } 178 | 179 | test("formatMainMethods.sorted") { 180 | val customName = s"Ammonite REPL & Script-Runner" 181 | val customDoc = "usage: amm [ammonite-options] [script-file [script-options]]" 182 | val rendered = 183 | parser.helpText(customName = customName, customDoc = customDoc, sorted = true).trim 184 | val expected = 185 | """Ammonite REPL & Script-Runner 186 | |usage: amm [ammonite-options] [script-file [script-options]] 187 | | -b --banner Customize the welcome banner that gets shown when Ammonite starts 188 | | --bsp Run a BSP server against the passed scripts 189 | | -c --code Pass in code to be run immediately in the REPL 190 | | --classBased Wrap user code in classes rather than singletons, typically for Java 191 | | serialization friendliness. 192 | | --color Enable or disable colored output; by default colors are enabled in both REPL 193 | | and scripts if the console is interactive, and disabled otherwise 194 | | -h --home The home directory of the REPL; where it looks for config and caches 195 | | --help Print this message 196 | | --no-default-predef Disable the default predef and run Ammonite with the minimal predef possible 197 | | --no-home-predef Disables the default behavior of loading predef files from your 198 | | ~/.ammonite/predef.sc, predefScript.sc, or predefShared.sc. You can choose an 199 | | additional predef to use using `--predef 200 | | --no-remote-logging (deprecated) Disable remote logging of the number of times a REPL starts and 201 | | runs commands 202 | | -p --predef Lets you load your predef from a custom location, rather than the default 203 | | location in your Ammonite home 204 | | --predef-code Any commands you want to execute at the start of the REPL session 205 | | -s --silent Make ivy logs go silent instead of printing though failures will still throw 206 | | exception 207 | | --thin Hide parts of the core of Ammonite and some of its dependencies. By default, 208 | | the core of Ammonite and all of its dependencies can be seen by users from 209 | | the Ammonite session. This option mitigates that via class loader isolation. 210 | | -w --watch Watch and re-run your scripts when they change 211 | | rest ... 212 | |""".stripMargin.trim 213 | 214 | assert(rendered == expected) 215 | 216 | } 217 | 218 | test("parseInvoke") { 219 | parser.constructEither(Array("--code", "println(1)").toIndexedSeq) ==> 220 | Right( 221 | AmmoniteConfig( 222 | AmmoniteConfig.Core( 223 | injectedConstant = AmmoniteConfig.InjectedConstant(), 224 | noDefaultPredef = Flag(), 225 | silent = Flag(), 226 | watch = Flag(), 227 | bsp = Flag(), 228 | code = Some("println(1)"), 229 | thin = Flag(), 230 | help = Flag() 231 | ), 232 | AmmoniteConfig.Predef(noHomePredef = Flag()), 233 | AmmoniteConfig.Repl(noRemoteLogging = Flag(), classBased = Flag()), 234 | Leftover() 235 | ) 236 | ) 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /mill: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. 4 | # 5 | # This script determines the Mill version to use by trying these sources 6 | # - env-variable `MILL_VERSION` 7 | # - local file `.mill-version` 8 | # - local file `.config/mill-version` 9 | # - `mill-version` from YAML fronmatter of current buildfile 10 | # - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) 11 | # - env-variable `DEFAULT_MILL_VERSION` 12 | # 13 | # If a version has the suffix '-native' a native binary will be used. 14 | # If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. 15 | # If no such suffix is found, the script will pick a default based on version and platform. 16 | # 17 | # Once a version was determined, it tries to use either 18 | # - a system-installed mill, if found and it's version matches 19 | # - an already downloaded version under ~/.cache/mill/download 20 | # 21 | # If no working mill version was found on the system, 22 | # this script downloads a binary file from Maven Central or Github Pages (this is version dependent) 23 | # into a cache location (~/.cache/mill/download). 24 | # 25 | # Mill Project URL: https://github.com/com-lihaoyi/mill 26 | # Script Version: 1.0.0-M1-21-7b6fae-DIRTY892b63e8 27 | # 28 | # If you want to improve this script, please also contribute your changes back! 29 | # This script was generated from: dist/scripts/src/mill.sh 30 | # 31 | # Licensed under the Apache License, Version 2.0 32 | 33 | set -e 34 | 35 | if [ -z "${DEFAULT_MILL_VERSION}" ] ; then 36 | DEFAULT_MILL_VERSION=1.0.0-RC1 37 | fi 38 | 39 | 40 | if [ -z "${GITHUB_RELEASE_CDN}" ] ; then 41 | GITHUB_RELEASE_CDN="" 42 | fi 43 | 44 | 45 | MILL_REPO_URL="https://github.com/com-lihaoyi/mill" 46 | 47 | if [ -z "${CURL_CMD}" ] ; then 48 | CURL_CMD=curl 49 | fi 50 | 51 | # Explicit commandline argument takes precedence over all other methods 52 | if [ "$1" = "--mill-version" ] ; then 53 | echo "The --mill-version option is no longer supported." 1>&2 54 | fi 55 | 56 | MILL_BUILD_SCRIPT="" 57 | 58 | if [ -f "build.mill" ] ; then 59 | MILL_BUILD_SCRIPT="build.mill" 60 | elif [ -f "build.mill.scala" ] ; then 61 | MILL_BUILD_SCRIPT="build.mill.scala" 62 | elif [ -f "build.sc" ] ; then 63 | MILL_BUILD_SCRIPT="build.sc" 64 | fi 65 | 66 | # Please note, that if a MILL_VERSION is already set in the environment, 67 | # We reuse it's value and skip searching for a value. 68 | 69 | # If not already set, read .mill-version file 70 | if [ -z "${MILL_VERSION}" ] ; then 71 | if [ -f ".mill-version" ] ; then 72 | MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" 73 | elif [ -f ".config/mill-version" ] ; then 74 | MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" 75 | elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then 76 | MILL_VERSION="$(cat ${MILL_BUILD_SCRIPT} | grep '//[|] *mill-version: *' | sed 's;//| *mill-version: *;;')" 77 | fi 78 | fi 79 | 80 | MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" 81 | 82 | if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then 83 | MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" 84 | fi 85 | 86 | # If not already set, try to fetch newest from Github 87 | if [ -z "${MILL_VERSION}" ] ; then 88 | # TODO: try to load latest version from release page 89 | echo "No mill version specified." 1>&2 90 | echo "You should provide a version via a '//| mill-version: ' comment or a '.mill-version' file." 1>&2 91 | 92 | mkdir -p "${MILL_DOWNLOAD_PATH}" 93 | LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( 94 | # we might be on OSX or BSD which don't have -d option for touch 95 | # but probably a -A [-][[hh]mm]SS 96 | touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" 97 | ) || ( 98 | # in case we still failed, we retry the first touch command with the intention 99 | # to show the (previously suppressed) error message 100 | LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 101 | ) 102 | 103 | # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 104 | # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then 105 | if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then 106 | # we know a current latest version 107 | MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) 108 | fi 109 | 110 | if [ -z "${MILL_VERSION}" ] ; then 111 | # we don't know a current latest version 112 | echo "Retrieving latest mill version ..." 1>&2 113 | LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" 114 | MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) 115 | fi 116 | 117 | if [ -z "${MILL_VERSION}" ] ; then 118 | # Last resort 119 | MILL_VERSION="${DEFAULT_MILL_VERSION}" 120 | echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 121 | else 122 | echo "Using mill version ${MILL_VERSION}" 1>&2 123 | fi 124 | fi 125 | 126 | MILL_NATIVE_SUFFIX="-native" 127 | MILL_JVM_SUFFIX="-jvm" 128 | FULL_MILL_VERSION=$MILL_VERSION 129 | ARTIFACT_SUFFIX="" 130 | set_artifact_suffix(){ 131 | if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then 132 | if [ "$(uname -m)" = "aarch64" ]; then 133 | ARTIFACT_SUFFIX="-native-linux-aarch64" 134 | else 135 | ARTIFACT_SUFFIX="-native-linux-amd64" 136 | fi 137 | elif [ "$(uname)" = "Darwin" ]; then 138 | if [ "$(uname -m)" = "arm64" ]; then 139 | ARTIFACT_SUFFIX="-native-mac-aarch64" 140 | else 141 | ARTIFACT_SUFFIX="-native-mac-amd64" 142 | fi 143 | else 144 | echo "This native mill launcher supports only Linux and macOS." 1>&2 145 | exit 1 146 | fi 147 | } 148 | 149 | case "$MILL_VERSION" in 150 | *"$MILL_NATIVE_SUFFIX") 151 | MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} 152 | set_artifact_suffix 153 | ;; 154 | 155 | *"$MILL_JVM_SUFFIX") 156 | MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} 157 | ;; 158 | 159 | *) 160 | case "$MILL_VERSION" in 161 | 0.1.*) ;; 162 | 0.2.*) ;; 163 | 0.3.*) ;; 164 | 0.4.*) ;; 165 | 0.5.*) ;; 166 | 0.6.*) ;; 167 | 0.7.*) ;; 168 | 0.8.*) ;; 169 | 0.9.*) ;; 170 | 0.10.*) ;; 171 | 0.11.*) ;; 172 | 0.12.*) ;; 173 | *) 174 | set_artifact_suffix 175 | esac 176 | ;; 177 | esac 178 | 179 | MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" 180 | 181 | try_to_use_system_mill() { 182 | if [ "$(uname)" != "Linux" ]; then 183 | return 0 184 | fi 185 | 186 | MILL_IN_PATH="$(command -v mill || true)" 187 | 188 | if [ -z "${MILL_IN_PATH}" ]; then 189 | return 0 190 | fi 191 | 192 | SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") 193 | if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then 194 | # MILL_IN_PATH is (very likely) a shell script and not the mill 195 | # executable, ignore it. 196 | return 0 197 | fi 198 | 199 | SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") 200 | SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") 201 | SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") 202 | 203 | if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then 204 | mkdir -p "${MILL_USER_CACHE_DIR}" 205 | fi 206 | 207 | SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" 208 | if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then 209 | parseSystemMillInfo() { 210 | LINE_NUMBER="${1}" 211 | # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the 212 | # variable definition in that line in two halves and return 213 | # the value, and finally remove the quotes. 214 | sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ 215 | cut -d= -f2 |\ 216 | sed 's/"\(.*\)"/\1/' 217 | } 218 | 219 | CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) 220 | CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) 221 | CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) 222 | CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) 223 | 224 | if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ 225 | && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ 226 | && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then 227 | if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then 228 | MILL="${SYSTEM_MILL_PATH}" 229 | return 0 230 | else 231 | return 0 232 | fi 233 | fi 234 | fi 235 | 236 | SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') 237 | 238 | cat < "${SYSTEM_MILL_INFO_FILE}" 239 | CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" 240 | CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" 241 | CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" 242 | CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" 243 | EOF 244 | 245 | if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then 246 | MILL="${SYSTEM_MILL_PATH}" 247 | fi 248 | } 249 | try_to_use_system_mill 250 | 251 | # If not already downloaded, download it 252 | if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then 253 | case $MILL_VERSION in 254 | 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) 255 | DOWNLOAD_SUFFIX="" 256 | DOWNLOAD_FROM_MAVEN=0 257 | ;; 258 | 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) 259 | DOWNLOAD_SUFFIX="-assembly" 260 | DOWNLOAD_FROM_MAVEN=0 261 | ;; 262 | *) 263 | DOWNLOAD_SUFFIX="-assembly" 264 | DOWNLOAD_FROM_MAVEN=1 265 | ;; 266 | esac 267 | case $MILL_VERSION in 268 | 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) 269 | DOWNLOAD_EXT="jar" 270 | ;; 271 | 0.12.* ) 272 | DOWNLOAD_EXT="exe" 273 | ;; 274 | 0.* ) 275 | DOWNLOAD_EXT="jar" 276 | ;; 277 | *) 278 | DOWNLOAD_EXT="exe" 279 | ;; 280 | esac 281 | 282 | DOWNLOAD_FILE=$(mktemp mill.XXXXXX) 283 | if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then 284 | DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${DOWNLOAD_EXT}" 285 | else 286 | MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') 287 | DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" 288 | unset MILL_VERSION_TAG 289 | fi 290 | 291 | if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then 292 | echo $DOWNLOAD_URL 293 | echo $MILL 294 | exit 0 295 | fi 296 | # TODO: handle command not found 297 | echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 298 | ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" 299 | chmod +x "${DOWNLOAD_FILE}" 300 | mkdir -p "${MILL_DOWNLOAD_PATH}" 301 | mv "${DOWNLOAD_FILE}" "${MILL}" 302 | 303 | unset DOWNLOAD_FILE 304 | unset DOWNLOAD_SUFFIX 305 | fi 306 | 307 | if [ -z "$MILL_MAIN_CLI" ] ; then 308 | MILL_MAIN_CLI="${0}" 309 | fi 310 | 311 | MILL_FIRST_ARG="" 312 | if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then 313 | # Need to preserve the first position of those listed options 314 | MILL_FIRST_ARG=$1 315 | shift 316 | fi 317 | 318 | unset MILL_DOWNLOAD_PATH 319 | unset MILL_OLD_DOWNLOAD_PATH 320 | unset OLD_MILL 321 | unset MILL_VERSION 322 | unset MILL_REPO_URL 323 | 324 | # -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 325 | # We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes 326 | # shellcheck disable=SC2086 327 | exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" 328 | -------------------------------------------------------------------------------- /mainargs/src/Renderer.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import java.io.{PrintWriter, StringWriter} 4 | import scala.math 5 | 6 | object Renderer { 7 | 8 | def getLeftColWidth(items: Seq[ArgSig]): Int = getLeftColWidth(items, Util.kebabCaseNameMapper) 9 | def getLeftColWidth(items: Seq[ArgSig], nameMapper: String => Option[String]): Int = { 10 | if (items.isEmpty) 0 11 | else items.map(renderArgShort(_, nameMapper).length).max 12 | } 13 | 14 | val newLine = System.lineSeparator() 15 | 16 | def normalizeNewlines(s: String) = s.replace("\r", "").replace("\n", newLine) 17 | 18 | def renderArgShort(arg: ArgSig): String = renderArgShort(arg, Util.nullNameMapper) 19 | 20 | def renderArgShort(arg: ArgSig, nameMapper: String => Option[String]): String = arg.reader match { 21 | case r: TokensReader.Flag => 22 | val shortPrefix = arg.shortName.map(c => s"-$c") 23 | val nameSuffix = arg.longName(nameMapper).map(s => s"--$s") 24 | (shortPrefix ++ nameSuffix).mkString(" ") 25 | 26 | case r: TokensReader.Simple[_] => 27 | val shortPrefix = arg.shortName.map(c => s"-$c") 28 | val typeSuffix = s"<${r.shortName}>" 29 | val nameSuffix = if (arg.positional) arg.longName(nameMapper) else arg.longName(nameMapper).map(s => s"--$s") 30 | (shortPrefix ++ nameSuffix ++ Seq(typeSuffix)).mkString(" ") 31 | 32 | case r: TokensReader.Leftover[_, _] => 33 | s"${arg.longName(nameMapper).get} <${r.shortName}>..." 34 | } 35 | 36 | /** 37 | * Returns a `Some[string]` with the sortable string or a `None` if it is an leftover. 38 | */ 39 | private def sortableName(arg: ArgSig, nameMapper: String => Option[String]): Option[String] = arg match { 40 | case arg: ArgSig if arg.reader.isLeftover => None 41 | 42 | case a: ArgSig => 43 | a.shortName.map(_.toString).orElse(a.longName(nameMapper)).orElse(Some("")) 44 | case a: ArgSig => 45 | a.longName(nameMapper) 46 | } 47 | 48 | object ArgOrd extends math.Ordering[ArgSig] { 49 | override def compare(x: ArgSig, y: ArgSig): Int = 50 | (sortableName(x, Util.nullNameMapper), sortableName(y, Util.nullNameMapper)) match { 51 | case (None, None) => 0 // don't sort leftovers 52 | case (None, Some(_)) => 1 // keep left overs at the end 53 | case (Some(_), None) => -1 // keep left overs at the end 54 | case (Some(l), Some(r)) => l.compare(r) 55 | } 56 | } 57 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 58 | def renderArg( 59 | arg: ArgSig, 60 | leftOffset: Int, 61 | wrappedWidth: Int): (String, String) = renderArg(arg, leftOffset, wrappedWidth, Util.kebabCaseNameMapper) 62 | 63 | def renderArg( 64 | arg: ArgSig, 65 | leftOffset: Int, 66 | wrappedWidth: Int, 67 | nameMapper: String => Option[String] 68 | ): (String, String) = { 69 | val wrapped = softWrap(arg.doc.getOrElse(""), leftOffset, wrappedWidth - leftOffset) 70 | (renderArgShort(arg, nameMapper), wrapped) 71 | } 72 | 73 | def formatMainMethods( 74 | mainMethods: Seq[MainData[_, _]], 75 | totalWidth: Int, 76 | docsOnNewLine: Boolean, 77 | customNames: Map[String, String], 78 | customDocs: Map[String, String], 79 | sorted: Boolean, 80 | ): String = formatMainMethods(mainMethods, totalWidth, docsOnNewLine, customNames, customDocs, sorted, Util.kebabCaseNameMapper) 81 | 82 | def formatMainMethods( 83 | mainMethods: Seq[MainData[_, _]], 84 | totalWidth: Int, 85 | docsOnNewLine: Boolean, 86 | customNames: Map[String, String], 87 | customDocs: Map[String, String], 88 | sorted: Boolean, 89 | nameMapper: String => Option[String] 90 | ): String = { 91 | val flattenedAll: Seq[ArgSig] = 92 | mainMethods.map(_.flattenedArgSigs) 93 | .flatten 94 | .map(_._1) 95 | 96 | val leftColWidth = getLeftColWidth(flattenedAll, nameMapper) 97 | mainMethods match { 98 | case Seq() => "" 99 | case Seq(main) => 100 | Renderer.formatMainMethodSignature( 101 | main, 102 | 0, 103 | totalWidth, 104 | leftColWidth, 105 | docsOnNewLine, 106 | customNames.get(main.name(nameMapper)), 107 | customDocs.get(main.name(nameMapper)), 108 | sorted, 109 | nameMapper 110 | ) 111 | case _ => 112 | val methods = 113 | for (main <- mainMethods) 114 | yield formatMainMethodSignature( 115 | main, 116 | 2, 117 | totalWidth, 118 | leftColWidth, 119 | docsOnNewLine, 120 | customNames.get(main.name(nameMapper)), 121 | customDocs.get(main.name(nameMapper)), 122 | sorted, 123 | nameMapper 124 | ) 125 | 126 | normalizeNewlines( 127 | s"""Available subcommands: 128 | | 129 | |${methods.mkString(newLine)}""".stripMargin 130 | ) 131 | } 132 | } 133 | 134 | @deprecated("Use other overload instead", "mainargs after 0.3.0") 135 | def formatMainMethods( 136 | mainMethods: Seq[MainData[_, _]], 137 | totalWidth: Int, 138 | docsOnNewLine: Boolean, 139 | customNames: Map[String, String], 140 | customDocs: Map[String, String], 141 | ): String = formatMainMethods( 142 | mainMethods, 143 | totalWidth, 144 | docsOnNewLine, 145 | customNames, 146 | customDocs, 147 | sorted = true, 148 | Util.kebabCaseNameMapper 149 | ) 150 | 151 | def formatMainMethodSignature( 152 | main: MainData[_, _], 153 | leftIndent: Int, 154 | totalWidth: Int, 155 | leftColWidth: Int, 156 | docsOnNewLine: Boolean, 157 | customName: Option[String], 158 | customDoc: Option[String], 159 | sorted: Boolean, 160 | nameMapper: String => Option[String] 161 | ): String = { 162 | 163 | val argLeftCol = if (docsOnNewLine) leftIndent + 8 else leftColWidth + leftIndent + 2 + 2 164 | 165 | val sortedArgs = 166 | if (sorted) main.renderedArgSigs.sorted(ArgOrd) 167 | else main.renderedArgSigs 168 | 169 | val args = sortedArgs.map(renderArg(_, argLeftCol, totalWidth, nameMapper)) 170 | 171 | val leftIndentStr = " " * leftIndent 172 | 173 | def formatArg(lhs: String, rhs: String) = { 174 | val lhsPadded = lhs.padTo(leftColWidth, ' ') 175 | val rhsPadded = rhs.linesIterator.mkString(newLine) 176 | if (rhs.isEmpty) s"$leftIndentStr $lhs" 177 | else if (docsOnNewLine) { 178 | s"$leftIndentStr $lhs\n$leftIndentStr $rhsPadded" 179 | } else { 180 | s"$leftIndentStr $lhsPadded $rhsPadded" 181 | } 182 | } 183 | val argStrings = for ((lhs, rhs) <- args) yield formatArg(lhs, rhs) 184 | 185 | val mainDocSuffix = customDoc.orElse(main.doc) match { 186 | case Some(d) => newLine + leftIndentStr + softWrap(d, leftIndent, totalWidth) 187 | case None => "" 188 | } 189 | s"""$leftIndentStr${customName.getOrElse(main.name(nameMapper))}$mainDocSuffix 190 | |${argStrings.map(_ + newLine).mkString}""".stripMargin 191 | } 192 | 193 | @deprecated("Use other overload instead", "mainargs after 0.3.0") 194 | def formatMainMethodSignature( 195 | main: MainData[_, _], 196 | leftIndent: Int, 197 | totalWidth: Int, 198 | leftColWidth: Int, 199 | docsOnNewLine: Boolean, 200 | customName: Option[String], 201 | customDoc: Option[String] 202 | ): String = formatMainMethodSignature( 203 | main, 204 | leftIndent, 205 | totalWidth, 206 | leftColWidth, 207 | docsOnNewLine, 208 | customName, 209 | customDoc, 210 | sorted = true, 211 | ) 212 | 213 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 214 | def formatMainMethodSignature( 215 | main: MainData[_, _], 216 | leftIndent: Int, 217 | totalWidth: Int, 218 | leftColWidth: Int, 219 | docsOnNewLine: Boolean, 220 | customName: Option[String], 221 | customDoc: Option[String], 222 | sorted: Boolean 223 | ): String = formatMainMethodSignature( 224 | main, 225 | leftIndent, 226 | totalWidth, 227 | leftColWidth, 228 | docsOnNewLine, 229 | customName, 230 | customDoc, 231 | sorted, 232 | Util.kebabCaseNameMapper 233 | ) 234 | 235 | def softWrap(s: String, leftOffset: Int, maxWidth: Int) = { 236 | if (s.isEmpty) s 237 | else { 238 | val oneLine = s.linesIterator.mkString(" ").split(' ').filter(_.nonEmpty) 239 | 240 | lazy val indent = " " * leftOffset 241 | 242 | val output = new StringBuilder(oneLine.head) 243 | var currentLineWidth = oneLine.head.length 244 | for (chunk <- oneLine.tail) { 245 | val addedWidth = currentLineWidth + chunk.length + 1 246 | if (addedWidth > maxWidth) { 247 | output.append(newLine + indent) 248 | output.append(chunk) 249 | currentLineWidth = chunk.length 250 | } else { 251 | currentLineWidth = addedWidth 252 | output.append(' ') 253 | output.append(chunk) 254 | } 255 | } 256 | output.mkString 257 | } 258 | } 259 | 260 | def pluralize(s: String, n: Int) = if (n == 1) s else s + "s" 261 | def renderEarlyError(result: Result.Failure.Early) = result match { 262 | case Result.Failure.Early.NoMainMethodsDetected() => 263 | "No @main methods declared" 264 | case Result.Failure.Early.SubcommandNotSpecified(options) => 265 | "Need to specify a sub command: " + options.mkString(", ") 266 | case Result.Failure.Early.UnableToFindSubcommand(options, token) => 267 | s"Unable to find subcommand: $token, available subcommands: ${options.mkString(", ")}" 268 | case Result.Failure.Early.SubcommandSelectionDashes(token) => 269 | "To select a subcommand to run, you don't need --s." + Renderer.newLine + 270 | s"Did you mean `${token.drop(2)}` instead of `$token`?" 271 | } 272 | 273 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 274 | def renderResult( 275 | main: MainData[_, _], 276 | result: Result.Failure, 277 | totalWidth: Int, 278 | printHelpOnError: Boolean, 279 | docsOnNewLine: Boolean, 280 | customName: Option[String], 281 | customDoc: Option[String], 282 | sorted: Boolean, 283 | ): String = renderResult( 284 | main, 285 | result, 286 | totalWidth, 287 | printHelpOnError, 288 | docsOnNewLine, 289 | customName, 290 | customDoc, 291 | sorted, 292 | Util.kebabCaseNameMapper 293 | ) 294 | def renderResult( 295 | main: MainData[_, _], 296 | result: Result.Failure, 297 | totalWidth: Int, 298 | printHelpOnError: Boolean, 299 | docsOnNewLine: Boolean, 300 | customName: Option[String], 301 | customDoc: Option[String], 302 | sorted: Boolean, 303 | nameMapper: String => Option[String] 304 | ): String = { 305 | 306 | def expectedMsg() = { 307 | if (printHelpOnError) { 308 | val leftColWidth = getLeftColWidth(main.renderedArgSigs, nameMapper) 309 | "Expected Signature: " + 310 | Renderer.formatMainMethodSignature( 311 | main, 312 | 0, 313 | totalWidth, 314 | leftColWidth, 315 | docsOnNewLine, 316 | customName, 317 | customDoc, 318 | sorted, 319 | nameMapper 320 | ) 321 | } else "" 322 | } 323 | result match { 324 | case err: Result.Failure.Early => renderEarlyError(err) 325 | case Result.Failure.Exception(t) => 326 | val s = new StringWriter() 327 | val ps = new PrintWriter(s) 328 | t.printStackTrace(ps) 329 | ps.close() 330 | s.toString 331 | case Result.Failure.MismatchedArguments(missing, unknown, duplicate, incomplete) => 332 | val missingStr = 333 | if (missing.isEmpty) "" 334 | else { 335 | val chunks = missing.map(renderArgShort(_, nameMapper)) 336 | 337 | val argumentsStr = pluralize("argument", chunks.length) 338 | s"Missing $argumentsStr: ${chunks.mkString(" ")}" + Renderer.newLine 339 | } 340 | 341 | val unknownStr = 342 | if (unknown.isEmpty) "" 343 | else { 344 | val argumentsStr = pluralize("argument", unknown.length) 345 | s"Unknown $argumentsStr: " + unknown.map(Util.literalize(_)).mkString( 346 | " " 347 | ) + Renderer.newLine 348 | } 349 | 350 | val duplicateStr = 351 | if (duplicate.isEmpty) "" 352 | else { 353 | val lines = 354 | for ((sig, options) <- duplicate) 355 | yield { 356 | s"Duplicate arguments for ${renderArgShort(sig, nameMapper)}: " + 357 | options.map(Util.literalize(_)).mkString(" ") + Renderer.newLine 358 | } 359 | 360 | lines.mkString 361 | 362 | } 363 | val incompleteStr = incomplete match { 364 | case None => "" 365 | case Some(sig) => 366 | s"Incomplete argument ${renderArgShort(sig, nameMapper)} is missing a corresponding value" + 367 | Renderer.newLine 368 | 369 | } 370 | 371 | Renderer.normalizeNewlines( 372 | s"""$missingStr$unknownStr$duplicateStr$incompleteStr${expectedMsg()} 373 | |""".stripMargin 374 | ) 375 | 376 | case Result.Failure.InvalidArguments(x) => 377 | val thingies = x.map { 378 | case Result.ParamError.Failed(p, vs, errMsg) => 379 | val literalV = vs.map(Util.literalize(_)).mkString(" ") 380 | s"Invalid argument ${renderArgShort(p, nameMapper)} failed to parse $literalV due to $errMsg" 381 | case Result.ParamError.Exception(p, vs, ex) => 382 | val literalV = vs.map(Util.literalize(_)).mkString(" ") 383 | s"Invalid argument ${renderArgShort(p, nameMapper)} failed to parse $literalV due to $ex" 384 | case Result.ParamError.DefaultFailed(p, ex) => 385 | s"Invalid argument ${renderArgShort(p, nameMapper)}'s default value failed to evaluate with $ex" 386 | } 387 | 388 | Renderer.normalizeNewlines( 389 | s"""${thingies.mkString(Renderer.newLine)} 390 | |${expectedMsg()} 391 | """.stripMargin 392 | ) 393 | } 394 | } 395 | 396 | @deprecated("Use other overload instead", "mainargs after 0.3.0") 397 | def renderResult( 398 | main: MainData[_, _], 399 | result: Result.Failure, 400 | totalWidth: Int, 401 | printHelpOnError: Boolean, 402 | docsOnNewLine: Boolean, 403 | customName: Option[String], 404 | customDoc: Option[String] 405 | ): String = renderResult( 406 | main, 407 | result, 408 | totalWidth, 409 | printHelpOnError, 410 | docsOnNewLine, 411 | customName, 412 | customDoc, 413 | sorted = true 414 | ) 415 | } 416 | -------------------------------------------------------------------------------- /mainargs/src/TokensReader.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | import scala.collection.compat._ 3 | import scala.collection.mutable 4 | 5 | /** 6 | * Represents the ability to parse CLI input arguments into a type [[T]] 7 | * 8 | * Has a fixed number of direct subtypes - [[Simple]], [[Constant]], [[Flag]], 9 | * [[Leftover]], and [[Class]] - but each of those can be extended by an 10 | * arbitrary number of user-specified instances. 11 | */ 12 | sealed trait TokensReader[T] { 13 | def isLeftover = false 14 | def isFlag = false 15 | def isClass = false 16 | def isConstant = false 17 | def isSimple = false 18 | } 19 | 20 | object TokensReader { 21 | 22 | sealed trait Terminal[T] extends TokensReader[T] 23 | 24 | sealed trait ShortNamed[T] extends Terminal[T] { 25 | /** 26 | * The label that shows up in the CLI help message, e.g. the `bar` in 27 | * `--foo ` 28 | */ 29 | def shortName: String 30 | } 31 | 32 | /** 33 | * A [[TokensReader]] for a single CLI parameter that takes a value 34 | * e.g. `--foo bar` 35 | */ 36 | trait Simple[T] extends ShortNamed[T] { 37 | /** 38 | * Converts the given input tokens to a [[T]] or an error `String`. 39 | * The input is a `Seq` because input tokens can be passed more than once, 40 | * e.g. `--foo bar --foo qux` will result in [[read]] being passed 41 | * `["foo", "qux"]` 42 | */ 43 | def read(strs: Seq[String]): Either[String, T] 44 | 45 | /** 46 | * Whether is CLI param is repeatable 47 | */ 48 | def alwaysRepeatable: Boolean = false 49 | 50 | /** 51 | * Whether this CLI param can be no passed from the CLI, even if a default 52 | * value is not specified. In that case, [[read]] receives an empty `Seq` 53 | */ 54 | def allowEmpty: Boolean = false 55 | override def isSimple = true 56 | } 57 | 58 | /** 59 | * A [[TokensReader]] that doesn't read any tokens and just returns a value. 60 | * Useful sometimes for injecting things into main methods that aren't 61 | * strictly computed from CLI argument tokens but nevertheless need to get 62 | * passed in. 63 | */ 64 | trait Constant[T] extends Terminal[T] { 65 | def read(): Either[String, T] 66 | override def isConstant = true 67 | } 68 | 69 | /** 70 | * A [[TokensReader]] for a flag that does not take any value, e.g. `--foo` 71 | */ 72 | trait Flag extends Terminal[mainargs.Flag] { 73 | override def isFlag = true 74 | } 75 | 76 | /** 77 | * A [[TokensReader]] for parsing the left-over parameters that do not belong 78 | * to any other flag or parameter. 79 | */ 80 | trait Leftover[T, V] extends ShortNamed[T] { 81 | def read(strs: Seq[String]): Either[String, T] 82 | 83 | def shortName: String 84 | override def isLeftover = true 85 | } 86 | 87 | /** 88 | * A [[TokensReader]] that can parse an instance of the class [[T]], which 89 | * may contain multiple fields each parsed by their own [[TokensReader]] 90 | */ 91 | trait Class[T] extends TokensReader[T] { 92 | def companion: () => Any 93 | def main: MainData[T, Any] 94 | override def isClass = true 95 | } 96 | 97 | def tryEither[T](f: => T) = 98 | try Right(f) 99 | catch { case e: Throwable => Left(e.toString) } 100 | 101 | implicit object FlagRead extends Flag 102 | implicit object StringRead extends Simple[String] { 103 | def shortName = "str" 104 | def read(strs: Seq[String]) = Right(strs.last) 105 | } 106 | implicit object BooleanRead extends Simple[Boolean] { 107 | def shortName = "bool" 108 | def read(strs: Seq[String]) = tryEither(strs.last.toBoolean) 109 | } 110 | implicit object ByteRead extends Simple[Byte] { 111 | def shortName = "byte" 112 | def read(strs: Seq[String]) = tryEither(strs.last.toByte) 113 | } 114 | implicit object ShortRead extends Simple[Short] { 115 | def shortName = "short" 116 | def read(strs: Seq[String]) = tryEither(strs.last.toShort) 117 | } 118 | implicit object IntRead extends Simple[Int] { 119 | def shortName = "int" 120 | def read(strs: Seq[String]) = tryEither(strs.last.toInt) 121 | } 122 | implicit object LongRead extends Simple[Long] { 123 | def shortName = "long" 124 | def read(strs: Seq[String]) = tryEither(strs.last.toLong) 125 | } 126 | implicit object FloatRead extends Simple[Float] { 127 | def shortName = "float" 128 | def read(strs: Seq[String]) = tryEither(strs.last.toFloat) 129 | } 130 | implicit object DoubleRead extends Simple[Double] { 131 | def shortName = "double" 132 | def read(strs: Seq[String]) = tryEither(strs.last.toDouble) 133 | } 134 | implicit object BigDecimalRead extends Simple[BigDecimal] { 135 | def shortName = "bigdecimal" 136 | def read(strs: Seq[String]) = tryEither(BigDecimal(strs.last)) 137 | } 138 | 139 | implicit def LeftoverRead[T: TokensReader.Simple]: TokensReader.Leftover[mainargs.Leftover[T], T] = 140 | new LeftoverRead[T]()(implicitly[TokensReader.Simple[T]]) 141 | 142 | class LeftoverRead[T](implicit wrapped: TokensReader.Simple[T]) 143 | extends Leftover[mainargs.Leftover[T], T] { 144 | def read(strs: Seq[String]) = { 145 | val (failures, successes) = strs 146 | .map(s => 147 | implicitly[TokensReader[T]] match{ 148 | case r: TokensReader.Simple[T] => r.read(Seq(s)) 149 | case r: TokensReader.Leftover[T, _] => r.read(Seq(s)) 150 | } 151 | ) 152 | .partitionMap(identity) 153 | 154 | if (failures.nonEmpty) Left(failures.head) 155 | else Right(Leftover(successes: _*)) 156 | } 157 | def shortName = wrapped.shortName 158 | } 159 | 160 | implicit def OptionRead[T: TokensReader.Simple]: TokensReader[Option[T]] = new OptionRead[T] 161 | class OptionRead[T: TokensReader.Simple] extends Simple[Option[T]] { 162 | def shortName = implicitly[TokensReader.Simple[T]].shortName 163 | def read(strs: Seq[String]) = { 164 | strs.lastOption match { 165 | case None => Right(None) 166 | case Some(s) => implicitly[TokensReader.Simple[T]].read(Seq(s)) match { 167 | case Left(s) => Left(s) 168 | case Right(s) => Right(Some(s)) 169 | } 170 | } 171 | } 172 | override def allowEmpty = true 173 | } 174 | 175 | implicit def SeqRead[C[_] <: Iterable[_], T: TokensReader.Simple](implicit 176 | factory: Factory[T, C[T]] 177 | ): TokensReader[C[T]] = 178 | new SeqRead[C, T] 179 | 180 | class SeqRead[C[_] <: Iterable[_], T: TokensReader.Simple](implicit factory: Factory[T, C[T]]) 181 | extends Simple[C[T]] { 182 | def shortName = implicitly[TokensReader.Simple[T]].shortName 183 | def read(strs: Seq[String]) = { 184 | strs 185 | .foldLeft(Right(factory.newBuilder): Either[String, mutable.Builder[T, C[T]]]) { 186 | case (Left(s), _) => Left(s) 187 | case (Right(builder), token) => 188 | implicitly[TokensReader.Simple[T]].read(Seq(token)) match { 189 | case Left(s) => Left(s) 190 | case Right(v) => 191 | builder += v 192 | Right(builder) 193 | } 194 | } 195 | .map(_.result()) 196 | } 197 | override def alwaysRepeatable = true 198 | override def allowEmpty = true 199 | } 200 | 201 | implicit def MapRead[K: TokensReader.Simple, V: TokensReader.Simple]: TokensReader[Map[K, V]] = 202 | new MapRead[K, V] 203 | class MapRead[K: TokensReader.Simple, V: TokensReader.Simple] extends Simple[Map[K, V]] { 204 | def shortName = "k=v" 205 | def read(strs: Seq[String]) = { 206 | strs.foldLeft[Either[String, Map[K, V]]](Right(Map())) { 207 | case (Left(s), _) => Left(s) 208 | case (Right(prev), token) => 209 | token.split("=", 2) match { 210 | case Array(k, v) => 211 | for { 212 | tuple <- Right((k, v)): Either[String, (String, String)] 213 | (k, v) = tuple 214 | key <- implicitly[TokensReader.Simple[K]].read(Seq(k)) 215 | value <- implicitly[TokensReader.Simple[V]].read(Seq(v)) 216 | } yield prev + (key -> value) 217 | 218 | case _ => Left("parameter must be in k=v format") 219 | } 220 | } 221 | } 222 | override def alwaysRepeatable = true 223 | override def allowEmpty = true 224 | } 225 | } 226 | 227 | object ArgSig { 228 | def create[T, B](name0: String, arg: mainargs.arg, defaultOpt: Option[B => T]) 229 | (implicit tokensReader: TokensReader[T]): ArgSig = { 230 | val shortOpt = arg.short match { 231 | case '\u0000' => if (name0.length != 1 || arg.noDefaultName) None else Some(name0(0)); 232 | case c => Some(c) 233 | } 234 | 235 | val docOpt = scala.Option(arg.doc) 236 | new ArgSig( 237 | if (arg.noDefaultName || name0.length == 1) None else Some(name0), 238 | scala.Option(arg.name), 239 | shortOpt, 240 | docOpt, 241 | defaultOpt.asInstanceOf[Option[Any => Any]], 242 | tokensReader, 243 | arg.positional, 244 | arg.hidden 245 | ) 246 | } 247 | 248 | def flatten[T](x: ArgSig): Seq[(ArgSig, TokensReader.Terminal[_])] = x.reader match { 249 | case r: TokensReader.Terminal[T] => Seq((x, r)) 250 | case cls: TokensReader.Class[_] => cls.main.argSigs0.flatMap(flatten(_)) 251 | } 252 | 253 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 254 | def apply(unMappedName: Option[String], 255 | shortName: Option[Char], 256 | doc: Option[String], 257 | default: Option[Any => Any], 258 | reader: TokensReader[_], 259 | positional: Boolean, 260 | hidden: Boolean) = { 261 | 262 | new ArgSig(unMappedName, unMappedName, shortName, doc, default, reader, positional, hidden) 263 | } 264 | 265 | def unapply(a: ArgSig) = Option( 266 | (a.unMappedName, a.shortName, a.doc, a.default, a.reader, a.positional, a.hidden) 267 | ) 268 | } 269 | 270 | /** 271 | * Models what is known by the router about a single argument: that it has 272 | * a [[longName]], a human-readable [[typeString]] describing what the type is 273 | * (just for logging and reading, not a replacement for a `TypeTag`) and 274 | * possible a function that can compute its default value 275 | */ 276 | class ArgSig private[mainargs] (val defaultLongName: Option[String], 277 | val argName: Option[String], 278 | val shortName: Option[Char], 279 | val doc: Option[String], 280 | val default: Option[Any => Any], 281 | val reader: TokensReader[_], 282 | val positional: Boolean, 283 | val hidden: Boolean 284 | ) extends Product with Serializable with Equals{ 285 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 286 | def name = defaultLongName 287 | override def canEqual(that: Any): Boolean = true 288 | 289 | override def hashCode(): Int = ArgSig.unapply(this).hashCode() 290 | override def equals(o: Any): Boolean = o match { 291 | case other: ArgSig => ArgSig.unapply(this) == ArgSig.unapply(other) 292 | case _ => false 293 | } 294 | 295 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 296 | def this(unmappedName: Option[String], 297 | shortName: Option[Char], 298 | doc: Option[String], 299 | default: Option[Any => Any], 300 | reader: TokensReader[_], 301 | positional: Boolean, 302 | hidden: Boolean) = { 303 | this(unmappedName, unmappedName, shortName, doc, default, reader, positional, hidden) 304 | } 305 | 306 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 307 | def copy(unMappedName: Option[String] = this.unMappedName, 308 | shortName: Option[Char] = this.shortName, 309 | doc: Option[String] = this.doc, 310 | default: Option[Any => Any] = this.default, 311 | reader: TokensReader[_] = this.reader, 312 | positional: Boolean = this.positional, 313 | hidden: Boolean = this.hidden) = { 314 | ArgSig(unMappedName, shortName, doc, default, reader, positional, hidden) 315 | } 316 | 317 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 318 | def productArity = 9 319 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 320 | def productElement(n: Int) = n match{ 321 | case 0 => defaultLongName 322 | case 1 => argName 323 | case 2 => shortName 324 | case 3 => doc 325 | case 4 => default 326 | case 5 => reader 327 | case 6 => positional 328 | case 7 => hidden 329 | } 330 | 331 | def unMappedName: Option[String] = argName.orElse(defaultLongName) 332 | def longName(nameMapper: String => Option[String]): Option[String] = argName.orElse(mappedName(nameMapper)).orElse(defaultLongName) 333 | def mappedName(nameMapper: String => Option[String]): Option[String] = 334 | if (argName.isDefined) None else defaultLongName.flatMap(nameMapper) 335 | } 336 | 337 | 338 | case class MethodMains[B](value: Seq[MainData[Any, B]], base: () => B) 339 | 340 | /** 341 | * What is known about a single endpoint for our routes. It has a [[name]], 342 | * [[flattenedArgSigs]] for each argument, and a macro-generated [[invoke0]] 343 | * that performs all the necessary argument parsing and de-serialization. 344 | * 345 | * Realistically, you will probably spend most of your time calling [[Invoker.invoke]] 346 | * instead, which provides a nicer API to call it that mimmicks the API of 347 | * calling a Scala method. 348 | */ 349 | class MainData[T, B] private[mainargs] ( 350 | val mainName: Option[String], 351 | val defaultName: String, 352 | val argSigs0: Seq[ArgSig], 353 | val doc: Option[String], 354 | val invokeRaw: (B, Seq[Any]) => T 355 | ) extends Product with Serializable with Equals{ 356 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 357 | def name = mainName.getOrElse(defaultName) 358 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 359 | def productArity = 5 360 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 361 | def productElement(n: Int) = n match{ 362 | case 0 => mainName 363 | case 1 => defaultName 364 | case 2 => argSigs0 365 | case 3 => doc 366 | case 4 => invokeRaw 367 | } 368 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 369 | def copy(name: String = this.unmappedName, 370 | argSigs0: Seq[ArgSig] = this.argSigs0, 371 | doc: Option[String] = this.doc, 372 | invokeRaw: (B, Seq[Any]) => T = this.invokeRaw) = MainData( 373 | name, argSigs0, doc, invokeRaw 374 | ) 375 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 376 | def this(name: String, 377 | argSigs0: Seq[ArgSig], 378 | doc: Option[String], 379 | invokeRaw: (B, Seq[Any]) => T) = this( 380 | Some(name), name, argSigs0, doc, invokeRaw 381 | ) 382 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 383 | override def hashCode(): Int = MainData.unapply(this).hashCode() 384 | 385 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 386 | override def equals(obj: Any): Boolean = obj match{ 387 | case x: MainData[_, _] => MainData.unapply(x) == MainData.unapply(this) 388 | case _ => false 389 | } 390 | 391 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 392 | override def canEqual(that: Any): Boolean = true 393 | 394 | def unmappedName: String = mainName.getOrElse(defaultName) 395 | 396 | def name(nameMapper: String => Option[String]) = mainName.orElse(mappedName(nameMapper)).getOrElse(defaultName) 397 | def mappedName(nameMapper: String => Option[String]): Option[String] = 398 | if (mainName.isDefined) None 399 | else nameMapper(defaultName) 400 | 401 | val flattenedArgSigs: Seq[(ArgSig, TokensReader.Terminal[_])] = 402 | argSigs0.iterator.flatMap[(ArgSig, TokensReader.Terminal[_])](ArgSig.flatten(_)).toVector 403 | 404 | val renderedArgSigs: Seq[ArgSig] = 405 | flattenedArgSigs.collect{case (a, r) if !a.hidden && !r.isConstant => a} 406 | } 407 | 408 | object MainData { 409 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 410 | def unapply[T, B](x: MainData[T, B]) = Option((x.mainName, x.defaultName, x.argSigs0, x.doc, x.invokeRaw)) 411 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 412 | def apply[T, B](name: String, 413 | argSigs0: Seq[ArgSig], 414 | doc: Option[String], 415 | invokeRaw: (B, Seq[Any]) => T) = { 416 | new MainData(Some(name), name, argSigs0, doc, invokeRaw) 417 | } 418 | def create[T, B]( 419 | methodName: String, 420 | main: mainargs.main, 421 | argSigs: Seq[ArgSig], 422 | invokeRaw: (B, Seq[Any]) => T 423 | ) = { 424 | new MainData( 425 | Option(main.name), 426 | methodName, 427 | argSigs, 428 | Option(main.doc), 429 | invokeRaw 430 | ) 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /mainargs/src/Parser.scala: -------------------------------------------------------------------------------- 1 | package mainargs 2 | 3 | import acyclic.skipped 4 | 5 | import scala.language.experimental.macros 6 | import java.io.PrintStream 7 | 8 | object Parser extends ParserForMethodsCompanionVersionSpecific with ParserForClassCompanionVersionSpecific 9 | object ParserForMethods extends ParserForMethodsCompanionVersionSpecific 10 | class ParserForMethods[B](val mains: MethodMains[B]) { 11 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 12 | def helpText( 13 | totalWidth: Int, 14 | docsOnNewLine: Boolean, 15 | customNames: Map[String, String], 16 | customDocs: Map[String, String], 17 | sorted: Boolean): String = { 18 | helpText(totalWidth, docsOnNewLine, customNames, customDocs, sorted, Util.kebabCaseNameMapper) 19 | } 20 | 21 | def helpText( 22 | totalWidth: Int = 100, 23 | docsOnNewLine: Boolean = false, 24 | customNames: Map[String, String] = Map(), 25 | customDocs: Map[String, String] = Map(), 26 | sorted: Boolean = true, 27 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 28 | ): String = { 29 | Renderer.formatMainMethods( 30 | mains.value, 31 | totalWidth, 32 | docsOnNewLine, 33 | customNames, 34 | customDocs, 35 | sorted, 36 | nameMapper 37 | ) 38 | } 39 | 40 | @deprecated("Binary compatibility shim, use other overload instead", "mainargs after 0.3.0") 41 | private[mainargs] def helpText( 42 | totalWidth: Int, 43 | docsOnNewLine: Boolean, 44 | customNames: Map[String, String], 45 | customDocs: Map[String, String] 46 | ): String = helpText(totalWidth, docsOnNewLine, customNames, customDocs, sorted = true) 47 | 48 | def runOrExit( 49 | args: Seq[String], 50 | allowPositional: Boolean = false, 51 | allowRepeats: Boolean = false, 52 | stderr: PrintStream = System.err, 53 | totalWidth: Int = 100, 54 | printHelpOnExit: Boolean = true, 55 | docsOnNewLine: Boolean = false, 56 | autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), 57 | customNames: Map[String, String] = Map(), 58 | customDocs: Map[String, String] = Map(), 59 | sorted: Boolean = true, 60 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 61 | ): Any = { 62 | runEither( 63 | args, 64 | allowPositional, 65 | allowRepeats, 66 | totalWidth, 67 | printHelpOnExit, 68 | docsOnNewLine, 69 | autoPrintHelpAndExit, 70 | customNames, 71 | customDocs, 72 | sorted, 73 | nameMapper 74 | ) match { 75 | case Left(msg) => 76 | stderr.println(msg) 77 | Compat.exit(1) 78 | case Right(v) => v 79 | } 80 | } 81 | 82 | def runOrExit( 83 | args: Seq[String], 84 | allowPositional: Boolean, 85 | allowRepeats: Boolean, 86 | stderr: PrintStream, 87 | totalWidth: Int, 88 | printHelpOnExit: Boolean, 89 | docsOnNewLine: Boolean, 90 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 91 | customNames: Map[String, String], 92 | customDocs: Map[String, String] 93 | ): Any = { 94 | runOrExit( 95 | args, 96 | allowPositional, 97 | allowRepeats, 98 | stderr, 99 | totalWidth, 100 | printHelpOnExit, 101 | docsOnNewLine, 102 | autoPrintHelpAndExit, 103 | customNames, 104 | customDocs, 105 | sorted = true, 106 | Util.kebabCaseNameMapper 107 | ) 108 | } 109 | 110 | def runOrThrow( 111 | args: Seq[String], 112 | allowPositional: Boolean, 113 | allowRepeats: Boolean, 114 | totalWidth: Int, 115 | printHelpOnExit: Boolean, 116 | docsOnNewLine: Boolean, 117 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 118 | customNames: Map[String, String], 119 | customDocs: Map[String, String], 120 | ): Any = runOrThrow( 121 | args, 122 | allowPositional, 123 | allowRepeats, 124 | totalWidth, 125 | printHelpOnExit, 126 | docsOnNewLine, 127 | autoPrintHelpAndExit, 128 | customNames, 129 | customDocs, 130 | Util.kebabCaseNameMapper 131 | ) 132 | 133 | def runOrThrow( 134 | args: Seq[String], 135 | allowPositional: Boolean = false, 136 | allowRepeats: Boolean = false, 137 | totalWidth: Int = 100, 138 | printHelpOnExit: Boolean = true, 139 | docsOnNewLine: Boolean = false, 140 | autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), 141 | customNames: Map[String, String] = Map(), 142 | customDocs: Map[String, String] = Map(), 143 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 144 | ): Any = { 145 | runEither( 146 | args, 147 | allowPositional, 148 | allowRepeats, 149 | totalWidth, 150 | printHelpOnExit, 151 | docsOnNewLine, 152 | autoPrintHelpAndExit, 153 | customNames, 154 | customDocs, 155 | nameMapper = nameMapper 156 | ) match { 157 | case Left(msg) => throw new Exception(msg) 158 | case Right(v) => v 159 | } 160 | } 161 | 162 | def runEither( 163 | args: Seq[String], 164 | allowPositional: Boolean = false, 165 | allowRepeats: Boolean = false, 166 | totalWidth: Int = 100, 167 | printHelpOnExit: Boolean = true, 168 | docsOnNewLine: Boolean = false, 169 | autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), 170 | customNames: Map[String, String] = Map(), 171 | customDocs: Map[String, String] = Map(), 172 | sorted: Boolean = true, 173 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 174 | ): Either[String, Any] = { 175 | if (autoPrintHelpAndExit.nonEmpty && args.take(1) == Seq("--help")) { 176 | val (exitCode, outputStream) = autoPrintHelpAndExit.get 177 | outputStream.println(helpText(totalWidth, docsOnNewLine, customNames, customDocs, sorted, nameMapper)) 178 | Compat.exit(exitCode) 179 | } else runRaw0(args, allowPositional, allowRepeats, nameMapper) match { 180 | case Left(err) => Left(Renderer.renderEarlyError(err)) 181 | case Right((main, res)) => 182 | res match { 183 | case Result.Success(v) => Right(v) 184 | case f: Result.Failure => 185 | Left( 186 | Renderer.renderResult( 187 | main, 188 | f, 189 | totalWidth, 190 | printHelpOnExit, 191 | docsOnNewLine, 192 | customNames.get(main.name(nameMapper)), 193 | customDocs.get(main.name(nameMapper)), 194 | sorted, 195 | nameMapper 196 | ) 197 | ) 198 | } 199 | } 200 | } 201 | 202 | @deprecated("Binary compatibility shim, use other overload instead", "mainargs after 0.3.0") 203 | private[mainargs] def runEither( 204 | args: Seq[String], 205 | allowPositional: Boolean, 206 | allowRepeats: Boolean, 207 | totalWidth: Int, 208 | printHelpOnExit: Boolean, 209 | docsOnNewLine: Boolean, 210 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 211 | customNames: Map[String, String], 212 | customDocs: Map[String, String] 213 | ): Either[String, Any] = runEither( 214 | args, 215 | allowPositional, 216 | allowRepeats, 217 | totalWidth, 218 | printHelpOnExit, 219 | docsOnNewLine, 220 | autoPrintHelpAndExit, 221 | customNames, 222 | customDocs, 223 | sorted = false 224 | ) 225 | 226 | 227 | 228 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 229 | def runEither( 230 | args: Seq[String], 231 | allowPositional: Boolean, 232 | allowRepeats: Boolean, 233 | totalWidth: Int, 234 | printHelpOnExit: Boolean, 235 | docsOnNewLine: Boolean, 236 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 237 | customNames: Map[String, String], 238 | customDocs: Map[String, String], 239 | sorted: Boolean 240 | ): Either[String, Any] = runEither( 241 | args, 242 | allowPositional, 243 | allowRepeats, 244 | totalWidth, 245 | printHelpOnExit, 246 | docsOnNewLine, 247 | autoPrintHelpAndExit, 248 | customNames, 249 | customDocs, 250 | sorted, 251 | Util.kebabCaseNameMapper 252 | ) 253 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 254 | def runRaw( 255 | args: Seq[String], 256 | allowPositional: Boolean, 257 | allowRepeats: Boolean, 258 | ): Result[Any] = runRaw( 259 | args, allowPositional, allowRepeats, Util.kebabCaseNameMapper 260 | ) 261 | def runRaw( 262 | args: Seq[String], 263 | allowPositional: Boolean = false, 264 | allowRepeats: Boolean = false, 265 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 266 | ): Result[Any] = { 267 | runRaw0(args, allowPositional, allowRepeats, nameMapper) match { 268 | case Left(err) => err 269 | case Right((main, res)) => res 270 | } 271 | } 272 | 273 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 274 | def runRaw0( 275 | args: Seq[String], 276 | allowPositional: Boolean, 277 | allowRepeats: Boolean, 278 | ): Either[Result.Failure.Early, (MainData[_, B], Result[Any])] = runRaw0( 279 | args, 280 | allowPositional, 281 | allowRepeats, 282 | Util.kebabCaseNameMapper 283 | ) 284 | 285 | def runRaw0( 286 | args: Seq[String], 287 | allowPositional: Boolean = false, 288 | allowRepeats: Boolean = false, 289 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 290 | ): Either[Result.Failure.Early, (MainData[_, B], Result[Any])] = { 291 | for (tuple <- Invoker.runMains(mains, args, allowPositional, allowRepeats, nameMapper)) yield { 292 | val (errMsg, res) = tuple 293 | (errMsg, res) 294 | } 295 | } 296 | } 297 | 298 | object ParserForClass extends ParserForClassCompanionVersionSpecific 299 | class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) 300 | extends TokensReader.Class[T] { 301 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 302 | def helpText( 303 | totalWidth: Int, 304 | docsOnNewLine: Boolean, 305 | customName: String, 306 | customDoc: String, 307 | sorted: Boolean): String = helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted, Util.kebabCaseNameMapper) 308 | 309 | def helpText( 310 | totalWidth: Int = 100, 311 | docsOnNewLine: Boolean = false, 312 | customName: String = null, 313 | customDoc: String = null, 314 | sorted: Boolean = true, 315 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 316 | ): String = { 317 | Renderer.formatMainMethodSignature( 318 | main, 319 | 0, 320 | totalWidth, 321 | Renderer.getLeftColWidth(main.renderedArgSigs, nameMapper), 322 | docsOnNewLine, 323 | Option(customName), 324 | Option(customDoc), 325 | sorted, 326 | nameMapper 327 | ) 328 | } 329 | 330 | @deprecated("Binary compatibility shim, use other overload instead", "mainargs after 0.3.0") 331 | private[mainargs] def helpText( 332 | totalWidth: Int, 333 | docsOnNewLine: Boolean, 334 | customName: String, 335 | customDoc: String 336 | ): String = helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted = true) 337 | 338 | @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") 339 | def constructOrExit( 340 | args: Seq[String], 341 | allowPositional: Boolean, 342 | allowRepeats: Boolean, 343 | stderr: PrintStream, 344 | totalWidth: Int, 345 | printHelpOnExit: Boolean, 346 | docsOnNewLine: Boolean, 347 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 348 | customName: String, 349 | customDoc: String): T = constructOrExit( 350 | args, 351 | allowPositional, 352 | allowRepeats, 353 | stderr, 354 | totalWidth, 355 | printHelpOnExit, 356 | docsOnNewLine, 357 | autoPrintHelpAndExit, 358 | customName, 359 | customDoc, 360 | Util.kebabCaseNameMapper 361 | ) 362 | 363 | def constructOrExit( 364 | args: Seq[String], 365 | allowPositional: Boolean = false, 366 | allowRepeats: Boolean = false, 367 | stderr: PrintStream = System.err, 368 | totalWidth: Int = 100, 369 | printHelpOnExit: Boolean = true, 370 | docsOnNewLine: Boolean = false, 371 | autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), 372 | customName: String = null, 373 | customDoc: String = null, 374 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 375 | ): T = { 376 | constructEither( 377 | args, 378 | allowPositional, 379 | allowRepeats, 380 | totalWidth, 381 | printHelpOnExit, 382 | docsOnNewLine, 383 | autoPrintHelpAndExit, 384 | customName, 385 | customDoc, 386 | nameMapper 387 | ) match { 388 | case Left(msg) => 389 | stderr.println(msg) 390 | Compat.exit(1) 391 | case Right(v) => v 392 | } 393 | } 394 | 395 | def constructOrThrow( 396 | args: Seq[String], 397 | allowPositional: Boolean, 398 | allowRepeats: Boolean, 399 | totalWidth: Int, 400 | printHelpOnExit: Boolean, 401 | docsOnNewLine: Boolean, 402 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 403 | customName: String, 404 | customDoc: String, 405 | ): T = constructOrThrow( 406 | args, 407 | allowPositional, 408 | allowRepeats, 409 | totalWidth, 410 | printHelpOnExit, 411 | docsOnNewLine, 412 | autoPrintHelpAndExit, 413 | customName, 414 | customDoc, 415 | Util.kebabCaseNameMapper 416 | ) 417 | 418 | def constructOrThrow( 419 | args: Seq[String], 420 | allowPositional: Boolean = false, 421 | allowRepeats: Boolean = false, 422 | totalWidth: Int = 100, 423 | printHelpOnExit: Boolean = true, 424 | docsOnNewLine: Boolean = false, 425 | autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), 426 | customName: String = null, 427 | customDoc: String = null, 428 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 429 | ): T = { 430 | constructEither( 431 | args, 432 | allowPositional, 433 | allowRepeats, 434 | totalWidth, 435 | printHelpOnExit, 436 | docsOnNewLine, 437 | autoPrintHelpAndExit, 438 | customName, 439 | customDoc, 440 | nameMapper 441 | ) match { 442 | case Left(msg) => throw new Exception(msg) 443 | case Right(v) => v 444 | } 445 | } 446 | 447 | def constructEither( 448 | args: Seq[String], 449 | allowPositional: Boolean, 450 | allowRepeats: Boolean, 451 | totalWidth: Int, 452 | printHelpOnExit: Boolean, 453 | docsOnNewLine: Boolean, 454 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 455 | customName: String, 456 | customDoc: String, 457 | sorted: Boolean, 458 | ): Either[String, T] = constructEither( 459 | args, 460 | allowPositional, 461 | allowRepeats, 462 | totalWidth, 463 | printHelpOnExit, 464 | docsOnNewLine, 465 | autoPrintHelpAndExit, 466 | customName, 467 | customDoc, 468 | sorted, 469 | Util.kebabCaseNameMapper 470 | ) 471 | def constructEither( 472 | args: Seq[String], 473 | allowPositional: Boolean = false, 474 | allowRepeats: Boolean = false, 475 | totalWidth: Int = 100, 476 | printHelpOnExit: Boolean = true, 477 | docsOnNewLine: Boolean = false, 478 | autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), 479 | customName: String = null, 480 | customDoc: String = null, 481 | sorted: Boolean = true, 482 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 483 | ): Either[String, T] = { 484 | if (autoPrintHelpAndExit.nonEmpty && args.take(1) == Seq("--help")) { 485 | val (exitCode, outputStream) = autoPrintHelpAndExit.get 486 | outputStream.println(helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted)) 487 | Compat.exit(exitCode) 488 | } else constructRaw(args, allowPositional, allowRepeats, nameMapper) match { 489 | case Result.Success(v) => Right(v) 490 | case f: Result.Failure => 491 | Left( 492 | Renderer.renderResult( 493 | main, 494 | f, 495 | totalWidth, 496 | printHelpOnExit, 497 | docsOnNewLine, 498 | Option(customName), 499 | Option(customDoc), 500 | sorted, 501 | nameMapper 502 | ) 503 | ) 504 | } 505 | } 506 | 507 | /** binary compatibility shim. */ 508 | private[mainargs] def constructEither( 509 | args: Seq[String], 510 | allowPositional: Boolean, 511 | allowRepeats: Boolean, 512 | totalWidth: Int, 513 | printHelpOnExit: Boolean, 514 | docsOnNewLine: Boolean, 515 | autoPrintHelpAndExit: Option[(Int, PrintStream)], 516 | customName: String, 517 | customDoc: String, 518 | nameMapper: String => Option[String] 519 | ): Either[String, T] = constructEither( 520 | args, 521 | allowPositional, 522 | allowRepeats, 523 | totalWidth, 524 | printHelpOnExit, 525 | docsOnNewLine, 526 | autoPrintHelpAndExit, 527 | customName, 528 | customDoc, 529 | sorted = true, 530 | nameMapper = nameMapper 531 | ) 532 | 533 | def constructRaw( 534 | args: Seq[String], 535 | allowPositional: Boolean, 536 | allowRepeats: Boolean, 537 | ): Result[T] = constructRaw( 538 | args, 539 | allowPositional, 540 | allowRepeats, 541 | nameMapper = Util.kebabCaseNameMapper 542 | ) 543 | 544 | def constructRaw( 545 | args: Seq[String], 546 | allowPositional: Boolean = false, 547 | allowRepeats: Boolean = false, 548 | nameMapper: String => Option[String] = Util.kebabCaseNameMapper 549 | ): Result[T] = { 550 | Invoker.construct[T](this, args, allowPositional, allowRepeats, nameMapper) 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mainargs 0.7.7 2 | 3 | MainArgs is a small, dependency-free library for command line argument parsing 4 | in Scala. 5 | 6 | MainArgs is used for command-line parsing of the 7 | [Ammonite Scala REPL](http://ammonite.io/) and for user-defined `@main` methods 8 | in its scripts, as well as for command-line parsing for the 9 | [Mill Build Tool](https://github.com/lihaoyi/mill) and for user-defined 10 | `T.command`s. 11 | 12 | - [mainargs](#mainargs) 13 | - [Usage](#usage) 14 | - [Parsing Main Method Parameters](#parsing-main-method-parameters) 15 | - [runOrExit](#runorexit) 16 | - [runOrThrow](#runorthrow) 17 | - [runEither](#runeither) 18 | - [runRaw](#runraw) 19 | - [Multiple Main Methods](#multiple-main-methods) 20 | - [Parsing Case Class Parameters](#parsing-case-class-parameters) 21 | - [Re-using Argument Sets](#re-using-argument-sets) 22 | - [Option or Sequence parameters](#option-or-sequence-parameters) 23 | - [Short Arguments](#short-arguments) 24 | - [Annotations](#annotations) 25 | - [@main](#main) 26 | - [@arg](#arg) 27 | - [Customization](#customization) 28 | - [Custom Argument Parsers](#custom-argument-parsers) 29 | - [Handlings Leftover Arguments](#handlings-leftover-arguments) 30 | - [Varargs Parameters](#varargs-parameters) 31 | - [Prior Art](#prior-art) 32 | - [Ammonite & Mill](#ammonite--mill) 33 | - [Case App](#case-app) 34 | - [Scopt](#scopt) 35 | - [Changelog](#changelog) 36 | - [Scaladoc](https://javadoc.io/doc/com.lihaoyi/mainargs_2.13/latest/mainargs/index.html) 37 | 38 | # Usage 39 | 40 | ```scala 41 | ivy"com.lihaoyi::mainargs:0.7.7" 42 | ``` 43 | 44 | ## Parsing Main Method Parameters 45 | 46 | You can parse command line arguments and use them to call a main method via 47 | `ParserForMethods(...)`: 48 | 49 | ```scala 50 | package testhello 51 | import mainargs.{main, arg, ParserForMethods, Flag} 52 | 53 | object Main{ 54 | @main 55 | def run(@arg(short = 'f', doc = "String to print repeatedly") 56 | foo: String, 57 | @arg(doc = "How many times to print string") 58 | myNum: Int = 2, 59 | @arg(doc = "Example flag, can be passed without any value to become true") 60 | bool: Flag) = { 61 | println(foo * myNum + " " + bool.value) 62 | } 63 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 64 | } 65 | ``` 66 | 67 | ```bash 68 | $ ./mill example.hello -f hello # short name 69 | hellohello false 70 | 71 | $ ./mill example.hello --foo hello # long name 72 | hellohello false 73 | 74 | $ ./mill example.hello --foo=hello # gflags-style 75 | hellohello false 76 | 77 | $ ./mill example.hello --foo "" # set to empty value 78 | false 79 | 80 | $ ./mill example.hello --foo= # gflags-style empty value 81 | false 82 | 83 | $ ./mill example.hello -f x --my-num 3 # camelCase automatically converted to kebab-case 84 | xxx false 85 | 86 | $ ./mill example.hello -f hello --my-num 3 --bool # flags 87 | hellohellohello true 88 | 89 | $ ./mill example.hello --wrong-flag 90 | Missing argument: --foo 91 | Unknown argument: "--wrong-flag" 92 | Expected Signature: run 93 | -f --foo String to print repeatedly 94 | --my-num How many times to print string 95 | --bool Example flag 96 | ``` 97 | 98 | Setting default values for the method arguments makes them optional, with the 99 | default value being used if an explicit value was not passed in from the 100 | command-line arguments list. 101 | 102 | After calling `Parser(...)` on the `object` containing your `@main` 103 | methods, you can call the following methods to perform the argument parsing and 104 | dispatch: 105 | 106 | ### runOrExit 107 | 108 | Runs the given main method if argument parsing succeeds, otherwise prints out 109 | the help text to standard error and calls `System.exit(1)` to exit the process 110 | 111 | ### runOrThrow 112 | 113 | Runs the given main method if argument parsing succeeds, otherwise throws an 114 | exception with the help text 115 | 116 | ### runEither 117 | 118 | Runs the given main method if argument parsing succeeds, returning `Right(v: Any)` containing the return value of the main method if it succeeds, or `Left(s: String)` containing the error message if it fails. 119 | 120 | ### runRaw 121 | 122 | Runs the given main method if argument parsing succeeds, returning 123 | `mainargs.Result.Success(v: Any)` containing the return value of the main method 124 | if it succeeds, or `mainargs.Result.Error` if it fails. This gives you the 125 | greatest flexibility to handle the error cases with custom logic, e.g. if you do 126 | not like the default CLI error reporting and would like to write your own. 127 | 128 | ## Multiple Main Methods 129 | 130 | Programs with multiple entrypoints are supported by annotating multiple `def`s 131 | with `@main`. Each entrypoint can have their own set of arguments: 132 | 133 | ```scala 134 | package testhello2 135 | import mainargs.{main, arg, Parser, Flag} 136 | 137 | object Main{ 138 | @main 139 | def foo(@arg(short = 'f', doc = "String to print repeatedly") 140 | foo: String, 141 | @arg(doc = "How many times to print string") 142 | myNum: Int = 2, 143 | @arg(doc = "Example flag") 144 | bool: Flag) = { 145 | println(foo * myNum + " " + bool.value) 146 | } 147 | @main 148 | def bar(i: Int, 149 | @arg(doc = "Pass in a custom `s` to override it") 150 | s: String = "lols") = { 151 | println(s * i) 152 | } 153 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 154 | } 155 | ``` 156 | 157 | ```bash 158 | $ ./mill example.hello2 159 | Need to specify a sub command: foo, bar 160 | 161 | $ ./mill example.hello2 foo -f hello 162 | hellohello false 163 | 164 | $ ./mill example.hello2 bar -i 10 165 | lolslolslolslolslolslolslolslolslolslols 166 | ``` 167 | 168 | ## Parsing Case Class Parameters 169 | 170 | If you want to construct a configuration object instead of directly calling a 171 | method, you can do so via `Parser[T]` and `constructOrExit: 172 | 173 | ```scala 174 | package testclass 175 | import mainargs.{main, arg, Parser, Flag} 176 | 177 | object Main{ 178 | @main 179 | case class Config(@arg(short = 'f', doc = "String to print repeatedly") 180 | foo: String, 181 | @arg(doc = "How many times to print string") 182 | myNum: Int = 2, 183 | @arg(doc = "Example flag") 184 | bool: Flag) 185 | def main(args: Array[String]): Unit = { 186 | val config = Parser[Config].constructOrExit(args) 187 | println(config) 188 | } 189 | } 190 | ``` 191 | 192 | ```bash 193 | $ ./mill example.caseclass --foo "hello" 194 | Config(hello,2,Flag(false)) 195 | 196 | $ ./mill example.caseclass 197 | Missing argument: --foo 198 | Expected Signature: apply 199 | -f --foo String to print repeatedly 200 | --my-num How many times to print string 201 | --bool Example flag 202 | ``` 203 | 204 | `Parser[T]` also provides corresponding `constructOrThrow`, 205 | `constructEither`, or `constructRaw` methods for you to handle the error cases 206 | in whichever style you prefer. 207 | 208 | ## Re-using Argument Sets 209 | 210 | You can share arguments between different `@main` methods by defining them in a 211 | `@main case class` configuration object with an implicit `Parser[T]` 212 | defined: 213 | 214 | ```scala 215 | package testclassarg 216 | import mainargs.{main, arg, Parser, Parser, Flag} 217 | 218 | object Main{ 219 | @main 220 | case class Config(@arg(short = 'f', doc = "String to print repeatedly") 221 | foo: String, 222 | @arg(doc = "How many times to print string") 223 | myNum: Int = 2, 224 | @arg(doc = "Example flag") 225 | bool: Flag) 226 | implicit def configParser = Parser[Config] 227 | 228 | @main 229 | def bar(config: Config, 230 | @arg(name = "extra-message") 231 | extraMessage: String) = { 232 | println(config.foo * config.myNum + " " + config.bool.value + " " + extraMessage) 233 | } 234 | @main 235 | def qux(config: Config, 236 | n: Int) = { 237 | println((config.foo * config.myNum + " " + config.bool.value + "\n") * n) 238 | } 239 | 240 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 241 | } 242 | ``` 243 | 244 | ```bash 245 | 246 | $ ./mill example.classarg bar --foo cow --extra-message "hello world" 247 | cowcow false hello world 248 | 249 | $ ./mill example.classarg qux --foo cow --n 5 250 | cowcow false 251 | cowcow false 252 | cowcow false 253 | cowcow false 254 | cowcow false 255 | ``` 256 | 257 | This allows you to re-use common command-line parsing configuration without 258 | needing to duplicate it in every `@main` method in which it is needed. A `@main def` can make use of multiple `@main case class`es, and `@main case class`es can 259 | be nested arbitrarily deeply. 260 | 261 | ## Option or Sequence parameters 262 | 263 | `@main` method parameters can be `Option[T]` or `Seq[T]` types, representing 264 | optional parameters without defaults or repeatable parameters 265 | 266 | ```scala 267 | package testoptseq 268 | import mainargs.{main, arg, Parser} 269 | 270 | object Main{ 271 | @main 272 | def runOpt(opt: Option[Int]) = println(opt) 273 | 274 | @main 275 | def runSeq(seq: Seq[Int]) = println(seq) 276 | 277 | @main 278 | def runVec(seq: Vector[Int]) = println(seq) 279 | 280 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 281 | } 282 | ``` 283 | 284 | ```bash 285 | $ ./mill example.optseq runOpt 286 | None 287 | 288 | $ ./mill example.optseq runOpt --opt 123 289 | Some(123) 290 | 291 | $ ./mill example.optseq runSeq --seq 123 --seq 456 --seq 789 292 | List(123, 456, 789) 293 | ``` 294 | 295 | 296 | ## Short Arguments 297 | 298 | `@main` method arguments that have single-character names are automatically converted 299 | to short arguments, invoked with a single `-` instead of double `--`. The short version 300 | of an argument can also be given explicitly via the `@arg(short = '...')`: 301 | 302 | ```scala 303 | object Base { 304 | @main 305 | def bools(a: Flag, b: Boolean = false) = println(Seq(a.value, b, c.value)) 306 | 307 | @main 308 | def strs(a: Flag, b: String) = println(Seq(a.value, b)) 309 | } 310 | ``` 311 | 312 | These can be invoked as normal, for `Flag`s like `-a` or normal arguments that take 313 | a value like `-b` below: 314 | 315 | ```bash 316 | $ ./mill example.short bools -a 317 | Seq(true, false) 318 | 319 | $ ./mill example.short bools -b true 320 | Seq(false, true) 321 | ``` 322 | 323 | Multiple short arguments can be combined into one `-ab` call: 324 | 325 | $ ./mill example.short bools -ab true 326 | Seq(true, true) 327 | ``` 328 | 329 | Short arguments can be combined with their value: 330 | 331 | ```scala 332 | $ ./mill example.short bools -btrue 333 | Seq(false, true) 334 | ``` 335 | 336 | And you can combine both multiple short arguments as well as the resulting value: 337 | 338 | ```scala 339 | $ ./mill example.short bools -abtrue 340 | Seq(true, true) 341 | ``` 342 | 343 | Note that when multiple short arguments are combined, whether via `-ab true` or via `-abtrue`, 344 | only the last short argument (in this case `b`) can take a value. 345 | 346 | If an `=` is present in the short argument group after the first character, the short 347 | argument group is treated as a key-value pair with the remaining characters after the `=` 348 | passed as the value to the first short argument: 349 | 350 | ```scala 351 | $ ./mill example.short strs -b=value 352 | Seq(false, value) 353 | 354 | $ ./mill example.short strs -a -b=value 355 | Seq(true, value) 356 | ``` 357 | 358 | You can use `-b=` as a shorthand to set the value of `b` to an empty string: 359 | 360 | ```scala 361 | $ ./mill example.short strs -a -b= 362 | Seq(true, ) 363 | ``` 364 | 365 | If an `=` is present in the short argument group after subsequent character, all characters 366 | except the first are passed to the first short argument. This can be useful for concisely 367 | passing key-value pairs to a short argument: 368 | 369 | ```scala 370 | $ ./mill example.short strs -a -bkey=value 371 | Seq(true, key=value) 372 | ``` 373 | 374 | ```scala 375 | These can also be combined into a single token, with the first non-`Flag` short argument in the 376 | token consuming the subsequent characters as a string (unless the subsequent characters start with 377 | an `=`, which is skipped): 378 | 379 | ```scala 380 | $ ./mill example.short strs -ab=value 381 | Seq(true, value) 382 | 383 | $ ./mill example.short strs -abkey=value 384 | Seq(true, key=value) 385 | ``` 386 | 387 | ## Annotations 388 | 389 | The library's annotations and methods support the following parameters to 390 | customize your usage: 391 | 392 | ### @main 393 | 394 | - `name: String`: lets you specify the top-level name of `@main` method you are 395 | defining. If multiple `@main` methods are provided, this name controls the 396 | sub-command name in the CLI. If an explicit `name` is not passed, both the 397 | (typically) `camelCase` name of the Scala `def` as well as its `kebab-case` 398 | equivalents will be accepted 399 | 400 | - `doc: String`: a documentation string used to provide additional information 401 | about the command. Normally printed below the command name in the help message 402 | 403 | ### @arg 404 | 405 | - `name: String`: lets you specify the long name of a CLI parameter, e.g. 406 | `--foo`. If an explicit `name` is not passed, both the (typically) `camelCase` 407 | name of the Scala method parameter as well as its `kebab-case` 408 | equivalents will be accepted 409 | 410 | - `short: Char`: lets you specify the short name of a CLI parameter, e.g. `-f`. 411 | If not given, the argument can only be provided via its long name 412 | 413 | - `doc: String`: a documentation string used to provide additional information 414 | about the command 415 | 416 | - `noDefaultName: Boolean`: if `true` this arg (e.g `fooBar`) can only be called by its mangled name `--foo-bar` and not by the original name `--fooBar`. Defaults to `false` 417 | 418 | - `positional: Boolean`: if `true` this arg can be passed "positionally" without 419 | the `--name` of the parameter being provided, e.g. `./mill example.hello hello 3 --bool`. Defaults to `false` 420 | 421 | - `hidden: Boolean`: if `true` this arg will not be included in the rendered help text. 422 | 423 | ## Customization 424 | 425 | Apart from taking the name of the main `object` or config `case class`, 426 | `Parser` has methods that support a number 427 | of useful configuration values: 428 | 429 | - `allowPositional: Boolean`: allows you to pass CLI arguments "positionally" 430 | without the `--name` of the parameter being provided, e.g. `./mill example.hello -f hello --my-num 3 --bool` could be called via `./mill example.hello hello 3 --bool`. Defaults to `false` 431 | 432 | - `allowRepeats: Boolean`: allows you to pass in a flag multiple times, and 433 | using the last provided value rather than raising an error. Defaults to 434 | `false` 435 | 436 | - `totalWidth: Int`: how wide to re-format the `doc` strings to when printing 437 | the help text. Defaults to `100` 438 | 439 | - `printHelpOnExit: Boolean`: whether or not to print the full help text when 440 | argument parsing fails. This can be convenient, but potentially very verbose 441 | if the list of arguments is long. Defaults to `true` 442 | 443 | - `docsOnNewLine: Boolean`: whether to print argument doc-strings on a new line 444 | below the name of the argument; this may make things easier to read, but at a 445 | cost of taking up much more vertical space. Defaults to `false` 446 | 447 | - `autoprintHelpAndExit: Option[(Int, PrintStream)]`: whether to detect `--help` 448 | being passed in automatically, and if so where to print the help message and 449 | what exit code to exit the process with. Defaults t, `Some((0, System.out))`, 450 | but can be disabled by passing in `None` if you want to handle help text 451 | manually (e.g. by calling `.helpText` on the parser object) 452 | 453 | - `customName`/`customNames` and `customDoc`/`customDocs`: allows you to 454 | override the main method names and documentation strings at runtime. This 455 | allows you to work around limitations in the use of the `@main(name = "...", doc = "...")` annotation that only allows simple static strings. 456 | 457 | - `sorted: Boolean`: whether to sort the arguments alphabetically in the help text. Defaults to `true` 458 | 459 | - `nameMapper: String => Option[String]`: how Scala `camelCase` names are mapping 460 | to CLI command and flag names. Defaults to translation to `kebab-case`, but 461 | you can pass in `mainargs.Util.snakeCaseNameMapper` for `snake_case` CLI names 462 | or `mainargs.Util.nullNameMapper` to disable mapping. 463 | 464 | ## Custom Argument Parsers 465 | 466 | If you want to parse arguments into types that are not provided by the library, 467 | you can do so by defining an implicit `TokensReader[T]` for that type: 468 | 469 | ```scala 470 | package testcustom 471 | import mainargs.{main, arg, Parser, TokensReader} 472 | 473 | object Main{ 474 | implicit object PathRead extends TokensReader.Simple[os.Path]{ 475 | def shortName = "path" 476 | def read(strs: Seq[String]) = Right(os.Path(strs.head, os.pwd)) 477 | } 478 | 479 | @main 480 | def run(from: os.Path, to: os.Path) = { 481 | println("from: " + from) 482 | println("to: " + to) 483 | } 484 | 485 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 486 | } 487 | ``` 488 | 489 | ```bash 490 | $ ./mill example.custom --from mainargs --to out 491 | from: /Users/lihaoyi/Github/mainargs/mainargs 492 | to: /Users/lihaoyi/Github/mainargs/out 493 | ``` 494 | 495 | In this example, we define an implicit `PathRead` to teach MainArgs how to parse 496 | `os.Path`s from the [OS-Lib](https://github.com/lihaoyi/os-lib) library. 497 | 498 | Note that `read` takes all tokens that were passed to a particular parameter. 499 | Normally this is a `Seq` of length `1`, but if `allowEmpty` is `true` it could 500 | be an empty `Seq`, and if `alwaysRepeatable` is `true` then it could be 501 | arbitrarily long. 502 | 503 | You can see the Scaladoc for `TokenReaders.Simple` for other things you can override: 504 | 505 | - [mainargs.TokenReaders.Simple](https://javadoc.io/doc/com.lihaoyi/mainargs_2.13/latest/mainargs/TokensReader$$Simple.html) 506 | 507 | 508 | ## Handlings Leftover Arguments 509 | 510 | You can use the special `Leftover[T]` type to store any tokens that are 511 | not consumed by other parsers: 512 | 513 | ```scala 514 | package testvararg 515 | import mainargs.{main, arg, Parser, Leftover} 516 | 517 | object Main{ 518 | @main 519 | def run(foo: String, 520 | myNum: Int = 2, 521 | rest: Leftover[String]) = { 522 | println(foo * myNum + " " + rest.value) 523 | } 524 | 525 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 526 | } 527 | ``` 528 | 529 | ```bash 530 | $ ./mill example.vararg --foo bar i am cow 531 | barbar List(i, am, cow) 532 | ``` 533 | 534 | This also works with `ParserForClass`: 535 | 536 | ```scala 537 | package testvararg2 538 | import mainargs.{main, arg, ParserForClass, Leftover} 539 | 540 | object Main{ 541 | @main 542 | case class Config(foo: String, 543 | myNum: Int = 2, 544 | rest: Leftover[String]) 545 | 546 | def main(args: Array[String]): Unit = { 547 | val config = ParserForClass[Config].constructOrExit(args) 548 | println(config) 549 | } 550 | } 551 | ``` 552 | 553 | ```bash 554 | $ ./mill example.vararg2 --foo bar i am cow 555 | Config(bar,2,Leftover(List(i, am, cow))) 556 | ``` 557 | 558 | You can also pass in a different type to `Leftover`, e.g. `Leftover[Int]` or 559 | `Leftover[Boolean]`, if you want to specify that leftover tokens all parse to a 560 | particular type. Any tokens that do not conform to that type will result in an 561 | argument parsing error. 562 | 563 | ### Varargs Parameters 564 | 565 | You can also use `*` "varargs" to define a parameter that takes in the remainder 566 | of the tokens passed to the CLI: 567 | 568 | ```scala 569 | package testvararg 570 | import mainargs.{main, arg, Parser, Leftover} 571 | 572 | object Main{ 573 | @main 574 | def run(foo: String, 575 | myNum: Int, 576 | rest: String*) = { 577 | println(foo * myNum + " " + rest.value) 578 | } 579 | 580 | def main(args: Array[String]): Unit = Parser(this).runOrExit(args) 581 | } 582 | ``` 583 | 584 | Note that this has a limitation that you cannot then assign default values to 585 | the other parameters of the function, and hence using `Leftover[T]` is 586 | preferable for those cases. 587 | 588 | # Prior Art 589 | 590 | ## Ammonite & Mill 591 | 592 | MainArgs grew out of the user-defined `@main` method feature supported by 593 | Ammonite Scala Scripts: 594 | 595 | - http://ammonite.io/#ScriptArguments 596 | 597 | This implementation was largely copy-pasted into the Mill build tool, to use for 598 | its user-defined `T.command`s. A parallel implementation was used to parse 599 | command-line parameters for Ammonite and Mill themselves. 600 | 601 | Now all four implementations have been unified in the MainArgs library, which 602 | both Ammonite and Mill rely heavily upon. MainArgs also provides some additional 603 | features, such as making it easy to define short versions of flags like `-c` via 604 | the `short = '...'` parameter, or re-naming the command line flags via `name = "..."`. 605 | 606 | ## Case App 607 | 608 | MainArgs' support for parsing Scala `case class`es was inspired by Alex 609 | Archambault's `case-app` library: 610 | 611 | - https://github.com/alexarchambault/case-app 612 | 613 | MainArgs has the following differentiators over `case-app`: 614 | 615 | - Support for directly dispatching to `@main` method(s), rather than only 616 | parsing into `case class`es 617 | - A dependency-free implementation, without pulling in the heavyweight Shapeless 618 | library. 619 | 620 | ## Scopt 621 | 622 | MainArgs takes a lot of inspiration from the old Scala Scopt library: 623 | 624 | - https://github.com/scopt/scopt 625 | 626 | Unlike Scopt, MainArgs lets you call `@main` methods or instantiate `case class`es directly, without needing to separately define a `case class` and 627 | parser. This makes it usable with much less boilerplate than Scopt: a single 628 | method annotated with `@main` is all you need to turn your program into a 629 | command-line friendly tool. 630 | 631 | # Changelog 632 | 633 | ## 0.7.7 634 | 635 | - Add `mainargs.Parser` as a shorthand alias for `mainargs.ParserForClass` and 636 | `mainargs.ParserForMethods` [#201](http://github.com/com-lihaoyi/mainargs/pull/201) 637 | 638 | ## 0.7.6 639 | 640 | - Add support for Scala 2 varargs in scala 3 macro [#167](http://github.com/com-lihaoyi/mainargs/pull/167) 641 | 642 | ## 0.7.5 643 | 644 | - Fix reporting of incomplete short args [#162](https://github.com/com-lihaoyi/mainargs/pull/162) 645 | - Add BigDecimal TokensReader [#152](https://github.com/com-lihaoyi/mainargs/pull/152) 646 | 647 | ## 0.7.4 648 | 649 | - Fix issue with binary infinite recursion in binary compatibility forwarders [#156](https://github.com/com-lihaoyi/mainargs/pull/156) 650 | 651 | ## 0.7.3 652 | 653 | - Add missing `nameMapper` argument to `.runOrExit`, make `sort` param on `runEither` return 654 | `true` for consistency with the docs [#155](https://github.com/com-lihaoyi/mainargs/pull/155) 655 | 656 | ## 0.7.2 657 | 658 | - Various improvements to Scala 3 macros to match Scala 2 implementation [#148](https://github.com/com-lihaoyi/mainargs/pull/148) 659 | 660 | ## 0.7.1 661 | 662 | - Fix detection of `@main` methods inherited from `trait`s in Scala 3.x [#142](https://github.com/com-lihaoyi/mainargs/pull/142) 663 | 664 | ## 0.7.0 665 | 666 | - Support for Scala-Native 0.5.0 667 | - Minimum version of Scala 3.x raised to 3.3.1 668 | 669 | ## 0.6.3 670 | 671 | - Fix usage of `ParserForClass` for `case class`es with more than 22 parameters in Scala 2.x 672 | 673 | ## 0.6.2 674 | 675 | - Make combine short args that fail to parse go through normal leftover-token code paths 676 | [#112](https://github.com/com-lihaoyi/mainargs/pull/112) 677 | 678 | ## 0.6.1 679 | 680 | - Fix stackoverflow from incorrect binary compatibility shim 681 | [#107](https://github.com/com-lihaoyi/mainargs/pull/107) 682 | 683 | ## 0.6.0 684 | 685 | - Automatically map `camelCase` Scala method and argument names to `kebab-case` 686 | CLI commands and flag names, with configurability by passing in custom 687 | `nameMappers` [#101](https://github.com/com-lihaoyi/mainargs/pull/101) 688 | 689 | - Allow short arguments and their values to be combined into a single token 690 | [#102](https://github.com/com-lihaoyi/mainargs/pull/102) 691 | 692 | ## 0.5.4 693 | 694 | - Remove unnecessary PPrint dependency 695 | 696 | ## 0.5.3 697 | 698 | - Support GFlags-style `--foo=bar` syntax [#98](https://github.com/com-lihaoyi/mainargs/pull/98) 699 | 700 | ## 0.5.1 701 | 702 | - Fix handling of case class main method parameter default parameters and 703 | annotations in Scala 3 [#88](https://github.com/com-lihaoyi/mainargs/pull/88) 704 | 705 | ## 0.5.0 706 | 707 | - Remove hard-code support for mainargs.Leftover/Flag/Subparser to support 708 | alternate implementations [#62](https://github.com/com-lihaoyi/mainargs/pull/62). 709 | Note that this is a binary-incompatible change, and any custom 710 | `mainargs.TokenReader`s you may implement will need to be updated to implement 711 | the `mainargs.TokenReader.Simple` trait 712 | 713 | - Fix argument parsing of flags in the presence of `allowPositional=true` 714 | [#66](https://github.com/com-lihaoyi/mainargs/pull/66) 715 | 716 | ## 0.4.0 717 | 718 | - Support sorting to args in help text and sort by default 719 | - Various dependency updates 720 | - This release is binary compatible with mainargs 0.3.0 721 | 722 | ## 0.3.0 723 | 724 | - Update all dependencies to latest 725 | - Support for Scala Native on Scala 3 726 | 727 | ## 0.2.5 728 | 729 | - Backport of *Fix usage of `ParserForClass` for `case class`es with more than 730 | 22 parameters with some default values in Scala 2.x (#123)* on top of 0.2.3 731 | 732 | ## 0.2.3 733 | 734 | - Support Scala 3 [#18](https://github.com/com-lihaoyi/mainargs/pull/18) 735 | 736 | ## 0.2.2 737 | 738 | - Fix hygiene of macros [#12](https://github.com/com-lihaoyi/mainargs/pull/12) 739 | - Allow special characters in method names and argument names [#13](https://github.com/com-lihaoyi/mainargs/pull/13) 740 | 741 | ## 0.2.1 742 | 743 | - Scala-Native 0.4.0 support 744 | 745 | ## 0.1.7 746 | 747 | - Add support for `positional=true` flag in `mainargs.arg`, to specify a 748 | specific argument can only be passed positionally regardless of whether 749 | `allowPositional` is enabled for the entire parser 750 | 751 | - Allow `-` and `--` to be passed as argument values without being treated as 752 | flags 753 | 754 | ## 0.1.4 755 | 756 | - First release 757 | --------------------------------------------------------------------------------