From 923c24616e333da23997951556e0ebd45a02d56c Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 12:28:08 +0800 Subject: [PATCH 1/8] . --- mainargs/src/TokenGrouping.scala | 60 ++++++++++++++++++- mainargs/test/src/FlagTests.scala | 97 +++++++++++++++++++++++++++++-- 2 files changed, 150 insertions(+), 7 deletions(-) diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index 077caf6..d053730 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -26,6 +26,16 @@ object TokenGrouping { .flatMap { x => getNames(x).map(_ -> x) } .toMap[String, ArgSig] + lazy val shortArgMap: Map[Char, ArgSig] = argSigs + .collect{case (a, _) => a.shortName.map(_ -> a)} + .flatten + .toMap[Char, ArgSig] + + lazy val shortFlagArgMap: Map[Char, ArgSig] = argSigs + .collect{case (a, r: TokensReader.Flag) => a.shortName.map(_ -> a)} + .flatten + .toMap[Char, ArgSig] + lazy val keywordArgMap = makeKeywordArgMap( x => x.name.map("--" + _) ++ x.shortName.map("-" + _) ) @@ -43,7 +53,55 @@ object TokenGrouping { m.get(k).map(a => (a, a.reader)) } - if (head.startsWith("-") && head.exists(_ != '-')) { + + // special handling for combined short args of the style "-xvf" or "-j10" + if (head.startsWith("-") && + head.lift(1).exists(c => c != '-' && c != '=') && + head.lift(2).exists(c => c != '-' && c != '=')){ + val chars = head.drop(1) + var rest2 = rest + var i = 0 + var currentMap = current + var failure: Result.Failure = null + + while (i < chars.length){ + val c = chars(i) + shortFlagArgMap.get(c) match{ + case Some(a) => + // For `Flag`s in chars, we consume the char, set it to `true`, and continue + currentMap = Util.appendMap(currentMap, a, "") + i += 1 + case None => + // For other kinds of short arguments, we consume the char, set the value to + // the remaining characters, and exit + shortArgMap.get(c) match{ + case Some(a) => + if (i == chars.length - 1) { + rest2 match{ + case Nil => + failure = Result.Failure.MismatchedArguments(incomplete = Some(a)) + case next :: remaining => + currentMap = Util.appendMap(currentMap, a, next) + rest2 = remaining + } + }else { + currentMap = Util.appendMap(currentMap, a, chars.drop(i + 1)) + } + case None => + println("C") + // If we encounter a character that is neither a short flag or a + // short argument, it is an error + failure = Result.Failure.MismatchedArguments(unknown = Seq(c.toString)) + } + i = chars.length + } + + } + + if (failure != null) failure + else rec(rest2, currentMap) + + } else if (head.startsWith("-") && head.exists(_ != '-')) { head.split("=", 2) match{ case Array(first, second) => lookupArgMap(first, longKeywordArgMap) match { diff --git a/mainargs/test/src/FlagTests.scala b/mainargs/test/src/FlagTests.scala index e75f98c..3904f1d 100644 --- a/mainargs/test/src/FlagTests.scala +++ b/mainargs/test/src/FlagTests.scala @@ -5,34 +5,119 @@ object FlagTests extends TestSuite { object Base { @main - def flaggy(a: Flag, b: Boolean, c: Flag) = a.value || b || c.value + def flaggy(a: Flag, b: Boolean, c: Flag) = Seq(a.value, b, c.value) } val check = new Checker(ParserForMethods(Base), allowPositional = true) val tests = Tests { test - check( List("-b", "true"), - Result.Success(true) + Result.Success(Seq(false, true, false)) ) test - check( List("-b", "false"), - Result.Success(false) + Result.Success(Seq(false, false, false)) ) test - check( List("-a", "-b", "false"), - Result.Success(true) + Result.Success(Seq(true, false, false)) ) test - check( List("-c", "-b", "false"), - Result.Success(true) + Result.Success(Seq(false, false, true)) ) test - check( List("-a", "-c", "-b", "false"), - Result.Success(true) + Result.Success(Seq(true, false, true)) ) + test("combined"){ + test - check( + List("-bfalse"), + Result.Success(List(false, false, false)) + ) + test - check( + List("-btrue"), + Result.Success(List(false, true, false)) + ) + + test - check( + List("-abtrue"), + Result.Success(List(true, true, false)) + ) + test - check( + List("-abfalse"), + Result.Success(List(true, false, false)) + ) + + test - check( + List("-a", "-btrue"), + Result.Success(List(true, true, false)) + ) + + test - check( + List("-a", "-bfalse"), + Result.Success(List(true, false, false)) + ) + + test - check( + List("-acbtrue"), + Result.Success(List(true, true, true)) + ) + + test - check( + List("-acbfalse"), + Result.Success(List(true, false, true)) + ) + + test - check( + List("-a", "-c", "-btrue"), + Result.Success(List(true, true, true)) + ) + + test - check( + List("-a", "-c", "-bfalse"), + Result.Success(List(true, false, true)) + ) + + test - check( + List("-a", "-btrue", "-c"), + Result.Success(List(true, true, true)) + ) + + test - check( + List("-a", "-bfalse", "-c"), + Result.Success(List(true, false, true)) + ) + + test - check( + List("-ba"), + Result.Failure.InvalidArguments( + List( + Result.ParamError.Failed( + new ArgSig(None, Some('b'), None, None, mainargs.TokensReader.BooleanRead, false, false), + Vector("a"), + "java.lang.IllegalArgumentException: For input string: \"a\"" + ) + ) + ) + ) + + test - check( + List("-ab"), + Result.Failure.MismatchedArguments( + Nil, + Nil, + Nil, + Some( + new ArgSig(None, Some('b'), None, None, TokensReader.BooleanRead, false, false) + ) + ) + ) + } + } } From 5017b200f320194d316b8d2d14929f5802906e0f Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 12:40:23 +0800 Subject: [PATCH 2/8] . --- build.sc | 3 ++- example/short/src/Main.scala | 9 +++++++ readme.md | 51 +++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 example/short/src/Main.scala diff --git a/build.sc b/build.sc index 0f914ce..20eb74b 100644 --- a/build.sc +++ b/build.sc @@ -90,7 +90,7 @@ trait ExampleModule extends ScalaModule { def moduleDeps = Seq(mainargs.jvm(scala213)) } -object example { +object example extends Module { object hello extends ExampleModule object hello2 extends ExampleModule object caseclass extends ExampleModule @@ -102,4 +102,5 @@ object example { } object vararg extends ExampleModule object vararg2 extends ExampleModule + object short extends ExampleModule } diff --git a/example/short/src/Main.scala b/example/short/src/Main.scala new file mode 100644 index 0000000..b4b2d47 --- /dev/null +++ b/example/short/src/Main.scala @@ -0,0 +1,9 @@ +package example.hello +import mainargs.{main, arg, ParserForMethods, Flag} + +object Main { + @main + def flaggy(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) + + def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args) +} diff --git a/readme.md b/readme.md index 79afd0f..971cfc4 100644 --- a/readme.md +++ b/readme.md @@ -287,6 +287,55 @@ $ ./mill example.optseq runSeq --seq 123 --seq 456 --seq 789 List(123, 456, 789) ``` + +## Short Arguments + +`@main` method arguments that have single-character names are automatically converted +to short arguments, invoked with a single `-` instead of double `--`. The short version +of an argument can also be given explicitly via the `@arg(short = '...')`: + +```scala +object Base { + @main + def flaggy(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) +} +``` + +These can be invoked as normal, for `Flag`s like `-a` or normal arguments that take +a value like `-b` below: + +```bash +$ ./mill example.short -a +Seq(true, false, false) + +$ ./mill example.short -b true +Seq(false, true, false) +``` + +Multiple short arguments can be combined into one `-ab` call: + +```scala +$ ./mill example.short -ab true +Seq(true, true, false) +``` + +Short arguments can be combined with their value: + +```scala +$ ./mill example.short -btrue +Seq(false, true, false) +``` + +And you can combine both multiple short arguments as well as the resulting value: + +```scala +$ ./mill example.short -abtrue +Seq(true, true, false) +``` + +Note that when multiple short arguments are combined, whether via `-ab true` or via `-abtrue`, +only the last short argument (in this case `b`) can take a value. + ## Annotations The library's annotations and methods support the following parameters to @@ -307,7 +356,7 @@ customize your usage: `--foo`. Defaults to the name of the function parameter if not given - `short: Char`: lets you specify the short name of a CLI parameter, e.g. `-f`. - If not given, theargument can only be provided via its long name + If not given, the argument can only be provided via its long name - `doc: String`: a documentation string used to provide additional information about the command From fd9d1ff11fa69603e270ca5777f2d6b40f87193e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 13:18:55 +0800 Subject: [PATCH 3/8] . --- mainargs/src/TokenGrouping.scala | 98 +++++++++++++++++-------------- mainargs/test/src/FlagTests.scala | 9 +++ 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index d053730..c9ab173 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -42,6 +42,55 @@ object TokenGrouping { lazy val longKeywordArgMap = makeKeywordArgMap(x => x.name.map("--" + _)) + def parseCombinedShortTokens(current: Map[ArgSig, Vector[String]], + head: String, + rest: List[String]) = { + val chars = head.drop(1) + var rest2 = rest + var i = 0 + var currentMap = current + var failure: Result.Failure = null + + while (i < chars.length) { + val c = chars(i) + shortFlagArgMap.get(c) match { + case Some(a) => + // For `Flag`s in chars, we consume the char, set it to `true`, and continue + currentMap = Util.appendMap(currentMap, a, "") + i += 1 + case None => + // For other kinds of short arguments, we consume the char, set the value to + // the remaining characters, and exit + shortArgMap.get(c) match { + case Some(a) => + if (i < chars.length - 1) { + currentMap = Util.appendMap(currentMap, a, chars.drop(i + 1)) + } else { + // If the non-flag argument is the last in the combined token, we look + // ahead to grab the next token and assign it as this argument's value + rest2 match { + case Nil => + // If there is no next token, it is an error + failure = Result.Failure.MismatchedArguments(incomplete = Some(a)) + case next :: remaining => + currentMap = Util.appendMap(currentMap, a, next) + rest2 = remaining + } + } + case None => + // If we encounter a character that is neither a short flag or a + // short argument, it is an error + failure = Result.Failure.MismatchedArguments(unknown = Seq(c.toString)) + } + i = chars.length + } + + } + + if (failure != null) Left(failure) + else Right((rest2, currentMap)) + } + @tailrec def rec( remaining: List[String], current: Map[ArgSig, Vector[String]] @@ -56,51 +105,14 @@ object TokenGrouping { // special handling for combined short args of the style "-xvf" or "-j10" if (head.startsWith("-") && - head.lift(1).exists(c => c != '-' && c != '=') && - head.lift(2).exists(c => c != '-' && c != '=')){ - val chars = head.drop(1) - var rest2 = rest - var i = 0 - var currentMap = current - var failure: Result.Failure = null - - while (i < chars.length){ - val c = chars(i) - shortFlagArgMap.get(c) match{ - case Some(a) => - // For `Flag`s in chars, we consume the char, set it to `true`, and continue - currentMap = Util.appendMap(currentMap, a, "") - i += 1 - case None => - // For other kinds of short arguments, we consume the char, set the value to - // the remaining characters, and exit - shortArgMap.get(c) match{ - case Some(a) => - if (i == chars.length - 1) { - rest2 match{ - case Nil => - failure = Result.Failure.MismatchedArguments(incomplete = Some(a)) - case next :: remaining => - currentMap = Util.appendMap(currentMap, a, next) - rest2 = remaining - } - }else { - currentMap = Util.appendMap(currentMap, a, chars.drop(i + 1)) - } - case None => - println("C") - // If we encounter a character that is neither a short flag or a - // short argument, it is an error - failure = Result.Failure.MismatchedArguments(unknown = Seq(c.toString)) - } - i = chars.length - } - + head.lift(1).exists(c => c != '-') && + head.lift(2).exists(c => c != '-') && + !head.contains('=')){ + parseCombinedShortTokens(current, head, rest) match{ + case Left(failure) => failure + case Right((rest2, currentMap)) => rec(rest2, currentMap) } - if (failure != null) failure - else rec(rest2, currentMap) - } else if (head.startsWith("-") && head.exists(_ != '-')) { head.split("=", 2) match{ case Array(first, second) => diff --git a/mainargs/test/src/FlagTests.scala b/mainargs/test/src/FlagTests.scala index 3904f1d..68f2e5d 100644 --- a/mainargs/test/src/FlagTests.scala +++ b/mainargs/test/src/FlagTests.scala @@ -117,6 +117,15 @@ object FlagTests extends TestSuite { ) ) ) + test - check( + List("-ab=true"), + Result.Failure.MismatchedArguments( + Seq(new ArgSig(None, Some('b'), None, None, TokensReader.BooleanRead, false, false)), + unknown = Seq("-ab=true"), + Nil, + None + ) + ) } } From b977127995d35a125306c9be800e2777a4b47924 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 13:20:18 +0800 Subject: [PATCH 4/8] . --- mainargs/src/TokenGrouping.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index c9ab173..c55f4eb 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -91,6 +91,10 @@ object TokenGrouping { else Right((rest2, currentMap)) } + def lookupArgMap(k: String, m: Map[String, ArgSig]): Option[(ArgSig, mainargs.TokensReader[_])] = { + m.get(k).map(a => (a, a.reader)) + } + @tailrec def rec( remaining: List[String], current: Map[ArgSig, Vector[String]] @@ -98,10 +102,6 @@ object TokenGrouping { remaining match { case head :: rest => - def lookupArgMap(k: String, m: Map[String, ArgSig]): Option[(ArgSig, mainargs.TokensReader[_])] = { - m.get(k).map(a => (a, a.reader)) - } - // special handling for combined short args of the style "-xvf" or "-j10" if (head.startsWith("-") && From 5f63aaaaed7082afd96891205ff341c62f28cb11 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 20:53:23 +0800 Subject: [PATCH 5/8] . --- example/short/src/Main.scala | 5 ++- mainargs/src/TokenGrouping.scala | 37 ++++++++------- mainargs/test/src/EqualsSyntaxTests.scala | 12 +---- mainargs/test/src/FlagTests.scala | 55 ++++++++++++----------- mainargs/test/src/PositionalTests.scala | 26 +---------- readme.md | 36 ++++++++++++--- 6 files changed, 84 insertions(+), 87 deletions(-) diff --git a/example/short/src/Main.scala b/example/short/src/Main.scala index b4b2d47..191174f 100644 --- a/example/short/src/Main.scala +++ b/example/short/src/Main.scala @@ -3,7 +3,10 @@ import mainargs.{main, arg, ParserForMethods, Flag} object Main { @main - def flaggy(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) + def bools(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) + + @main + def strs(a: Flag, b: String) = println(Seq(a.value, b)) def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args) } diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index c55f4eb..6866ac5 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -27,20 +27,16 @@ object TokenGrouping { .toMap[String, ArgSig] lazy val shortArgMap: Map[Char, ArgSig] = argSigs - .collect{case (a, _) => a.shortName.map(_ -> a)} + .collect{case (a, _) if !a.positional => a.shortName.map(_ -> a)} .flatten .toMap[Char, ArgSig] lazy val shortFlagArgMap: Map[Char, ArgSig] = argSigs - .collect{case (a, r: TokensReader.Flag) => a.shortName.map(_ -> a)} + .collect{case (a, r: TokensReader.Flag) if !a.positional => a.shortName.map(_ -> a)} .flatten .toMap[Char, ArgSig] - lazy val keywordArgMap = makeKeywordArgMap( - x => x.name.map("--" + _) ++ x.shortName.map("-" + _) - ) - - lazy val longKeywordArgMap = makeKeywordArgMap(x => x.name.map("--" + _)) + lazy val longKeywordArgMap = makeKeywordArgMap(_.name.map("--" + _)) def parseCombinedShortTokens(current: Map[ArgSig, Vector[String]], head: String, @@ -80,7 +76,7 @@ object TokenGrouping { case None => // If we encounter a character that is neither a short flag or a // short argument, it is an error - failure = Result.Failure.MismatchedArguments(unknown = Seq(c.toString)) + failure = Result.Failure.MismatchedArguments(unknown = Seq("-" + c.toString)) } i = chars.length } @@ -101,16 +97,23 @@ object TokenGrouping { ): Result[TokenGrouping[B]] = { remaining match { case head :: rest => + // special handling for combined short args of the style "-xvf" or "-j10" + if (head.startsWith("-") && head.lift(1).exists(c => c != '-')){ + head.split("=", 2) match { + case Array(first, second) => + shortArgMap.get(first(1)) match{ + case Some(a) if a.reader.isSimple => + if (first.length == 2) rec(rest, Util.appendMap(current, a, second)) + else rec(rest, Util.appendMap(current, a, head.drop(2))) + case _ => Result.Failure.MismatchedArguments(Nil, Seq(first), Nil) + } - // special handling for combined short args of the style "-xvf" or "-j10" - if (head.startsWith("-") && - head.lift(1).exists(c => c != '-') && - head.lift(2).exists(c => c != '-') && - !head.contains('=')){ - parseCombinedShortTokens(current, head, rest) match{ - case Left(failure) => failure - case Right((rest2, currentMap)) => rec(rest2, currentMap) + case _ => + parseCombinedShortTokens(current, head, rest) match{ + case Left(failure) => failure + case Right((rest2, currentMap)) => rec(rest2, currentMap) + } } } else if (head.startsWith("-") && head.exists(_ != '-')) { @@ -124,7 +127,7 @@ object TokenGrouping { } case _ => - lookupArgMap(head, keywordArgMap) match { + lookupArgMap(head, longKeywordArgMap) match { case Some((cliArg, _: TokensReader.Flag)) => rec(rest, Util.appendMap(current, cliArg, "")) case Some((cliArg, _: TokensReader.Simple[_])) => diff --git a/mainargs/test/src/EqualsSyntaxTests.scala b/mainargs/test/src/EqualsSyntaxTests.scala index 06aed69..143cd76 100644 --- a/mainargs/test/src/EqualsSyntaxTests.scala +++ b/mainargs/test/src/EqualsSyntaxTests.scala @@ -28,17 +28,9 @@ object EqualsSyntaxTests extends TestSuite { "bar=quxbar=qux false" } test("shortName") { - // -f=bar syntax doesn't work for short names + // -f=bar syntax does work for short names ParserForMethods(Main).runEither(Array("-f=bar")) ==> - Left( - """Missing argument: -f --foo - |Unknown argument: "-f=bar" - |Expected Signature: run - | -f --foo String to print repeatedly - | --my-num How many times to print string - | --bool Example flag - | - |""".stripMargin) + Right("barbar false") } test("notFlags") { // -f=bar syntax doesn't work for flags diff --git a/mainargs/test/src/FlagTests.scala b/mainargs/test/src/FlagTests.scala index 68f2e5d..a291d22 100644 --- a/mainargs/test/src/FlagTests.scala +++ b/mainargs/test/src/FlagTests.scala @@ -5,96 +5,99 @@ object FlagTests extends TestSuite { object Base { @main - def flaggy(a: Flag, b: Boolean, c: Flag) = Seq(a.value, b, c.value) + def bool(a: Flag, b: Boolean, c: Flag) = Seq(a.value, b, c.value) + @main + def str(a: Flag, b: String) = Seq(a.value, b) } + val check = new Checker(ParserForMethods(Base), allowPositional = true) val tests = Tests { test - check( - List("-b", "true"), + List("bool", "-b", "true"), Result.Success(Seq(false, true, false)) ) test - check( - List("-b", "false"), + List("bool", "-b", "false"), Result.Success(Seq(false, false, false)) ) test - check( - List("-a", "-b", "false"), + List("bool", "-a", "-b", "false"), Result.Success(Seq(true, false, false)) ) test - check( - List("-c", "-b", "false"), + List("bool", "-c", "-b", "false"), Result.Success(Seq(false, false, true)) ) test - check( - List("-a", "-c", "-b", "false"), + List("bool", "-a", "-c", "-b", "false"), Result.Success(Seq(true, false, true)) ) test("combined"){ test - check( - List("-bfalse"), + List("bool", "-bfalse"), Result.Success(List(false, false, false)) ) test - check( - List("-btrue"), + List("bool", "-btrue"), Result.Success(List(false, true, false)) ) test - check( - List("-abtrue"), + List("bool", "-abtrue"), Result.Success(List(true, true, false)) ) test - check( - List("-abfalse"), + List("bool", "-abfalse"), Result.Success(List(true, false, false)) ) test - check( - List("-a", "-btrue"), + List("bool", "-a", "-btrue"), Result.Success(List(true, true, false)) ) test - check( - List("-a", "-bfalse"), + List("bool", "-a", "-bfalse"), Result.Success(List(true, false, false)) ) test - check( - List("-acbtrue"), + List("bool", "-acbtrue"), Result.Success(List(true, true, true)) ) test - check( - List("-acbfalse"), + List("bool", "-acbfalse"), Result.Success(List(true, false, true)) ) test - check( - List("-a", "-c", "-btrue"), + List("bool", "-a", "-c", "-btrue"), Result.Success(List(true, true, true)) ) test - check( - List("-a", "-c", "-bfalse"), + List("bool", "-a", "-c", "-bfalse"), Result.Success(List(true, false, true)) ) test - check( - List("-a", "-btrue", "-c"), + List("bool", "-a", "-btrue", "-c"), Result.Success(List(true, true, true)) ) test - check( - List("-a", "-bfalse", "-c"), + List("bool", "-a", "-bfalse", "-c"), Result.Success(List(true, false, true)) ) test - check( - List("-ba"), + List("bool", "-ba"), Result.Failure.InvalidArguments( List( Result.ParamError.Failed( @@ -107,7 +110,7 @@ object FlagTests extends TestSuite { ) test - check( - List("-ab"), + List("bool", "-ab"), Result.Failure.MismatchedArguments( Nil, Nil, @@ -117,14 +120,12 @@ object FlagTests extends TestSuite { ) ) ) + + test - check(List("str", "-b=value", "-a"), Result.Success(List(true, "value"))) + test - check( - List("-ab=true"), - Result.Failure.MismatchedArguments( - Seq(new ArgSig(None, Some('b'), None, None, TokensReader.BooleanRead, false, false)), - unknown = Seq("-ab=true"), - Nil, - None - ) + List("str", "-bvalue", "-akey=value"), + Result.Failure.MismatchedArguments(Nil, List("-akey"), Nil, None) ) } diff --git a/mainargs/test/src/PositionalTests.scala b/mainargs/test/src/PositionalTests.scala index c82d092..683e5d4 100644 --- a/mainargs/test/src/PositionalTests.scala +++ b/mainargs/test/src/PositionalTests.scala @@ -44,31 +44,7 @@ object PositionalTests extends TestSuite { ) test - check( List("-x", "true", "-y", "false", "-z", "false"), - Result.Failure.MismatchedArguments( - Vector( - ArgSig( - None, - Some('y'), - None, - None, - TokensReader.BooleanRead, - positional = true, - hidden = false - ), - ArgSig( - None, - Some('z'), - None, - None, - TokensReader.BooleanRead, - positional = false, - hidden = false - ) - ), - List("-y", "false", "-z", "false"), - List(), - None - ) + Result.Failure.MismatchedArguments(List(), List("-y"), List(), None) ) } } diff --git a/readme.md b/readme.md index 971cfc4..96eb54c 100644 --- a/readme.md +++ b/readme.md @@ -297,7 +297,10 @@ of an argument can also be given explicitly via the `@arg(short = '...')`: ```scala object Base { @main - def flaggy(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) + def bools(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) + + @main + def strs(a: Flag, b: String) = println(Seq(a.value, b)) } ``` @@ -305,36 +308,55 @@ These can be invoked as normal, for `Flag`s like `-a` or normal arguments that t a value like `-b` below: ```bash -$ ./mill example.short -a +$ ./mill example.short bools -a Seq(true, false, false) -$ ./mill example.short -b true +$ ./mill example.short bools -b true Seq(false, true, false) ``` Multiple short arguments can be combined into one `-ab` call: ```scala -$ ./mill example.short -ab true +$ ./mill example.short bools -ab true Seq(true, true, false) ``` Short arguments can be combined with their value: ```scala -$ ./mill example.short -btrue +$ ./mill example.short bools -btrue Seq(false, true, false) ``` And you can combine both multiple short arguments as well as the resulting value: ```scala -$ ./mill example.short -abtrue +$ ./mill example.short bools -abtrue Seq(true, true, false) ``` Note that when multiple short arguments are combined, whether via `-ab true` or via `-abtrue`, -only the last short argument (in this case `b`) can take a value. +only the last short argument (in this case `b`) can take a value. + +If an `=` is present in the short argument group after the first character, the short +argument group is treated as a key-value pair with the remaining characters after the `=` +passed as the value to the first short argument: + +```scala +$ ./mill example.short strs -a -b=value +Seq(true, value) +``` + + +If an `=` is present in the short argument group after subsequent character, all characters +except the first are passed to the first short argument. This can be useful for concisely +passing key-value pairs to a short argument: + +```scala +$ ./mill example.short strs -a -bkey=value +Seq(true, key=value) +``` ## Annotations From 9bb17c16de9eaf3561d8ab2c9bf8ad9a9ed0a6c7 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 20:59:25 +0800 Subject: [PATCH 6/8] . --- mainargs/src/TokenGrouping.scala | 20 ++++---------------- mainargs/test/src/FlagTests.scala | 4 +++- readme.md | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index 6866ac5..0b4154c 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -60,7 +60,7 @@ object TokenGrouping { shortArgMap.get(c) match { case Some(a) => if (i < chars.length - 1) { - currentMap = Util.appendMap(currentMap, a, chars.drop(i + 1)) + currentMap = Util.appendMap(currentMap, a, chars.drop(i + 1).stripPrefix("=")) } else { // If the non-flag argument is the last in the combined token, we look // ahead to grab the next token and assign it as this argument's value @@ -99,21 +99,9 @@ object TokenGrouping { case head :: rest => // special handling for combined short args of the style "-xvf" or "-j10" if (head.startsWith("-") && head.lift(1).exists(c => c != '-')){ - head.split("=", 2) match { - case Array(first, second) => - shortArgMap.get(first(1)) match{ - case Some(a) if a.reader.isSimple => - if (first.length == 2) rec(rest, Util.appendMap(current, a, second)) - else rec(rest, Util.appendMap(current, a, head.drop(2))) - - case _ => Result.Failure.MismatchedArguments(Nil, Seq(first), Nil) - } - - case _ => - parseCombinedShortTokens(current, head, rest) match{ - case Left(failure) => failure - case Right((rest2, currentMap)) => rec(rest2, currentMap) - } + parseCombinedShortTokens(current, head, rest) match{ + case Left(failure) => failure + case Right((rest2, currentMap)) => rec(rest2, currentMap) } } else if (head.startsWith("-") && head.exists(_ != '-')) { diff --git a/mainargs/test/src/FlagTests.scala b/mainargs/test/src/FlagTests.scala index a291d22..e37868c 100644 --- a/mainargs/test/src/FlagTests.scala +++ b/mainargs/test/src/FlagTests.scala @@ -123,9 +123,11 @@ object FlagTests extends TestSuite { test - check(List("str", "-b=value", "-a"), Result.Success(List(true, "value"))) + test - check(List("str", "-ab=value"), Result.Success(List(true, "value"))) + test - check( List("str", "-bvalue", "-akey=value"), - Result.Failure.MismatchedArguments(Nil, List("-akey"), Nil, None) + Result.Failure.MismatchedArguments(Nil, List("-k"), Nil, None) ) } diff --git a/readme.md b/readme.md index 96eb54c..77171ae 100644 --- a/readme.md +++ b/readme.md @@ -344,6 +344,9 @@ argument group is treated as a key-value pair with the remaining characters afte passed as the value to the first short argument: ```scala +$ ./mill example.short strs -b=value +Seq(false, value) + $ ./mill example.short strs -a -b=value Seq(true, value) ``` @@ -358,6 +361,18 @@ $ ./mill example.short strs -a -bkey=value Seq(true, key=value) ``` +These can also be combined into a single token, with the first non-`Flag` short argument in the +token consuming the subsequent characters as a string (unless the subsequent characters start with +an `=`, which is skipped): + +```scala +$ ./mill example.short strs -ab=value +Seq(true, value) + +$ ./mill example.short strs -abkey=value +Seq(true, key=value) +``` + ## Annotations The library's annotations and methods support the following parameters to From b4c1401e283704d6bccfe6c43db0140abfe35748 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 25 Jan 2024 21:14:13 +0800 Subject: [PATCH 7/8] . --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index 77171ae..a6ccaba 100644 --- a/readme.md +++ b/readme.md @@ -78,6 +78,9 @@ hellohello false $ ./mill example.hello -f hello --my-num 3 hellohellohello false +$ ./mill example.hello -f hello --my-num=3 +hellohellohello false + $ ./mill example.hello -f hello --my-num 3 --bool hellohellohello true From d9145a04ecda15044ae751fc8b0cbcb4f7f800d4 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 26 Jan 2024 08:50:01 +0800 Subject: [PATCH 8/8] readme --- mainargs/test/src/EqualsSyntaxTests.scala | 5 +++ mainargs/test/src/FlagTests.scala | 1 + readme.md | 37 ++++++++++++++++------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/mainargs/test/src/EqualsSyntaxTests.scala b/mainargs/test/src/EqualsSyntaxTests.scala index 143cd76..461556c 100644 --- a/mainargs/test/src/EqualsSyntaxTests.scala +++ b/mainargs/test/src/EqualsSyntaxTests.scala @@ -27,6 +27,11 @@ object EqualsSyntaxTests extends TestSuite { ParserForMethods(Main).runOrThrow(Array("--foo=bar=qux")) ==> "bar=quxbar=qux false" } + test("empty") { + // --foo= syntax sets `foo` to an empty string + ParserForMethods(Main).runOrThrow(Array("--foo=")) ==> + " false" + } test("shortName") { // -f=bar syntax does work for short names ParserForMethods(Main).runEither(Array("-f=bar")) ==> diff --git a/mainargs/test/src/FlagTests.scala b/mainargs/test/src/FlagTests.scala index e37868c..d99dff0 100644 --- a/mainargs/test/src/FlagTests.scala +++ b/mainargs/test/src/FlagTests.scala @@ -122,6 +122,7 @@ object FlagTests extends TestSuite { ) test - check(List("str", "-b=value", "-a"), Result.Success(List(true, "value"))) + test - check(List("str", "-b=", "-a"), Result.Success(List(true, ""))) test - check(List("str", "-ab=value"), Result.Success(List(true, "value"))) diff --git a/readme.md b/readme.md index a6ccaba..668ed9b 100644 --- a/readme.md +++ b/readme.md @@ -72,16 +72,25 @@ object Main{ ``` ```bash -$ ./mill example.hello -f hello +$ ./mill example.hello -f hello # short name hellohello false -$ ./mill example.hello -f hello --my-num 3 -hellohellohello false +$ ./mill example.hello --foo hello # long name +hellohello false + +$ ./mill example.hello --foo=hello # gflags-style +hellohello false + +$ ./mill example.hello --foo "" # set to empty value + false -$ ./mill example.hello -f hello --my-num=3 +$ ./mill example.hello --foo= # gflags-style empty value + false + +$ ./mill example.hello -f hello --my-num 3 # numeric arguments hellohellohello false -$ ./mill example.hello -f hello --my-num 3 --bool +$ ./mill example.hello -f hello --my-num 3 --bool # flags hellohellohello true $ ./mill example.hello --wrong-flag @@ -300,7 +309,7 @@ of an argument can also be given explicitly via the `@arg(short = '...')`: ```scala object Base { @main - def bools(a: Flag, b: Boolean = false, c: Flag) = println(Seq(a.value, b, c.value)) + def bools(a: Flag, b: Boolean = false) = println(Seq(a.value, b, c.value)) @main def strs(a: Flag, b: String) = println(Seq(a.value, b)) @@ -312,31 +321,31 @@ a value like `-b` below: ```bash $ ./mill example.short bools -a -Seq(true, false, false) +Seq(true, false) $ ./mill example.short bools -b true -Seq(false, true, false) +Seq(false, true) ``` Multiple short arguments can be combined into one `-ab` call: ```scala $ ./mill example.short bools -ab true -Seq(true, true, false) +Seq(true, true) ``` Short arguments can be combined with their value: ```scala $ ./mill example.short bools -btrue -Seq(false, true, false) +Seq(false, true) ``` And you can combine both multiple short arguments as well as the resulting value: ```scala $ ./mill example.short bools -abtrue -Seq(true, true, false) +Seq(true, true) ``` Note that when multiple short arguments are combined, whether via `-ab true` or via `-abtrue`, @@ -354,6 +363,12 @@ $ ./mill example.short strs -a -b=value Seq(true, value) ``` +You can use `-b=` as a shorthand to set the value of `b` to an empty string: + +```scala +$ ./mill example.short strs -a -b= +Seq(true, ) +``` If an `=` is present in the short argument group after subsequent character, all characters except the first are passed to the first short argument. This can be useful for concisely