Skip to content

Commit 3569d79

Browse files
authored
Do not print stack trace if Cli.parseArgs fails (#1629)
* Do not print stack trace if Cli.parseArgs fails We're currently printing a rather unhelpful stack trace if `Cli.parseArgs` fails. This currently happens if - a required argument is missing - the value of an argument cannot be parsed - `--help` or `--usage` is provided With this PR no stack traces are printed under these conditions. Error or help messages are just printed to stderr or stdout and the program exits with an appropriate status. * Test unrecognized argument * Test finiteDurationArgParser with previous value
1 parent d472cf5 commit 3569d79

File tree

4 files changed

+51
-43
lines changed

4 files changed

+51
-43
lines changed

modules/core/src/main/scala/org/scalasteward/core/Main.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package org.scalasteward.core
1818

1919
import cats.effect.{ExitCode, IO, IOApp}
20-
import org.scalasteward.core.application.Context
20+
import org.scalasteward.core.application.{Cli, Context}
2121

2222
object Main extends IOApp {
2323
override def run(args: List[String]): IO[ExitCode] =
24-
Context.create[IO](args).use(_.runF)
24+
Cli.parseArgs(args) match {
25+
case Cli.ParseResult.Success(args) => Context.create[IO](args).use(_.runF)
26+
case Cli.ParseResult.Help(help) => IO(Console.out.println(help)).as(ExitCode.Success)
27+
case Cli.ParseResult.Error(error) => IO(Console.err.println(error)).as(ExitCode.Error)
28+
}
2529
}

modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,13 @@
1717
package org.scalasteward.core.application
1818

1919
import caseapp._
20-
import caseapp.core.Error.{MalformedValue, Other}
20+
import caseapp.core.Error.MalformedValue
2121
import caseapp.core.argparser.{ArgParser, SimpleArgParser}
2222
import cats.syntax.all._
2323
import org.http4s.Uri
2424
import org.http4s.syntax.literals._
25-
import org.scalasteward.core.application.Cli._
26-
import org.scalasteward.core.util.ApplicativeThrowable
2725
import scala.concurrent.duration._
2826

29-
final class Cli[F[_]](implicit F: ApplicativeThrowable[F]) {
30-
def parseArgs(args: List[String]): F[Args] =
31-
F.fromEither {
32-
CaseApp
33-
.parseWithHelp[Args](args)
34-
.flatMap {
35-
case (_, true, _, _) => Left(Other(CaseApp.helpMessage[Args]))
36-
case (_, _, true, _) => Left(Other(CaseApp.usageMessage[Args]))
37-
case (parsed @ Right(_), _, _, _) => parsed
38-
case (e @ Left(_), _, _, _) => e
39-
}
40-
.leftMap(e => new Throwable(e.message))
41-
}
42-
}
43-
4427
object Cli {
4528
final case class Args(
4629
workspace: String,
@@ -70,6 +53,22 @@ object Cli {
7053

7154
final case class EnvVar(name: String, value: String)
7255

56+
sealed trait ParseResult extends Product with Serializable
57+
object ParseResult {
58+
final case class Success(args: Args) extends ParseResult
59+
final case class Help(help: String) extends ParseResult
60+
final case class Error(error: String) extends ParseResult
61+
}
62+
63+
def parseArgs(args: List[String]): ParseResult =
64+
CaseApp.parseWithHelp[Args](args) match {
65+
case Right((_, true, _, _)) => ParseResult.Help(CaseApp.helpMessage[Args])
66+
case Right((_, _, true, _)) => ParseResult.Help(CaseApp.usageMessage[Args])
67+
case Right((Right(args), _, _, _)) => ParseResult.Success(args)
68+
case Right((Left(error), _, _, _)) => ParseResult.Error(error.message)
69+
case Left(error) => ParseResult.Error(error.message)
70+
}
71+
7372
implicit val envVarArgParser: SimpleArgParser[EnvVar] =
7473
SimpleArgParser.from[EnvVar]("env-var") { s =>
7574
s.trim.split('=').toList match {

modules/core/src/main/scala/org/scalasteward/core/application/Context.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ import org.scalasteward.core.vcs.{VCSApiAlg, VCSExtraAlg, VCSRepoAlg, VCSSelecti
4444

4545
object Context {
4646
def create[F[_]: ConcurrentEffect: ContextShift: Parallel: Timer](
47-
args: List[String]
47+
args: Cli.Args
4848
): Resource[F, StewardAlg[F]] =
4949
for {
5050
blocker <- Blocker[F]
51-
cliArgs_ <- Resource.liftF(new Cli[F].parseArgs(args))
52-
implicit0(config: Config) <- Resource.liftF(Config.create[F](cliArgs_))
51+
implicit0(config: Config) <- Resource.liftF(Config.create[F](args))
5352
implicit0(client: Client[F]) <- AsyncHttpClient.resource[F]()
5453
implicit0(logger: Logger[F]) <- Resource.liftF(Slf4jLogger.create[F])
5554
implicit0(httpExistenceClient: HttpExistenceClient[F]) <- HttpExistenceClient.create[F]

modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@ package org.scalasteward.core.application
22

33
import org.http4s.syntax.literals._
44
import org.scalasteward.core.application.Cli.EnvVar
5+
import org.scalasteward.core.application.Cli.ParseResult._
56
import org.scalatest.EitherValues
67
import org.scalatest.funsuite.AnyFunSuite
78
import org.scalatest.matchers.should.Matchers
89
import scala.concurrent.duration._
910

1011
class CliTest extends AnyFunSuite with Matchers with EitherValues {
11-
type Result[A] = Either[Throwable, A]
12-
val cli: Cli[Result] = new Cli[Result]
13-
14-
test("parseArgs") {
15-
cli.parseArgs(
12+
test("parseArgs: example") {
13+
Cli.parseArgs(
1614
List(
1715
List("--workspace", "a"),
1816
List("--repos-file", "b"),
@@ -27,7 +25,7 @@ class CliTest extends AnyFunSuite with Matchers with EitherValues {
2725
List("--env-var", "i=j"),
2826
List("--process-timeout", "30min")
2927
).flatten
30-
) shouldBe Right(
28+
) shouldBe Success(
3129
Cli.Args(
3230
workspace = "a",
3331
reposFile = "b",
@@ -44,16 +42,16 @@ class CliTest extends AnyFunSuite with Matchers with EitherValues {
4442
)
4543
}
4644

47-
test("parseArgs minimal version") {
48-
cli.parseArgs(
45+
test("parseArgs: minimal example") {
46+
Cli.parseArgs(
4947
List(
5048
List("--workspace", "a"),
5149
List("--repos-file", "b"),
5250
List("--git-author-email", "d"),
5351
List("--vcs-login", "e"),
5452
List("--git-ask-pass", "f")
5553
).flatten
56-
) shouldBe Right(
54+
) shouldBe Success(
5755
Cli.Args(
5856
workspace = "a",
5957
reposFile = "b",
@@ -64,36 +62,44 @@ class CliTest extends AnyFunSuite with Matchers with EitherValues {
6462
)
6563
}
6664

67-
test("parseArgs fail if required option not provided") {
68-
cli.parseArgs(Nil).isLeft shouldBe true
65+
test("parseArgs: fail if required option not provided") {
66+
Cli.parseArgs(Nil).asInstanceOf[Error].error should startWith("Required option")
67+
}
68+
69+
test("parseArgs: unrecognized argument") {
70+
Cli.parseArgs(List("--foo")).asInstanceOf[Error].error should startWith("Unrecognized")
6971
}
7072

71-
test("parseArgs --help") {
72-
cli.parseArgs(List("--help")).left.value.getMessage should include("--git-author-email")
73+
test("parseArgs: --help") {
74+
Cli.parseArgs(List("--help")).asInstanceOf[Help].help should include("--git-author-email")
7375
}
7476

75-
test("parseArgs --usage") {
76-
cli.parseArgs(List("--usage")).left.value.getMessage should startWith("Usage: args")
77+
test("parseArgs: --usage") {
78+
Cli.parseArgs(List("--usage")).asInstanceOf[Help].help should startWith("Usage: args")
7779
}
7880

79-
test("env-var without equals sign") {
81+
test("envVarArgParser: env-var without equals sign") {
8082
Cli.envVarArgParser(None, "SBT_OPTS").isLeft shouldBe true
8183
}
8284

83-
test("env-var with multiple equals signs") {
85+
test("envVarArgParser: env-var with multiple equals signs") {
8486
val value = "-Xss8m -XX:MaxMetaspaceSize=256m"
8587
Cli.envVarArgParser(None, s"SBT_OPTS=$value") shouldBe Right(EnvVar("SBT_OPTS", value))
8688
}
8789

88-
test("valid timeout") {
90+
test("finiteDurationArgParser: well-formed duration") {
8991
Cli.finiteDurationArgParser(None, "30min") shouldBe Right(30.minutes)
9092
}
9193

92-
test("malformed timeout") {
94+
test("finiteDurationArgParser: malformed duration") {
9395
Cli.finiteDurationArgParser(None, "xyz").isLeft shouldBe true
9496
}
9597

96-
test("malformed timeout (Inf)") {
98+
test("finiteDurationArgParser: malformed duration (Inf)") {
9799
Cli.finiteDurationArgParser(None, "Inf").isLeft shouldBe true
98100
}
101+
102+
test("finiteDurationArgParser: previous value") {
103+
Cli.finiteDurationArgParser(Some(10.seconds), "20seconds").isLeft shouldBe true
104+
}
99105
}

0 commit comments

Comments
 (0)