From 144be014405eaacd5418cc52abdd6b2650a88fec Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 19:44:48 +0800 Subject: [PATCH 1/8] . --- .mill-version | 2 +- build.sc | 6 + mainargs/src/Invoker.scala | 36 +++- mainargs/src/Parser.scala | 229 ++++++++++++++++++--- mainargs/src/Renderer.scala | 85 ++++++-- mainargs/src/TokenGrouping.scala | 24 ++- mainargs/src/TokensReader.scala | 164 +++++++++++++-- mainargs/src/Util.scala | 38 ++++ mainargs/test/src/Checker.scala | 4 +- mainargs/test/src/CoreTests.scala | 2 +- mainargs/test/src/DashedArgumentName.scala | 108 ++++++++-- mill | 2 +- readme.md | 4 + 13 files changed, 612 insertions(+), 92 deletions(-) diff --git a/.mill-version b/.mill-version index badd018..ecd2d5d 100644 --- a/.mill-version +++ b/.mill-version @@ -1,2 +1,2 @@ -0.11.3 +0.11.6 diff --git a/build.sc b/build.sc index 0f914ce..aa7a238 100644 --- a/build.sc +++ b/build.sc @@ -23,6 +23,12 @@ trait MainArgsPublishModule def publishVersion = VcsVersion.vcsState().format() + // Skip MIMA checks for Scala 3 since it's impossible to evolve the case classes + // in a way that's compatible with both Scala 2 and Scala 3 due to conflicting + // signatures for `def unapply + def mimaReportBinaryIssues(): Command[Unit] = + if (crossScalaVersion == scala3) T.command{/*do nothing*/} else super.mimaReportBinaryIssues() + override def mimaPreviousVersions = Seq("0.5.0") override def versionScheme: T[Option[VersionScheme]] = T(Some(VersionScheme.EarlySemVer)) diff --git a/mainargs/src/Invoker.scala b/mainargs/src/Invoker.scala index a937d66..732dc31 100644 --- a/mainargs/src/Invoker.scala +++ b/mainargs/src/Invoker.scala @@ -5,7 +5,15 @@ object Invoker { cep: TokensReader.Class[T], args: Seq[String], allowPositional: Boolean, - allowRepeats: Boolean + allowRepeats: Boolean, + ): Result[T] = construct(cep, args, allowPositional, allowRepeats, Util.nullNameMapper) + + def construct[T]( + cep: TokensReader.Class[T], + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + nameMapper: String => Option[String] ): Result[T] = { TokenGrouping .groupArgs( @@ -13,7 +21,8 @@ object Invoker { cep.main.flattenedArgSigs, allowPositional, allowRepeats, - cep.main.argSigs0.exists(_.reader.isLeftover) + cep.main.argSigs0.exists(_.reader.isLeftover), + nameMapper ) .flatMap((group: TokenGrouping[Any]) => invoke(cep.companion(), cep.main, group)) } @@ -82,7 +91,15 @@ object Invoker { mains: MethodMains[B], args: Seq[String], allowPositional: Boolean, - allowRepeats: Boolean + allowRepeats: Boolean): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = { + runMains(mains, args, allowPositional, allowRepeats, Util.nullNameMapper) + } + def runMains[B]( + mains: MethodMains[B], + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + nameMapper: String => Option[String] ): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = { def groupArgs(main: MainData[Any, B], argsList: Seq[String]) = { def invokeLocal(group: TokenGrouping[Any]) = @@ -98,7 +115,8 @@ object Invoker { main.argSigs0.exists { case x: ArgSig => x.reader.isLeftover case _ => false - } + }, + nameMapper ) .flatMap(invokeLocal) ) @@ -108,14 +126,16 @@ object Invoker { case Seq(main) => groupArgs(main, args) case multiple => args.toList match { - case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name))) + case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name(nameMapper)))) case head :: tail => if (head.startsWith("-")) { Left(Result.Failure.Early.SubcommandSelectionDashes(head)) } else { - multiple.find(_.name == head) match { - case None => - Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name), head)) + multiple.find{ m => + val name = m.name(nameMapper) + name == head || (m.mainName.isEmpty && m.defaultName == head) + } match { + case None => Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name(nameMapper)), head)) case Some(main) => groupArgs(main, tail) } } diff --git a/mainargs/src/Parser.scala b/mainargs/src/Parser.scala index d225b7a..4030c80 100644 --- a/mainargs/src/Parser.scala +++ b/mainargs/src/Parser.scala @@ -7,12 +7,23 @@ import java.io.PrintStream object ParserForMethods extends ParserForMethodsCompanionVersionSpecific class ParserForMethods[B](val mains: MethodMains[B]) { + @deprecated("Binary Compatibility Shim") + def helpText( + totalWidth: Int, + docsOnNewLine: Boolean, + customNames: Map[String, String], + customDocs: Map[String, String], + sorted: Boolean): String = { + helpText(totalWidth, docsOnNewLine, customNames, customDocs, sorted, Util.kebabCaseNameMapper) + } + def helpText( totalWidth: Int = 100, docsOnNewLine: Boolean = false, customNames: Map[String, String] = Map(), customDocs: Map[String, String] = Map(), - sorted: Boolean = true + sorted: Boolean = true, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): String = { Renderer.formatMainMethods( mains.value, @@ -20,7 +31,8 @@ class ParserForMethods[B](val mains: MethodMains[B]) { docsOnNewLine, customNames, customDocs, - sorted + sorted, + nameMapper ) } @@ -62,6 +74,28 @@ class ParserForMethods[B](val mains: MethodMains[B]) { } } + def runOrThrow( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + totalWidth: Int, + printHelpOnExit: Boolean, + docsOnNewLine: Boolean, + autoPrintHelpAndExit: Option[(Int, PrintStream)], + customNames: Map[String, String], + customDocs: Map[String, String], + ): Any = runOrThrow( + args, + allowPositional, + allowRepeats, + totalWidth, + printHelpOnExit, + docsOnNewLine, + autoPrintHelpAndExit, + customNames, + customDocs, + ) + def runOrThrow( args: Seq[String], allowPositional: Boolean = false, @@ -71,7 +105,8 @@ class ParserForMethods[B](val mains: MethodMains[B]) { docsOnNewLine: Boolean = false, autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), customNames: Map[String, String] = Map(), - customDocs: Map[String, String] = Map() + customDocs: Map[String, String] = Map(), + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): Any = { runEither( args, @@ -82,7 +117,8 @@ class ParserForMethods[B](val mains: MethodMains[B]) { docsOnNewLine, autoPrintHelpAndExit, customNames, - customDocs + customDocs, + nameMapper = nameMapper ) match { case Left(msg) => throw new Exception(msg) case Right(v) => v @@ -99,7 +135,8 @@ class ParserForMethods[B](val mains: MethodMains[B]) { autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), customNames: Map[String, String] = Map(), customDocs: Map[String, String] = Map(), - sorted: Boolean = false + sorted: Boolean = false, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): Either[String, Any] = { if (autoPrintHelpAndExit.nonEmpty && args.take(1) == Seq("--help")) { val (exitCode, outputStream) = autoPrintHelpAndExit.get @@ -118,8 +155,8 @@ class ParserForMethods[B](val mains: MethodMains[B]) { totalWidth, printHelpOnExit, docsOnNewLine, - customNames.get(main.name), - customDocs.get(main.name), + customNames.get(main.name(nameMapper)), + customDocs.get(main.name(nameMapper)), sorted ) ) @@ -151,23 +188,69 @@ class ParserForMethods[B](val mains: MethodMains[B]) { sorted = false ) + + + @deprecated("Binary Compatibility Shim") + def runEither( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + totalWidth: Int, + printHelpOnExit: Boolean, + docsOnNewLine: Boolean, + autoPrintHelpAndExit: Option[(Int, PrintStream)], + customNames: Map[String, String], + customDocs: Map[String, String], + sorted: Boolean + ): Either[String, Any] = runEither( + args, + allowPositional, + allowRepeats, + totalWidth, + printHelpOnExit, + docsOnNewLine, + autoPrintHelpAndExit, + customNames, + customDocs, + sorted + ) + @deprecated("Binary Compatibility Shim") + def runRaw( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + ): Result[Any] = runRaw( + args, allowPositional, allowRepeats, Util.kebabCaseNameMapper + ) def runRaw( args: Seq[String], allowPositional: Boolean = false, - allowRepeats: Boolean = false + allowRepeats: Boolean = false, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): Result[Any] = { - runRaw0(args, allowPositional, allowRepeats) match { + runRaw0(args, allowPositional, allowRepeats, nameMapper) match { case Left(err) => err case Right((main, res)) => res } } + def runRaw0( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + ): Either[Result.Failure.Early, (MainData[_, B], Result[Any])] = runRaw0( + args, + allowPositional, + allowRepeats, + Util.kebabCaseNameMapper + ) def runRaw0( args: Seq[String], allowPositional: Boolean = false, - allowRepeats: Boolean = false + allowRepeats: Boolean = false, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): Either[Result.Failure.Early, (MainData[_, B], Result[Any])] = { - for (tuple <- Invoker.runMains(mains, args, allowPositional, allowRepeats)) yield { + for (tuple <- Invoker.runMains(mains, args, allowPositional, allowRepeats, nameMapper)) yield { val (errMsg, res) = tuple (errMsg, res) } @@ -177,12 +260,21 @@ class ParserForMethods[B](val mains: MethodMains[B]) { object ParserForClass extends ParserForClassCompanionVersionSpecific class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) extends TokensReader.Class[T] { + @deprecated("Binary Compatibility Shim") + def helpText( + totalWidth: Int, + docsOnNewLine: Boolean, + customName: String, + customDoc: String, + sorted: Boolean): String = helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted, Util.kebabCaseNameMapper) + def helpText( totalWidth: Int = 100, docsOnNewLine: Boolean = false, customName: String = null, customDoc: String = null, - sorted: Boolean = true + sorted: Boolean = true, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): String = { Renderer.formatMainMethodSignature( main, @@ -192,7 +284,8 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine, Option(customName), Option(customDoc), - sorted + sorted, + nameMapper ) } @@ -204,6 +297,31 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) customDoc: String ): String = helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted = true) + @deprecated("Binary Compatibility Shim") + def constructOrExit( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + stderr: PrintStream, + totalWidth: Int, + printHelpOnExit: Boolean, + docsOnNewLine: Boolean, + autoPrintHelpAndExit: Option[(Int, PrintStream)], + customName: String, + customDoc: String): T = constructOrExit( + args, + allowPositional, + allowRepeats, + stderr, + totalWidth, + printHelpOnExit, + docsOnNewLine, + autoPrintHelpAndExit, + customName, + customDoc, + Util.kebabCaseNameMapper + ) + def constructOrExit( args: Seq[String], allowPositional: Boolean = false, @@ -214,7 +332,8 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine: Boolean = false, autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), customName: String = null, - customDoc: String = null + customDoc: String = null, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): T = { constructEither( args, @@ -225,7 +344,8 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine, autoPrintHelpAndExit, customName, - customDoc + customDoc, + nameMapper ) match { case Left(msg) => stderr.println(msg) @@ -234,6 +354,29 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) } } + def constructOrThrow( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + totalWidth: Int, + printHelpOnExit: Boolean, + docsOnNewLine: Boolean, + autoPrintHelpAndExit: Option[(Int, PrintStream)], + customName: String, + customDoc: String, + ): T = constructOrThrow( + args, + allowPositional, + allowRepeats, + totalWidth, + printHelpOnExit, + docsOnNewLine, + autoPrintHelpAndExit, + customName, + customDoc, + Util.kebabCaseNameMapper + ) + def constructOrThrow( args: Seq[String], allowPositional: Boolean = false, @@ -243,7 +386,8 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine: Boolean = false, autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), customName: String = null, - customDoc: String = null + customDoc: String = null, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): T = { constructEither( args, @@ -254,13 +398,37 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine, autoPrintHelpAndExit, customName, - customDoc + customDoc, + nameMapper ) match { case Left(msg) => throw new Exception(msg) case Right(v) => v } } + def constructEither( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + totalWidth: Int, + printHelpOnExit: Boolean, + docsOnNewLine: Boolean, + autoPrintHelpAndExit: Option[(Int, PrintStream)], + customName: String, + customDoc: String, + sorted: Boolean, + ): Either[String, T] = constructEither( + args, + allowPositional, + allowRepeats, + totalWidth, + printHelpOnExit, + docsOnNewLine, + autoPrintHelpAndExit, + customName, + customDoc, + sorted, + ) def constructEither( args: Seq[String], allowPositional: Boolean = false, @@ -271,13 +439,14 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) autoPrintHelpAndExit: Option[(Int, PrintStream)] = Some((0, System.out)), customName: String = null, customDoc: String = null, - sorted: Boolean = true + sorted: Boolean = true, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): Either[String, T] = { if (autoPrintHelpAndExit.nonEmpty && args.take(1) == Seq("--help")) { val (exitCode, outputStream) = autoPrintHelpAndExit.get outputStream.println(helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted)) Compat.exit(exitCode) - } else constructRaw(args, allowPositional, allowRepeats) match { + } else constructRaw(args, allowPositional, allowRepeats, nameMapper) match { case Result.Success(v) => Right(v) case f: Result.Failure => Left( @@ -305,7 +474,8 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine: Boolean, autoPrintHelpAndExit: Option[(Int, PrintStream)], customName: String, - customDoc: String + customDoc: String, + nameMapper: String => Option[String] ): Either[String, T] = constructEither( args, allowPositional, @@ -316,14 +486,27 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) autoPrintHelpAndExit, customName, customDoc, - sorted = true + sorted = true, + nameMapper = nameMapper + ) + + def constructRaw( + args: Seq[String], + allowPositional: Boolean, + allowRepeats: Boolean, + ): Result[T] = constructRaw( + args, + allowPositional, + allowRepeats, + nameMapper = Util.kebabCaseNameMapper ) def constructRaw( args: Seq[String], allowPositional: Boolean = false, - allowRepeats: Boolean = false + allowRepeats: Boolean = false, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): Result[T] = { - Invoker.construct[T](this, args, allowPositional, allowRepeats) + Invoker.construct[T](this, args, allowPositional, allowRepeats, nameMapper) } } diff --git a/mainargs/src/Renderer.scala b/mainargs/src/Renderer.scala index b70fb01..400c707 100644 --- a/mainargs/src/Renderer.scala +++ b/mainargs/src/Renderer.scala @@ -67,7 +67,16 @@ object Renderer { docsOnNewLine: Boolean, customNames: Map[String, String], customDocs: Map[String, String], - sorted: Boolean + sorted: Boolean, + ): String = formatMainMethods(mainMethods, totalWidth, docsOnNewLine, customNames, customDocs, sorted, Util.kebabCaseNameMapper) + def formatMainMethods( + mainMethods: Seq[MainData[_, _]], + totalWidth: Int, + docsOnNewLine: Boolean, + customNames: Map[String, String], + customDocs: Map[String, String], + sorted: Boolean, + nameMapper: String => Option[String] ): String = { val flattenedAll: Seq[ArgSig] = mainMethods.map(_.flattenedArgSigs) @@ -84,9 +93,10 @@ object Renderer { totalWidth, leftColWidth, docsOnNewLine, - customNames.get(main.name), - customDocs.get(main.name), - sorted + customNames.get(main.name(nameMapper)), + customDocs.get(main.name(nameMapper)), + sorted, + nameMapper ) case _ => val methods = @@ -97,9 +107,10 @@ object Renderer { totalWidth, leftColWidth, docsOnNewLine, - customNames.get(main.name), - customDocs.get(main.name), - sorted + customNames.get(main.name(nameMapper)), + customDocs.get(main.name(nameMapper)), + sorted, + nameMapper ) normalizeNewlines( @@ -116,14 +127,15 @@ object Renderer { totalWidth: Int, docsOnNewLine: Boolean, customNames: Map[String, String], - customDocs: Map[String, String] + customDocs: Map[String, String], ): String = formatMainMethods( mainMethods, totalWidth, docsOnNewLine, customNames, customDocs, - sorted = true + sorted = true, + Util.kebabCaseNameMapper ) def formatMainMethodSignature( @@ -134,7 +146,8 @@ object Renderer { docsOnNewLine: Boolean, customName: Option[String], customDoc: Option[String], - sorted: Boolean + sorted: Boolean, + nameMapper: String => Option[String] ): String = { val argLeftCol = if (docsOnNewLine) leftIndent + 8 else leftColWidth + leftIndent + 2 + 2 @@ -163,7 +176,7 @@ object Renderer { case Some(d) => newLine + leftIndentStr + softWrap(d, leftIndent, totalWidth) case None => "" } - s"""$leftIndentStr${customName.getOrElse(main.name)}$mainDocSuffix + s"""$leftIndentStr${customName.getOrElse(main.name(nameMapper))}$mainDocSuffix |${argStrings.map(_ + newLine).mkString}""".stripMargin } @@ -184,7 +197,29 @@ object Renderer { docsOnNewLine, customName, customDoc, - sorted = true + sorted = true, + ) + + @deprecated("Binary Compatibility Shim") + def formatMainMethodSignature( + main: MainData[_, _], + leftIndent: Int, + totalWidth: Int, + leftColWidth: Int, + docsOnNewLine: Boolean, + customName: Option[String], + customDoc: Option[String], + sorted: Boolean + ): String = formatMainMethodSignature( + main, + leftIndent, + totalWidth, + leftColWidth, + docsOnNewLine, + customName, + customDoc, + sorted, + Util.kebabCaseNameMapper ) def softWrap(s: String, leftOffset: Int, maxWidth: Int) = { @@ -233,7 +268,28 @@ object Renderer { docsOnNewLine: Boolean, customName: Option[String], customDoc: Option[String], - sorted: Boolean + sorted: Boolean, + ): String = renderResult( + main, + result, + totalWidth, + printHelpOnError, + docsOnNewLine, + customName, + customDoc, + sorted, + Util.kebabCaseNameMapper + ) + def renderResult( + main: MainData[_, _], + result: Result.Failure, + totalWidth: Int, + printHelpOnError: Boolean, + docsOnNewLine: Boolean, + customName: Option[String], + customDoc: Option[String], + sorted: Boolean, + nameMapper: String => Option[String] = Util.kebabCaseNameMapper ): String = { def expectedMsg() = { @@ -248,7 +304,8 @@ object Renderer { docsOnNewLine, customName, customDoc, - sorted + sorted, + nameMapper ) } else "" } diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index 077caf6..df66e9b 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -5,20 +5,32 @@ import scala.annotation.tailrec case class TokenGrouping[B](remaining: List[String], grouped: Map[ArgSig, Seq[String]]) object TokenGrouping { + @deprecated("Binary Compatibility Shim") def groupArgs[B]( flatArgs0: Seq[String], argSigs: Seq[(ArgSig, TokensReader.Terminal[_])], allowPositional: Boolean, allowRepeats: Boolean, - allowLeftover: Boolean + allowLeftover: Boolean, ): Result[TokenGrouping[B]] = { - val positionalArgSigs = argSigs.collect { + groupArgs(flatArgs0, argSigs, allowPositional, allowRepeats, allowLeftover, _ => None) + } + + def groupArgs[B]( + flatArgs0: Seq[String], + argSigs: Seq[(ArgSig, TokensReader.Terminal[_])], + allowPositional: Boolean, + allowRepeats: Boolean, + allowLeftover: Boolean, + nameMapper: String => Option[String] + ): Result[TokenGrouping[B]] = { + val positionalArgSigs: Seq[ArgSig] = argSigs.collect { case (a, r: TokensReader.Simple[_]) if allowPositional | a.positional => a } val flatArgs = flatArgs0.toList - def makeKeywordArgMap(getNames: ArgSig => Iterable[String]) = argSigs + def makeKeywordArgMap(getNames: ArgSig => Iterable[String]): Map[String, ArgSig] = argSigs .collect { case (a, r: TokensReader.Simple[_]) if !a.positional => a case (a, r: TokensReader.Flag) => a @@ -27,10 +39,12 @@ object TokenGrouping { .toMap[String, ArgSig] lazy val keywordArgMap = makeKeywordArgMap( - x => x.name.map("--" + _) ++ x.shortName.map("-" + _) + x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.name.map("--" + _) ++ x.shortName.map("-" + _) ) - lazy val longKeywordArgMap = makeKeywordArgMap(x => x.name.map("--" + _)) + lazy val longKeywordArgMap = makeKeywordArgMap( + x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.name.map("--" + _) + ) @tailrec def rec( remaining: List[String], diff --git a/mainargs/src/TokensReader.scala b/mainargs/src/TokensReader.scala index 42449d0..36f9ec9 100644 --- a/mainargs/src/TokensReader.scala +++ b/mainargs/src/TokensReader.scala @@ -223,15 +223,15 @@ object TokensReader { object ArgSig { def create[T, B](name0: String, arg: mainargs.arg, defaultOpt: Option[B => T]) (implicit tokensReader: TokensReader[T]): ArgSig = { - val nameOpt = scala.Option(arg.name).orElse(if (name0.length == 1 || arg.noDefaultName) None - else Some(name0)) val shortOpt = arg.short match { case '\u0000' => if (name0.length != 1 || arg.noDefaultName) None else Some(name0(0)); case c => Some(c) } + val docOpt = scala.Option(arg.doc) - ArgSig( - nameOpt, + new ArgSig( + if (arg.noDefaultName) None else Some(name0), + scala.Option(arg.name), shortOpt, docOpt, defaultOpt.asInstanceOf[Option[Any => Any]], @@ -245,6 +245,23 @@ object ArgSig { case r: TokensReader.Terminal[T] => Seq((x, r)) case cls: TokensReader.Class[_] => cls.main.argSigs0.flatMap(flatten(_)) } + + @deprecated("Binary Compatibility Shim") + def apply( + name: Option[String], + shortName: Option[Char], + doc: Option[String], + default: Option[Any => Any], + reader: TokensReader[_], + positional: Boolean, + hidden: Boolean + ) = { + new ArgSig(name, name, shortName, doc, default, reader, positional, hidden) + } + + def unapply(a: ArgSig) = Option( + (a.name, a.shortName, a.doc, a.default, a.reader, a.positional, a.hidden) + ) } /** @@ -253,15 +270,67 @@ object ArgSig { * (just for logging and reading, not a replacement for a `TypeTag`) and * possible a function that can compute its default value */ -case class ArgSig( - name: Option[String], - shortName: Option[Char], - doc: Option[String], - default: Option[Any => Any], - reader: TokensReader[_], - positional: Boolean, - hidden: Boolean -) +class ArgSig private[mainargs] ( + val defaultName: Option[String], + val argName: Option[String], + val shortName: Option[Char], + val doc: Option[String], + val default: Option[Any => Any], + val reader: TokensReader[_], + val positional: Boolean, + val hidden: Boolean +) extends Product with Serializable with Equals{ + override def canEqual(that: Any): Boolean = true + + override def hashCode(): Int = ArgSig.unapply(this).hashCode() + override def equals(o: Any): Boolean = o match { + case other: ArgSig => ArgSig.unapply(this) == ArgSig.unapply(other) + case _ => false + } + + @deprecated("Binary Compatibility Shim") + def this( + name: Option[String], + shortName: Option[Char], + doc: Option[String], + default: Option[Any => Any], + reader: TokensReader[_], + positional: Boolean, + hidden: Boolean + ) = { + this(name, name, shortName, doc, default, reader, positional, hidden) + } + + @deprecated("Binary Compatibility Shim") + def copy(name: Option[String] = this.name, + shortName: Option[Char] = this.shortName, + doc: Option[String] = this.doc, + default: Option[Any => Any] = this.default, + reader: TokensReader[_] = this.reader, + positional: Boolean = this.positional, + hidden: Boolean = this.hidden) = { + ArgSig(name, shortName, doc, default, reader, positional, hidden) + } + + @deprecated("Binary Compatibility Shim") + def productArity = 9 + @deprecated("Binary Compatibility Shim") + def productElement(n: Int) = n match{ + case 0 => defaultName + case 1 => argName + case 2 => shortName + case 3 => doc + case 4 => default + case 5 => reader + case 6 => positional + case 7 => hidden + } + + def name: Option[String] = argName.orElse(defaultName.flatMap(d => if (d.length == 1) None else Some(d))) + def mappedName(nameMapper: String => Option[String]): Option[String] = + if (argName.isDefined) None else defaultName.flatMap(nameMapper) +} + case class MethodMains[B](value: Seq[MainData[Any, B]], base: () => B) @@ -274,12 +343,55 @@ case class MethodMains[B](value: Seq[MainData[Any, B]], base: () => B) * instead, which provides a nicer API to call it that mimmicks the API of * calling a Scala method. */ -case class MainData[T, B]( - name: String, - argSigs0: Seq[ArgSig], - doc: Option[String], - invokeRaw: (B, Seq[Any]) => T -) { +class MainData[T, B] private[mainargs] ( + val mainName: Option[String], + val defaultName: String, + val argSigs0: Seq[ArgSig], + val doc: Option[String], + val invokeRaw: (B, Seq[Any]) => T +) extends Product with Serializable with Equals{ + @deprecated("Binary Compatibility Shim") + def productArity = 5 + @deprecated("Binary Compatibility Shim") + def productElement(n: Int) = n match{ + case 0 => mainName + case 1 => defaultName + case 2 => argSigs0 + case 3 => doc + case 4 => invokeRaw + } + @deprecated("Binary Compatibility Shim") + def copy(name: String = this.name, + argSigs0: Seq[ArgSig] = this.argSigs0, + doc: Option[String] = this.doc, + invokeRaw: (B, Seq[Any]) => T = this.invokeRaw) = MainData( + name, argSigs0, doc, invokeRaw + ) + @deprecated("Binary Compatibility Shim") + def this(name: String, + argSigs0: Seq[ArgSig], + doc: Option[String], + invokeRaw: (B, Seq[Any]) => T) = this( + Some(name), name, argSigs0, doc, invokeRaw + ) + @deprecated("Binary Compatibility Shim") + override def hashCode(): Int = MainData.unapply(this).hashCode() + + @deprecated("Binary Compatibility Shim") + override def equals(obj: Any): Boolean = obj match{ + case x: MainData[_, _] => MainData.unapply(x) == MainData.unapply(this) + case _ => false + } + + @deprecated("Binary Compatibility Shim") + override def canEqual(that: Any): Boolean = true + + def name: String = mainName.getOrElse(defaultName) + + def name(nameMapper: String => Option[String]) = mappedName(nameMapper).getOrElse(defaultName) + def mappedName(nameMapper: String => Option[String]): Option[String] = + if (mainName.isDefined) None + else nameMapper(defaultName) val flattenedArgSigs: Seq[(ArgSig, TokensReader.Terminal[_])] = argSigs0.iterator.flatMap[(ArgSig, TokensReader.Terminal[_])](ArgSig.flatten(_)).toVector @@ -289,14 +401,24 @@ case class MainData[T, B]( } object MainData { + @deprecated("Binary Compatibility Shim") + def unapply[T, B](x: MainData[T, B]) = Option((x.mainName, x.defaultName, x.argSigs0, x.doc, x.invokeRaw)) + @deprecated("Binary Compatibility Shim") + def apply[T, B](name: String, + argSigs0: Seq[ArgSig], + doc: Option[String], + invokeRaw: (B, Seq[Any]) => T) = { + new MainData(Some(name), name, argSigs0, doc, invokeRaw) + } def create[T, B]( methodName: String, main: mainargs.main, argSigs: Seq[ArgSig], invokeRaw: (B, Seq[Any]) => T ) = { - MainData( - Option(main.name).getOrElse(methodName), + new MainData( + Option(main.name), + methodName, argSigs, Option(main.doc), invokeRaw diff --git a/mainargs/src/Util.scala b/mainargs/src/Util.scala index b89bebc..b79390c 100644 --- a/mainargs/src/Util.scala +++ b/mainargs/src/Util.scala @@ -3,6 +3,44 @@ package mainargs import scala.annotation.{switch, tailrec} object Util { + def nullNameMapper(s: String): Option[String] = None + + def kebabCaseNameMapper(s: String): Option[String] = { + baseNameMapper(s, "-") + } + def snakeCaseNameMapper(s: String): Option[String] = { + baseNameMapper(s, "_") + } + + def baseNameMapper(s: String, sep: String): Option[String] = { + val chars = new collection.mutable.StringBuilder + // 'D' -> digit + // 'U' -> uppercase + // 'L' -> lowercase + // 'O' -> other + var state = ' ' + + for (c <- s) { + if (c.isDigit){ + if (state == 'L' || state == 'U') chars.append(sep) + chars.append(c) + state = 'D' + } else if (c.isUpper) { + if (state == 'L' || state == 'D') chars.append(sep) + chars.append(c.toLower) + state = 'U' + } else if (c.isLower){ + chars.append(c) + state = 'L' + } else { + state = 'O' + chars.append(c) + } + } + println(chars.toString()) + Some(chars.toString()) + } + def literalize(s: IndexedSeq[Char], unicode: Boolean = false) = { val sb = new StringBuilder sb.append('"') diff --git a/mainargs/test/src/Checker.scala b/mainargs/test/src/Checker.scala index 6a4e4d3..a1788e6 100644 --- a/mainargs/test/src/Checker.scala +++ b/mainargs/test/src/Checker.scala @@ -1,9 +1,9 @@ package mainargs -class Checker[B](val parser: ParserForMethods[B], allowPositional: Boolean) { +class Checker[B](val parser: ParserForMethods[B], allowPositional: Boolean, nameMapper: String => Option[String] = Util.kebabCaseNameMapper) { val mains = parser.mains def parseInvoke(input: List[String]) = { - parser.runRaw(input, allowPositional = allowPositional) + parser.runRaw(input, allowPositional = allowPositional, nameMapper = nameMapper) } def apply[T](input: List[String], expected: Result[T]) = { val result = parseInvoke(input) diff --git a/mainargs/test/src/CoreTests.scala b/mainargs/test/src/CoreTests.scala index 03e2aae..df6eaeb 100644 --- a/mainargs/test/src/CoreTests.scala +++ b/mainargs/test/src/CoreTests.scala @@ -52,7 +52,7 @@ class CoreTests(allowPositional: Boolean) extends TestSuite { parsed ==> expected } test("basicModelling") { - val names = check.mains.value.map(_.name) + val names = check.mains.value.map(_.name(Util.nullNameMapper)) assert( names == diff --git a/mainargs/test/src/DashedArgumentName.scala b/mainargs/test/src/DashedArgumentName.scala index 8b49e69..f21a812 100644 --- a/mainargs/test/src/DashedArgumentName.scala +++ b/mainargs/test/src/DashedArgumentName.scala @@ -6,27 +6,103 @@ object DashedArgumentName extends TestSuite { object Base { @main def `opt-for-18+name`(`opt-for-18+arg`: Boolean) = `opt-for-18+arg` + @main def `opt-for-29+name`(`opt-for-29+arg`: Boolean) = `opt-for-29+arg` + + @main + def camelOptFor29Name(camelOptFor29Arg: Boolean) = camelOptFor29Arg + + @main(name = "camelOptFor29NameForce") + def camelOptFor29NameForce(@arg(name = "camelOptFor29ArgForce") camelOptFor29ArgForce: Boolean) = camelOptFor29ArgForce } val check = new Checker(ParserForMethods(Base), allowPositional = true) + val snakeCaseCheck = new Checker(ParserForMethods(Base), allowPositional = true, nameMapper = Util.snakeCaseNameMapper) val tests = Tests { - test - check( - List("opt-for-18+name", "--opt-for-18+arg", "true"), - Result.Success(true) - ) - test - check( - List("opt-for-18+name", "--opt-for-18+arg", "false"), - Result.Success(false) - ) - test - check( - List("opt-for-29+name", "--opt-for-29+arg", "true"), - Result.Success(true) - ) - test - check( - List("opt-for-29+name", "--opt-for-29+arg", "false"), - Result.Success(false) - ) + test("backticked") { + test - check( + List("opt-for-18+name", "--opt-for-18+arg", "true"), + Result.Success(true) + ) + test - check( + List("opt-for-18+name", "--opt-for-18+arg", "false"), + Result.Success(false) + ) + test - check( + List("opt-for-29+name", "--opt-for-29+arg", "true"), + Result.Success(true) + ) + test - check( + List("opt-for-29+name", "--opt-for-29+arg", "false"), + Result.Success(false) + ) + } + test("camelKebabNameMapped") { + test("mapped") - check( + List("camel-opt-for-29-name", "--camel-opt-for-29-arg", "false"), + Result.Success(false) + ) + + // Make sure we continue to support un-mapped names for backwards compatibility + test("backwardsCompatUnmapped") - check( + List("camelOptFor29Name", "--camelOptFor29Arg", "false"), + Result.Success(false) + ) + + test("explicitNameUnmapped") - check( + List("camelOptFor29NameForce", "--camelOptFor29ArgForce", "false"), + Result.Success(false) + ) + + // For names given explicitly via `main(name = ...)` or `arg(name = ...)`, we + // do not use a name mapper, since we assume the user would provide the exact + // name they want. + test("explicitMainNameMappedFails") - check( + List("camel-opt-for-29-name-force", "--camel-opt-for-29-arg-force", "false"), + Result.Failure.Early.UnableToFindSubcommand( + List("opt-for-18+name", "opt-for-29+name", "camel-opt-for-29-name", "camelOptFor29NameForce"), + "camel-opt-for-29-name-force" + ) + ) + test("explicitArgNameMappedFails") - check( + List("camelOptFor29NameForce", "--camel-opt-for-29-arg-force", "false"), + Result.Failure.MismatchedArguments( + Vector( + new ArgSig( + Some("camelOptFor29ArgForce"), + Some("camelOptFor29ArgForce"), + None, + None, + None, + mainargs.TokensReader.BooleanRead, + positional = false, + hidden = false + ) + ), + List("--camel-opt-for-29-arg-force", "false"), + List(), + None + ) + + ) + } + test("camelSnakeNameMapped") { + test("mapped") - snakeCaseCheck( + List("camel_opt_for_29_name", "--camel_opt_for_29_arg", "false"), + Result.Success(false) + ) + + // Make sure we continue to support un-mapped names for backwards compatibility + test("backwardsCompatUnmapped") - check( + List("camelOptFor29Name", "--camelOptFor29Arg", "false"), + Result.Success(false) + ) + + test("explicitNameUnmapped") - check( + List("camelOptFor29NameForce", "--camelOptFor29ArgForce", "false"), + Result.Success(false) + ) + } } } diff --git a/mill b/mill index cb1ee32..0c5078a 100755 --- a/mill +++ b/mill @@ -7,7 +7,7 @@ set -e if [ -z "${DEFAULT_MILL_VERSION}" ] ; then - DEFAULT_MILL_VERSION=0.11.0 + DEFAULT_MILL_VERSION=0.11.6 fi if [ -z "$MILL_VERSION" ] ; then diff --git a/readme.md b/readme.md index 79afd0f..87bc335 100644 --- a/readme.md +++ b/readme.md @@ -519,6 +519,10 @@ command-line friendly tool. # Changelog +## 0.5.5 + +- Automatically map a + ## 0.5.4 - Remove unnecessary PPrint dependency From 5839da195e9044f8c229a5d4cbc86818d7715fa1 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 19:45:06 +0800 Subject: [PATCH 2/8] . --- mainargs/src/Util.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainargs/src/Util.scala b/mainargs/src/Util.scala index b79390c..6207ff4 100644 --- a/mainargs/src/Util.scala +++ b/mainargs/src/Util.scala @@ -37,7 +37,7 @@ object Util { chars.append(c) } } - println(chars.toString()) + Some(chars.toString()) } From eeb35f56f6565db23b74360100236be55b35026d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 19:46:59 +0800 Subject: [PATCH 3/8] . --- mainargs/src/Util.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mainargs/src/Util.scala b/mainargs/src/Util.scala index 6207ff4..d4e894f 100644 --- a/mainargs/src/Util.scala +++ b/mainargs/src/Util.scala @@ -6,13 +6,13 @@ object Util { def nullNameMapper(s: String): Option[String] = None def kebabCaseNameMapper(s: String): Option[String] = { - baseNameMapper(s, "-") + baseNameMapper(s, '-') } def snakeCaseNameMapper(s: String): Option[String] = { - baseNameMapper(s, "_") + baseNameMapper(s, '_') } - def baseNameMapper(s: String, sep: String): Option[String] = { + def baseNameMapper(s: String, sep: Char): Option[String] = { val chars = new collection.mutable.StringBuilder // 'D' -> digit // 'U' -> uppercase From 6bbd2435e4eb3f5ca07dc9df730aaeacfd668d6b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 19:51:24 +0800 Subject: [PATCH 4/8] . --- readme.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 87bc335..52b1295 100644 --- a/readme.md +++ b/readme.md @@ -350,6 +350,11 @@ of useful configuration values: - `sorted: Boolean`: whether to sort the arguments alphabetically in the help text. Defaults to `true` +- `nameMapper: String => Option[String]`: how Scala `camelCase` names are mapping + to CLI command and flag names. Defaults to translation to `kebab-case`, but + you can pass in `mainargs.Util.snakeCaseNameMapper` for `snake_case` CLI names + or `mainargs.Util.nullNameMapper` to disable mapping. + ## Custom Argument Parsers If you want to parse arguments into types that are not provided by the library, @@ -519,9 +524,11 @@ command-line friendly tool. # Changelog -## 0.5.5 +## master -- Automatically map a +- Automatically map `camelCase` Scala method and argument names to `kebab-case` + CLI commands and flag names, with configurability by passing in custom + `nameMappers` [#101](https://github.com/com-lihaoyi/mainargs/pull/101) ## 0.5.4 From 8b61ba8ae0d9dcb6ac8c189e852027243726d5f3 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 20:48:02 +0800 Subject: [PATCH 5/8] readme --- mainargs/test/src/EqualsSyntaxTests.scala | 2 +- readme.md | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/mainargs/test/src/EqualsSyntaxTests.scala b/mainargs/test/src/EqualsSyntaxTests.scala index 06aed69..56d6872 100644 --- a/mainargs/test/src/EqualsSyntaxTests.scala +++ b/mainargs/test/src/EqualsSyntaxTests.scala @@ -8,7 +8,7 @@ object EqualsSyntaxTests extends TestSuite { def run( @arg(short = 'f', doc = "String to print repeatedly") foo: String, - @arg(name = "my-num", doc = "How many times to print string") + @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag diff --git a/readme.md b/readme.md index 52b1295..c51409e 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# mainargs 0.5.4 +# mainargs 0.5.5 MainArgs is a small, dependency-free library for command line argument parsing in Scala. @@ -45,7 +45,7 @@ in its scripts, as well as for command-line parsing for the # Usage ```scala -ivy"com.lihaoyi::mainargs:0.5.4" +ivy"com.lihaoyi::mainargs:0.5.5" ``` ## Parsing Main Method Parameters @@ -61,7 +61,7 @@ object Main{ @main def run(@arg(short = 'f', doc = "String to print repeatedly") foo: String, - @arg(name = "my-num", doc = "How many times to print string") + @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag, can be passed without any value to become true") bool: Flag) = { @@ -133,7 +133,7 @@ object Main{ @main def foo(@arg(short = 'f', doc = "String to print repeatedly") foo: String, - @arg(name = "my-num", doc = "How many times to print string") + @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag) = { @@ -173,7 +173,7 @@ object Main{ @main case class Config(@arg(short = 'f', doc = "String to print repeatedly") foo: String, - @arg(name = "my-num", doc = "How many times to print string") + @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag) @@ -214,7 +214,7 @@ object Main{ @main case class Config(@arg(short = 'f', doc = "String to print repeatedly") foo: String, - @arg(name = "my-num", doc = "How many times to print string") + @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag) @@ -296,7 +296,9 @@ customize your usage: - `name: String`: lets you specify the top-level name of `@main` method you are defining. If multiple `@main` methods are provided, this name controls the - sub-command name in the CLI + sub-command name in the CLI. If an explicit `name` not passed, both the + (typically) `camelCase` name of the Scala `def` as well as its `kebab-case` + equivalents will be accepted - `doc: String`: a documentation string used to provide additional information about the command. Normally printed below the command name in the help message @@ -304,7 +306,9 @@ customize your usage: ### @arg - `name: String`: lets you specify the long name of a CLI parameter, e.g. - `--foo`. Defaults to the name of the function parameter if not given + `--foo`. If an explicit `name` not passed, both the (typically) `camelCase` + name of the Scala method parameter as well as its `kebab-case` + equivalents will be accepted - `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 @@ -524,7 +528,7 @@ command-line friendly tool. # Changelog -## master +## 0.5.5 - Automatically map `camelCase` Scala method and argument names to `kebab-case` CLI commands and flag names, with configurability by passing in custom From 332009e01f6d915b7828ac57f87ac69137cc22e3 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 21:50:00 +0800 Subject: [PATCH 6/8] . --- mainargs/src/Parser.scala | 14 ++++-- mainargs/src/Renderer.scala | 55 +++++++++++++--------- mainargs/src/TokenGrouping.scala | 4 +- mainargs/src/TokensReader.scala | 81 ++++++++++++++++---------------- 4 files changed, 85 insertions(+), 69 deletions(-) diff --git a/mainargs/src/Parser.scala b/mainargs/src/Parser.scala index 4030c80..9ec45c7 100644 --- a/mainargs/src/Parser.scala +++ b/mainargs/src/Parser.scala @@ -140,9 +140,9 @@ class ParserForMethods[B](val mains: MethodMains[B]) { ): Either[String, Any] = { if (autoPrintHelpAndExit.nonEmpty && args.take(1) == Seq("--help")) { val (exitCode, outputStream) = autoPrintHelpAndExit.get - outputStream.println(helpText(totalWidth, docsOnNewLine, customNames, customDocs, sorted)) + outputStream.println(helpText(totalWidth, docsOnNewLine, customNames, customDocs, sorted, nameMapper)) Compat.exit(exitCode) - } else runRaw0(args, allowPositional, allowRepeats) match { + } else runRaw0(args, allowPositional, allowRepeats, nameMapper) match { case Left(err) => Left(Renderer.renderEarlyError(err)) case Right((main, res)) => res match { @@ -157,7 +157,8 @@ class ParserForMethods[B](val mains: MethodMains[B]) { docsOnNewLine, customNames.get(main.name(nameMapper)), customDocs.get(main.name(nameMapper)), - sorted + sorted, + nameMapper ) ) } @@ -234,6 +235,7 @@ class ParserForMethods[B](val mains: MethodMains[B]) { } } + @deprecated("Binary Compatibility Shim") def runRaw0( args: Seq[String], allowPositional: Boolean, @@ -244,6 +246,7 @@ class ParserForMethods[B](val mains: MethodMains[B]) { allowRepeats, Util.kebabCaseNameMapper ) + def runRaw0( args: Seq[String], allowPositional: Boolean = false, @@ -280,7 +283,7 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) main, 0, totalWidth, - Renderer.getLeftColWidth(main.renderedArgSigs), + Renderer.getLeftColWidth(main.renderedArgSigs, nameMapper), docsOnNewLine, Option(customName), Option(customDoc), @@ -458,7 +461,8 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) docsOnNewLine, Option(customName), Option(customDoc), - sorted + sorted, + nameMapper ) ) } diff --git a/mainargs/src/Renderer.scala b/mainargs/src/Renderer.scala index 400c707..dde8c18 100644 --- a/mainargs/src/Renderer.scala +++ b/mainargs/src/Renderer.scala @@ -5,60 +5,69 @@ import scala.math object Renderer { - def getLeftColWidth(items: Seq[ArgSig]) = { + def getLeftColWidth(items: Seq[ArgSig]): Int = getLeftColWidth(items, Util.kebabCaseNameMapper) + def getLeftColWidth(items: Seq[ArgSig], nameMapper: String => Option[String]): Int = { if (items.isEmpty) 0 - else items.map(renderArgShort(_).length).max + else items.map(renderArgShort(_, nameMapper).length).max } val newLine = System.lineSeparator() def normalizeNewlines(s: String) = s.replace("\r", "").replace("\n", newLine) - def renderArgShort(arg: ArgSig) = arg.reader match { + def renderArgShort(arg: ArgSig): String = renderArgShort(arg, Util.nullNameMapper) + + def renderArgShort(arg: ArgSig, nameMapper: String => Option[String]): String = arg.reader match { case r: TokensReader.Flag => val shortPrefix = arg.shortName.map(c => s"-$c") - val nameSuffix = arg.name.map(s => s"--$s") + val nameSuffix = arg.longName(nameMapper).map(s => s"--$s") (shortPrefix ++ nameSuffix).mkString(" ") case r: TokensReader.Simple[_] => val shortPrefix = arg.shortName.map(c => s"-$c") val typeSuffix = s"<${r.shortName}>" - val nameSuffix = if (arg.positional) arg.name else arg.name.map(s => s"--$s") + val nameSuffix = if (arg.positional) arg.longName(nameMapper) else arg.longName(nameMapper).map(s => s"--$s") (shortPrefix ++ nameSuffix ++ Seq(typeSuffix)).mkString(" ") case r: TokensReader.Leftover[_, _] => - s"${arg.name.get} <${r.shortName}>..." + s"${arg.longName(nameMapper).get} <${r.shortName}>..." } /** * Returns a `Some[string]` with the sortable string or a `None` if it is an leftover. */ - private def sortableName(arg: ArgSig): Option[String] = arg match { + private def sortableName(arg: ArgSig, nameMapper: String => Option[String]): Option[String] = arg match { case arg: ArgSig if arg.reader.isLeftover => None case a: ArgSig => - a.shortName.map(_.toString).orElse(a.name).orElse(Some("")) + a.shortName.map(_.toString).orElse(a.longName(nameMapper)).orElse(Some("")) case a: ArgSig => - a.name.orElse(Some("")) + a.longName(nameMapper) } object ArgOrd extends math.Ordering[ArgSig] { override def compare(x: ArgSig, y: ArgSig): Int = - (sortableName(x), sortableName(y)) match { + (sortableName(x, Util.nullNameMapper), sortableName(y, Util.nullNameMapper)) match { case (None, None) => 0 // don't sort leftovers case (None, Some(_)) => 1 // keep left overs at the end case (Some(_), None) => -1 // keep left overs at the end case (Some(l), Some(r)) => l.compare(r) } } + @deprecated("Binary Compatibility Shim") + def renderArg( + arg: ArgSig, + leftOffset: Int, + wrappedWidth: Int): (String, String) = renderArg(arg, leftOffset, wrappedWidth, Util.kebabCaseNameMapper) def renderArg( arg: ArgSig, leftOffset: Int, - wrappedWidth: Int + wrappedWidth: Int, + nameMapper: String => Option[String] ): (String, String) = { val wrapped = softWrap(arg.doc.getOrElse(""), leftOffset, wrappedWidth - leftOffset) - (renderArgShort(arg), wrapped) + (renderArgShort(arg, nameMapper), wrapped) } def formatMainMethods( @@ -69,6 +78,7 @@ object Renderer { customDocs: Map[String, String], sorted: Boolean, ): String = formatMainMethods(mainMethods, totalWidth, docsOnNewLine, customNames, customDocs, sorted, Util.kebabCaseNameMapper) + def formatMainMethods( mainMethods: Seq[MainData[_, _]], totalWidth: Int, @@ -83,7 +93,7 @@ object Renderer { .flatten .map(_._1) - val leftColWidth = getLeftColWidth(flattenedAll) + val leftColWidth = getLeftColWidth(flattenedAll, nameMapper) mainMethods match { case Seq() => "" case Seq(main) => @@ -156,7 +166,7 @@ object Renderer { if (sorted) main.renderedArgSigs.sorted(ArgOrd) else main.renderedArgSigs - val args = sortedArgs.map(renderArg(_, argLeftCol, totalWidth)) + val args = sortedArgs.map(renderArg(_, argLeftCol, totalWidth, nameMapper)) val leftIndentStr = " " * leftIndent @@ -260,6 +270,7 @@ object Renderer { s"Did you mean `${token.drop(2)}` instead of `$token`?" } + @deprecated("Binary Compatibility Shim") def renderResult( main: MainData[_, _], result: Result.Failure, @@ -289,12 +300,12 @@ object Renderer { customName: Option[String], customDoc: Option[String], sorted: Boolean, - nameMapper: String => Option[String] = Util.kebabCaseNameMapper + nameMapper: String => Option[String] ): String = { def expectedMsg() = { if (printHelpOnError) { - val leftColWidth = getLeftColWidth(main.renderedArgSigs) + val leftColWidth = getLeftColWidth(main.renderedArgSigs, nameMapper) "Expected Signature: " + Renderer.formatMainMethodSignature( main, @@ -321,7 +332,7 @@ object Renderer { val missingStr = if (missing.isEmpty) "" else { - val chunks = missing.map(renderArgShort(_)) + val chunks = missing.map(renderArgShort(_, nameMapper)) val argumentsStr = pluralize("argument", chunks.length) s"Missing $argumentsStr: ${chunks.mkString(" ")}" + Renderer.newLine @@ -342,7 +353,7 @@ object Renderer { val lines = for ((sig, options) <- duplicate) yield { - s"Duplicate arguments for ${renderArgShort(sig)}: " + + s"Duplicate arguments for ${renderArgShort(sig, nameMapper)}: " + options.map(Util.literalize(_)).mkString(" ") + Renderer.newLine } @@ -352,7 +363,7 @@ object Renderer { val incompleteStr = incomplete match { case None => "" case Some(sig) => - s"Incomplete argument ${renderArgShort(sig)} is missing a corresponding value" + + s"Incomplete argument ${renderArgShort(sig, nameMapper)} is missing a corresponding value" + Renderer.newLine } @@ -366,12 +377,12 @@ object Renderer { val thingies = x.map { case Result.ParamError.Failed(p, vs, errMsg) => val literalV = vs.map(Util.literalize(_)).mkString(" ") - s"Invalid argument ${renderArgShort(p)} failed to parse $literalV due to $errMsg" + s"Invalid argument ${renderArgShort(p, nameMapper)} failed to parse $literalV due to $errMsg" case Result.ParamError.Exception(p, vs, ex) => val literalV = vs.map(Util.literalize(_)).mkString(" ") - s"Invalid argument ${renderArgShort(p)} failed to parse $literalV due to $ex" + s"Invalid argument ${renderArgShort(p, nameMapper)} failed to parse $literalV due to $ex" case Result.ParamError.DefaultFailed(p, ex) => - s"Invalid argument ${renderArgShort(p)}'s default value failed to evaluate with $ex" + s"Invalid argument ${renderArgShort(p, nameMapper)}'s default value failed to evaluate with $ex" } Renderer.normalizeNewlines( diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index df66e9b..2c03aae 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -39,11 +39,11 @@ object TokenGrouping { .toMap[String, ArgSig] lazy val keywordArgMap = makeKeywordArgMap( - x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.name.map("--" + _) ++ x.shortName.map("-" + _) + x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.longName(Util.nullNameMapper).map("--" + _) ++ x.shortName.map("-" + _) ) lazy val longKeywordArgMap = makeKeywordArgMap( - x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.name.map("--" + _) + x => x.mappedName(nameMapper).map("--"+ _ ) ++ x.longName(Util.nullNameMapper).map("--" + _) ) @tailrec def rec( diff --git a/mainargs/src/TokensReader.scala b/mainargs/src/TokensReader.scala index 36f9ec9..925dfa9 100644 --- a/mainargs/src/TokensReader.scala +++ b/mainargs/src/TokensReader.scala @@ -230,7 +230,7 @@ object ArgSig { val docOpt = scala.Option(arg.doc) new ArgSig( - if (arg.noDefaultName) None else Some(name0), + if (arg.noDefaultName || name0.length == 1) None else Some(name0), scala.Option(arg.name), shortOpt, docOpt, @@ -247,39 +247,39 @@ object ArgSig { } @deprecated("Binary Compatibility Shim") - def apply( - name: Option[String], - shortName: Option[Char], - doc: Option[String], - default: Option[Any => Any], - reader: TokensReader[_], - positional: Boolean, - hidden: Boolean - ) = { - new ArgSig(name, name, shortName, doc, default, reader, positional, hidden) + def apply(unMappedName: Option[String], + shortName: Option[Char], + doc: Option[String], + default: Option[Any => Any], + reader: TokensReader[_], + positional: Boolean, + hidden: Boolean) = { + + new ArgSig(unMappedName, unMappedName, shortName, doc, default, reader, positional, hidden) } def unapply(a: ArgSig) = Option( - (a.name, a.shortName, a.doc, a.default, a.reader, a.positional, a.hidden) + (a.unMappedName, a.shortName, a.doc, a.default, a.reader, a.positional, a.hidden) ) } /** * Models what is known by the router about a single argument: that it has - * a [[name]], a human-readable [[typeString]] describing what the type is + * a [[longName]], a human-readable [[typeString]] describing what the type is * (just for logging and reading, not a replacement for a `TypeTag`) and * possible a function that can compute its default value */ -class ArgSig private[mainargs] ( - val defaultName: Option[String], - val argName: Option[String], - val shortName: Option[Char], - val doc: Option[String], - val default: Option[Any => Any], - val reader: TokensReader[_], - val positional: Boolean, - val hidden: Boolean +class ArgSig private[mainargs] (val defaultLongName: Option[String], + val argName: Option[String], + val shortName: Option[Char], + val doc: Option[String], + val default: Option[Any => Any], + val reader: TokensReader[_], + val positional: Boolean, + val hidden: Boolean ) extends Product with Serializable with Equals{ + @deprecated("Binary Compatibility Shim") + def name = defaultLongName override def canEqual(that: Any): Boolean = true override def hashCode(): Int = ArgSig.unapply(this).hashCode() @@ -289,34 +289,32 @@ class ArgSig private[mainargs] ( } @deprecated("Binary Compatibility Shim") - def this( - name: Option[String], - shortName: Option[Char], - doc: Option[String], - default: Option[Any => Any], - reader: TokensReader[_], - positional: Boolean, - hidden: Boolean - ) = { - this(name, name, shortName, doc, default, reader, positional, hidden) + def this(unmappedName: Option[String], + shortName: Option[Char], + doc: Option[String], + default: Option[Any => Any], + reader: TokensReader[_], + positional: Boolean, + hidden: Boolean) = { + this(unmappedName, unmappedName, shortName, doc, default, reader, positional, hidden) } @deprecated("Binary Compatibility Shim") - def copy(name: Option[String] = this.name, + def copy(unMappedName: Option[String] = this.unMappedName, shortName: Option[Char] = this.shortName, doc: Option[String] = this.doc, default: Option[Any => Any] = this.default, reader: TokensReader[_] = this.reader, positional: Boolean = this.positional, hidden: Boolean = this.hidden) = { - ArgSig(name, shortName, doc, default, reader, positional, hidden) + ArgSig(unMappedName, shortName, doc, default, reader, positional, hidden) } @deprecated("Binary Compatibility Shim") def productArity = 9 @deprecated("Binary Compatibility Shim") def productElement(n: Int) = n match{ - case 0 => defaultName + case 0 => defaultLongName case 1 => argName case 2 => shortName case 3 => doc @@ -326,9 +324,10 @@ class ArgSig private[mainargs] ( case 7 => hidden } - def name: Option[String] = argName.orElse(defaultName.flatMap(d => if (d.length == 1) None else Some(d))) + def unMappedName: Option[String] = argName.orElse(defaultLongName) + def longName(nameMapper: String => Option[String]): Option[String] = argName.orElse(mappedName(nameMapper)).orElse(defaultLongName) def mappedName(nameMapper: String => Option[String]): Option[String] = - if (argName.isDefined) None else defaultName.flatMap(nameMapper) + if (argName.isDefined) None else defaultLongName.flatMap(nameMapper) } @@ -350,6 +349,8 @@ class MainData[T, B] private[mainargs] ( val doc: Option[String], val invokeRaw: (B, Seq[Any]) => T ) extends Product with Serializable with Equals{ + @deprecated("Binary Compatibility Shim") + def name = mainName.getOrElse(defaultName) @deprecated("Binary Compatibility Shim") def productArity = 5 @deprecated("Binary Compatibility Shim") @@ -361,7 +362,7 @@ class MainData[T, B] private[mainargs] ( case 4 => invokeRaw } @deprecated("Binary Compatibility Shim") - def copy(name: String = this.name, + def copy(name: String = this.unmappedName, argSigs0: Seq[ArgSig] = this.argSigs0, doc: Option[String] = this.doc, invokeRaw: (B, Seq[Any]) => T = this.invokeRaw) = MainData( @@ -386,9 +387,9 @@ class MainData[T, B] private[mainargs] ( @deprecated("Binary Compatibility Shim") override def canEqual(that: Any): Boolean = true - def name: String = mainName.getOrElse(defaultName) + def unmappedName: String = mainName.getOrElse(defaultName) - def name(nameMapper: String => Option[String]) = mappedName(nameMapper).getOrElse(defaultName) + def name(nameMapper: String => Option[String]) = mainName.orElse(mappedName(nameMapper)).getOrElse(defaultName) def mappedName(nameMapper: String => Option[String]): Option[String] = if (mainName.isDefined) None else nameMapper(defaultName) From c00bd610fa96e2cd49770bf61c2e0019299bbb62 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 22:00:39 +0800 Subject: [PATCH 7/8] update deprecation warnings --- build.sc | 6 ------ mainargs/src/Parser.scala | 12 ++++++------ mainargs/src/Renderer.scala | 6 +++--- mainargs/src/TokenGrouping.scala | 2 +- mainargs/src/TokensReader.scala | 32 ++++++++++++++++---------------- 5 files changed, 26 insertions(+), 32 deletions(-) diff --git a/build.sc b/build.sc index aa7a238..0f914ce 100644 --- a/build.sc +++ b/build.sc @@ -23,12 +23,6 @@ trait MainArgsPublishModule def publishVersion = VcsVersion.vcsState().format() - // Skip MIMA checks for Scala 3 since it's impossible to evolve the case classes - // in a way that's compatible with both Scala 2 and Scala 3 due to conflicting - // signatures for `def unapply - def mimaReportBinaryIssues(): Command[Unit] = - if (crossScalaVersion == scala3) T.command{/*do nothing*/} else super.mimaReportBinaryIssues() - override def mimaPreviousVersions = Seq("0.5.0") override def versionScheme: T[Option[VersionScheme]] = T(Some(VersionScheme.EarlySemVer)) diff --git a/mainargs/src/Parser.scala b/mainargs/src/Parser.scala index 9ec45c7..790d781 100644 --- a/mainargs/src/Parser.scala +++ b/mainargs/src/Parser.scala @@ -7,7 +7,7 @@ import java.io.PrintStream object ParserForMethods extends ParserForMethodsCompanionVersionSpecific class ParserForMethods[B](val mains: MethodMains[B]) { - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def helpText( totalWidth: Int, docsOnNewLine: Boolean, @@ -191,7 +191,7 @@ class ParserForMethods[B](val mains: MethodMains[B]) { - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def runEither( args: Seq[String], allowPositional: Boolean, @@ -215,7 +215,7 @@ class ParserForMethods[B](val mains: MethodMains[B]) { customDocs, sorted ) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def runRaw( args: Seq[String], allowPositional: Boolean, @@ -235,7 +235,7 @@ class ParserForMethods[B](val mains: MethodMains[B]) { } } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def runRaw0( args: Seq[String], allowPositional: Boolean, @@ -263,7 +263,7 @@ class ParserForMethods[B](val mains: MethodMains[B]) { object ParserForClass extends ParserForClassCompanionVersionSpecific class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) extends TokensReader.Class[T] { - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def helpText( totalWidth: Int, docsOnNewLine: Boolean, @@ -300,7 +300,7 @@ class ParserForClass[T](val main: MainData[T, Any], val companion: () => Any) customDoc: String ): String = helpText(totalWidth, docsOnNewLine, customName, customDoc, sorted = true) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def constructOrExit( args: Seq[String], allowPositional: Boolean, diff --git a/mainargs/src/Renderer.scala b/mainargs/src/Renderer.scala index dde8c18..bbde53b 100644 --- a/mainargs/src/Renderer.scala +++ b/mainargs/src/Renderer.scala @@ -54,7 +54,7 @@ object Renderer { case (Some(l), Some(r)) => l.compare(r) } } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def renderArg( arg: ArgSig, leftOffset: Int, @@ -210,7 +210,7 @@ object Renderer { sorted = true, ) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def formatMainMethodSignature( main: MainData[_, _], leftIndent: Int, @@ -270,7 +270,7 @@ object Renderer { s"Did you mean `${token.drop(2)}` instead of `$token`?" } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def renderResult( main: MainData[_, _], result: Result.Failure, diff --git a/mainargs/src/TokenGrouping.scala b/mainargs/src/TokenGrouping.scala index 2c03aae..745ae3a 100644 --- a/mainargs/src/TokenGrouping.scala +++ b/mainargs/src/TokenGrouping.scala @@ -5,7 +5,7 @@ import scala.annotation.tailrec case class TokenGrouping[B](remaining: List[String], grouped: Map[ArgSig, Seq[String]]) object TokenGrouping { - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def groupArgs[B]( flatArgs0: Seq[String], argSigs: Seq[(ArgSig, TokensReader.Terminal[_])], diff --git a/mainargs/src/TokensReader.scala b/mainargs/src/TokensReader.scala index 925dfa9..b03bd5a 100644 --- a/mainargs/src/TokensReader.scala +++ b/mainargs/src/TokensReader.scala @@ -246,7 +246,7 @@ object ArgSig { case cls: TokensReader.Class[_] => cls.main.argSigs0.flatMap(flatten(_)) } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def apply(unMappedName: Option[String], shortName: Option[Char], doc: Option[String], @@ -278,7 +278,7 @@ class ArgSig private[mainargs] (val defaultLongName: Option[String], val positional: Boolean, val hidden: Boolean ) extends Product with Serializable with Equals{ - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def name = defaultLongName override def canEqual(that: Any): Boolean = true @@ -288,7 +288,7 @@ class ArgSig private[mainargs] (val defaultLongName: Option[String], case _ => false } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def this(unmappedName: Option[String], shortName: Option[Char], doc: Option[String], @@ -299,7 +299,7 @@ class ArgSig private[mainargs] (val defaultLongName: Option[String], this(unmappedName, unmappedName, shortName, doc, default, reader, positional, hidden) } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def copy(unMappedName: Option[String] = this.unMappedName, shortName: Option[Char] = this.shortName, doc: Option[String] = this.doc, @@ -310,9 +310,9 @@ class ArgSig private[mainargs] (val defaultLongName: Option[String], ArgSig(unMappedName, shortName, doc, default, reader, positional, hidden) } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def productArity = 9 - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def productElement(n: Int) = n match{ case 0 => defaultLongName case 1 => argName @@ -349,11 +349,11 @@ class MainData[T, B] private[mainargs] ( val doc: Option[String], val invokeRaw: (B, Seq[Any]) => T ) extends Product with Serializable with Equals{ - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def name = mainName.getOrElse(defaultName) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def productArity = 5 - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def productElement(n: Int) = n match{ case 0 => mainName case 1 => defaultName @@ -361,30 +361,30 @@ class MainData[T, B] private[mainargs] ( case 3 => doc case 4 => invokeRaw } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def copy(name: String = this.unmappedName, argSigs0: Seq[ArgSig] = this.argSigs0, doc: Option[String] = this.doc, invokeRaw: (B, Seq[Any]) => T = this.invokeRaw) = MainData( name, argSigs0, doc, invokeRaw ) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def this(name: String, argSigs0: Seq[ArgSig], doc: Option[String], invokeRaw: (B, Seq[Any]) => T) = this( Some(name), name, argSigs0, doc, invokeRaw ) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") override def hashCode(): Int = MainData.unapply(this).hashCode() - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") override def equals(obj: Any): Boolean = obj match{ case x: MainData[_, _] => MainData.unapply(x) == MainData.unapply(this) case _ => false } - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") override def canEqual(that: Any): Boolean = true def unmappedName: String = mainName.getOrElse(defaultName) @@ -402,9 +402,9 @@ class MainData[T, B] private[mainargs] ( } object MainData { - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def unapply[T, B](x: MainData[T, B]) = Option((x.mainName, x.defaultName, x.argSigs0, x.doc, x.invokeRaw)) - @deprecated("Binary Compatibility Shim") + @deprecated("Binary Compatibility Shim", "mainargs 0.6.0") def apply[T, B](name: String, argSigs0: Seq[ArgSig], doc: Option[String], From 046046166c783be872806b1d962774a063d48cab Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 24 Jan 2024 22:20:36 +0800 Subject: [PATCH 8/8] use keep-going in CI to thoroughly list out all failures --- .github/workflows/actions.yml | 4 ++-- readme.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index cdc7da3..2f0807b 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -26,7 +26,7 @@ jobs: distribution: 'temurin' java-version: ${{ matrix.java }} - name: Run tests - run: ./mill -i __.publishArtifacts + __.test + run: ./mill -i -k __.publishArtifacts + __.test check-binary-compatibility: runs-on: ubuntu-latest steps: @@ -38,7 +38,7 @@ jobs: distribution: 'temurin' java-version: 8 - name: Check Binary Compatibility - run: ./mill -i __.mimaReportBinaryIssues + run: ./mill -i -k __.mimaReportBinaryIssues publish-sonatype: if: github.repository == 'com-lihaoyi/mainargs' && startsWith(github.ref, 'refs/tags/') diff --git a/readme.md b/readme.md index c51409e..fd81acc 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# mainargs 0.5.5 +# mainargs 0.6.0 MainArgs is a small, dependency-free library for command line argument parsing in Scala. @@ -45,7 +45,7 @@ in its scripts, as well as for command-line parsing for the # Usage ```scala -ivy"com.lihaoyi::mainargs:0.5.5" +ivy"com.lihaoyi::mainargs:0.6.0" ``` ## Parsing Main Method Parameters @@ -528,7 +528,7 @@ command-line friendly tool. # Changelog -## 0.5.5 +## 0.6.0 - Automatically map `camelCase` Scala method and argument names to `kebab-case` CLI commands and flag names, with configurability by passing in custom