Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Ignore unrecognized arguments #266

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cats/shared/src/main/scala/caseapp/cats/IOCaseApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ abstract class IOCaseApp[T](implicit val parser0: Parser[T], val messages: Help[

def parser: Parser[T] = {
val p = parser0.nameFormatter(nameFormatter)
if (stopAtFirstUnrecognized)
if (ignoreUnrecognized)
p.ignoreUnrecognized
else if (stopAtFirstUnrecognized)
p.stopAtFirstUnrecognized
else
p
Expand Down Expand Up @@ -65,11 +67,20 @@ abstract class IOCaseApp[T](implicit val parser0: Parser[T], val messages: Help[
def stopAtFirstUnrecognized: Boolean =
false

/**
* Whether to ignore unrecognized arguments.
*
* That is, if there are unrecognized arguments, the parsing still succeeds.
* The unparsed arguments are put in the `args` argument of `run`.
*/
def ignoreUnrecognized: Boolean =
alexarchambault marked this conversation as resolved.
Show resolved Hide resolved
false

def nameFormatter: Formatter[Name] =
Formatter.DefaultNameFormatter

override def run(args: List[String]): IO[ExitCode] =
parser.withHelp.detailedParse(expandArgs(args), stopAtFirstUnrecognized) match {
parser.withHelp.detailedParse(expandArgs(args), stopAtFirstUnrecognized, ignoreUnrecognized) match {
case Left(err) => error(err)
case Right((WithHelp(_, true, _), _)) => helpAsked
case Right((WithHelp(true, _, _), _)) => usageAsked
Expand Down
15 changes: 13 additions & 2 deletions core/shared/src/main/scala/caseapp/core/app/CaseApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]

def parser: Parser[T] = {
val p = parser0.nameFormatter(nameFormatter)
if (stopAtFirstUnrecognized)
if (ignoreUnrecognized)
p.ignoreUnrecognized
else if (stopAtFirstUnrecognized)
p.stopAtFirstUnrecognized
else
p
Expand Down Expand Up @@ -70,11 +72,20 @@ abstract class CaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]
def stopAtFirstUnrecognized: Boolean =
false

/**
* Whether to ignore unrecognized arguments.
*
* That is, if there are unrecognized arguments, the parsing still succeeds.
* The unparsed arguments are put in the `args` argument of `run`.
*/
def ignoreUnrecognized: Boolean =
false

def nameFormatter: Formatter[Name] =
Formatter.DefaultNameFormatter

def main(args: Array[String]): Unit =
parser.withHelp.detailedParse(expandArgs(args.toList), stopAtFirstUnrecognized) match {
parser.withHelp.detailedParse(expandArgs(args.toList), stopAtFirstUnrecognized, ignoreUnrecognized) match {
case Left(err) => error(err)
case Right((WithHelp(_, true, _), _)) => helpAsked()
case Right((WithHelp(true, _, _), _)) => usageAsked()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import caseapp.Name
override def defaultStopAtFirstUnrecognized: Boolean =
underlying.defaultStopAtFirstUnrecognized

override def defaultIgnoreUnrecognized: Boolean =
underlying.defaultIgnoreUnrecognized

override def defaultNameFormatter: Formatter[Name] =
underlying.defaultNameFormatter

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package caseapp.core.parser

import caseapp.core.{Arg, Error}
import dataclass.data
import caseapp.core.util.Formatter
import caseapp.Name

@data class IgnoreUnrecognizedParser[T](underlying: Parser[T]) extends Parser[T] {
type D = underlying.D
def init: D = underlying.init
def step(
args: List[String],
d: D,
nameFormatter: Formatter[Name]
): Either[(Error, List[String]), Option[(D, List[String])]] =
underlying.step(args, d, nameFormatter)
def get(d: D, nameFormatter: Formatter[Name]): Either[Error, T] =
underlying.get(d, nameFormatter)
def args: Seq[Arg] =
underlying.args
override def defaultStopAtFirstUnrecognized: Boolean =
underlying.defaultStopAtFirstUnrecognized
override def defaultIgnoreUnrecognized: Boolean =
true
override def defaultNameFormatter: Formatter[Name] =
underlying.defaultNameFormatter
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import caseapp.Name
override def defaultStopAtFirstUnrecognized: Boolean =
underlying.defaultStopAtFirstUnrecognized

override def defaultIgnoreUnrecognized: Boolean =
underlying.defaultIgnoreUnrecognized

override def defaultNameFormatter: Formatter[Name] =
underlying.defaultNameFormatter
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import caseapp.Name
override def defaultStopAtFirstUnrecognized: Boolean =
underlying.defaultStopAtFirstUnrecognized

override def defaultIgnoreUnrecognized: Boolean =
underlying.defaultIgnoreUnrecognized

override def defaultNameFormatter: Formatter[Name] =
underlying.defaultNameFormatter
}
27 changes: 24 additions & 3 deletions core/shared/src/main/scala/caseapp/core/parser/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ abstract class Parser[T] {
def stopAtFirstUnrecognized: Parser[T] =
StopAtFirstUnrecognizedParser(this)

def defaultIgnoreUnrecognized: Boolean =
false
def ignoreUnrecognized: Parser[T] =
IgnoreUnrecognizedParser(this)

def defaultNameFormatter: Formatter[Name] =
Formatter.DefaultNameFormatter

Expand All @@ -119,12 +124,24 @@ abstract class Parser[T] {
final def detailedParse(args: Seq[String]): Either[Error, (T, RemainingArgs)] =
detailedParse(
args,
stopAtFirstUnrecognized = defaultStopAtFirstUnrecognized
stopAtFirstUnrecognized = defaultStopAtFirstUnrecognized,
ignoreUnrecognized = defaultIgnoreUnrecognized
)

final def detailedParse(
args: Seq[String],
stopAtFirstUnrecognized: Boolean
): Either[Error, (T, RemainingArgs)] =
detailedParse(
args,
stopAtFirstUnrecognized = stopAtFirstUnrecognized,
ignoreUnrecognized = defaultIgnoreUnrecognized
)

final def detailedParse(
args: Seq[String],
stopAtFirstUnrecognized: Boolean,
ignoreUnrecognized: Boolean
): Either[Error, (T, RemainingArgs)] = {

def helper(
Expand All @@ -149,7 +166,9 @@ abstract class Parser[T] {
(t, RemainingArgs(extraArgsReverse.reverse, rem))
}
case opt :: rem if opt.startsWith("-") =>
if (stopAtFirstUnrecognized)
if (ignoreUnrecognized)
helper(current, rem, opt :: extraArgsReverse)
else if (stopAtFirstUnrecognized)
get(current)
// extraArgsReverse should be empty anyway here
.map((_, RemainingArgs(extraArgsReverse.reverse ::: args, Nil)))
Expand Down Expand Up @@ -190,7 +209,9 @@ abstract class Parser[T] {
final def withHelp: Parser[WithHelp[T]] = {
implicit val parser: Parser.Aux[T, D] = this
val p = ParserWithNameFormatter(Parser[WithHelp[T]], defaultNameFormatter)
if (defaultStopAtFirstUnrecognized)
if (defaultIgnoreUnrecognized)
p.ignoreUnrecognized
else if (defaultStopAtFirstUnrecognized)
p.stopAtFirstUnrecognized
else
p
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ import caseapp.Name
override def defaultStopAtFirstUnrecognized: Boolean =
underlying.defaultStopAtFirstUnrecognized

override def defaultIgnoreUnrecognized: Boolean =
underlying.defaultIgnoreUnrecognized

override def defaultNameFormatter: Formatter[Name] = f
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import caseapp.Name
underlying.args
override def defaultStopAtFirstUnrecognized: Boolean =
true
override def defaultIgnoreUnrecognized: Boolean =
underlying.defaultIgnoreUnrecognized
override def defaultNameFormatter: Formatter[Name] =
underlying.defaultNameFormatter
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ object ManualCommandNotAdtOptions {
@ProgName("c4")
final case class Command4Opts(someString: String = "default")

@ProgName("c5")
final case class Command5Opts(l: Long = 0)
}
53 changes: 53 additions & 0 deletions tests/shared/src/test/scala/caseapp/CaseAppTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,20 @@ object CaseAppTests extends TestSuite {
}
}

"ignore unrecognized argument if asked so" - {
* - {
val res = ManualCommandNotAdt.commandParser.parse[Default0](Seq("c5", "-b"))
val expectedRes = Right((Default0(), Nil, Some(Right((Seq("c5"), Inr(Inr(Inr(Inr(Inl(ManualCommandNotAdtOptions.Command5Opts()))))), Seq("-b"))))))
assert(res == expectedRes)
}

* - {
val res = ManualCommandNotAdt.commandParser.parse[Default0](Seq("c5", "--foo", "bar", "-l", "1", "--baz"))
val expectedRes = Right((Default0(), Nil, Some(Right((Seq("c5"), Inr(Inr(Inr(Inr(Inl(ManualCommandNotAdtOptions.Command5Opts(1)))))), Seq("--foo", "bar", "--baz"))))))
assert(res == expectedRes)
}
}

"stop at first unrecognized argument if asked so" - {
* - {
val res = ManualCommandNotAdt.commandParser.parse[Default0](Seq("c3", "-b"))
Expand Down Expand Up @@ -394,6 +408,45 @@ object CaseAppTests extends TestSuite {
val parser = Parser[DefaultsThrow]
}

"ignore unrecognized argument if asked" - {
val parser = Parser[FewArgs]
* - {
val res = parser.detailedParse(Nil, stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(), RemainingArgs(Nil, Nil)))
assert(res == expected)
}
* - {
val res = parser.detailedParse(Seq("--foo", "bar", "--value", "a"), stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(value = "a"), RemainingArgs(Seq("--foo", "bar"), Nil)))
assert(res == expected)
}
* - {
val res = parser.detailedParse(Seq("--value", "a", "--other"), stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(value = "a"), RemainingArgs(Seq("--other"), Nil)))
assert(res == expected)
}
* - {
val res = parser.detailedParse(Seq("--value", "a"), stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(value = "a"), RemainingArgs(Nil, Nil)))
assert(res == expected)
}
* - {
val res = parser.detailedParse(Seq("--value", "a", "--", "--other"), stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(value = "a"), RemainingArgs(Nil, Seq("--other"))))
assert(res == expected)
}
* - {
val res = parser.detailedParse(Seq("foo", "--value", "a"), stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(value = "a"), RemainingArgs(Seq("foo"), Nil)))
assert(res == expected)
}
* - {
val res = parser.detailedParse(Seq("--value", "a", "foo", "--", "--other"), stopAtFirstUnrecognized = false, ignoreUnrecognized = true)
val expected = Right((FewArgs(value = "a"), RemainingArgs(Seq("foo"), Seq("--other"))))
assert(res == expected)
}
}

"stop at first unrecognized argument if asked" - {
val parser = Parser[FewArgs]
* - {
Expand Down
9 changes: 9 additions & 0 deletions tests/shared/src/test/scala/caseapp/demo/Demo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,27 @@ object ManualCommandNotAdtStuff {
}
}

case object Command5IgnoreUnrecognized extends CaseApp[ManualCommandNotAdtOptions.Command5Opts] {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had forgotten about that file ^^ That's where this should have been tested 👍

override def ignoreUnrecognized = true
def run(options: ManualCommandNotAdtOptions.Command5Opts, args: RemainingArgs): Unit = {

}
}

val parser = CommandParser.nil
.add(Command1)
.add(Command2)
.add(Command3StopAtUnreco)
.add(Command4NameFormatter)
.add(Command5IgnoreUnrecognized)
.reverse

val help = CommandsHelp.nil
.add(Command1)
.add(Command2)
.add(Command3StopAtUnreco)
.add(Command4NameFormatter)
.add(Command5IgnoreUnrecognized)
.reverse

}
Expand Down