Skip to content

Commit

Permalink
Fixed issue-226 (exception when parsing non-key-value-pair) (#229)
Browse files Browse the repository at this point in the history
* Fixed issue-226 (exception when parsing non-key-value-pair)

* Fixed formatting
  • Loading branch information
ithinkicancode authored Jul 16, 2023
1 parent 9d204b8 commit 3d2ac54
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 18 deletions.
93 changes: 75 additions & 18 deletions zio-cli/shared/src/main/scala/zio/cli/Options.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import java.time.{
ZoneOffset => JZoneOffset,
ZonedDateTime => JZonedDateTime
}
import scala.annotation.tailrec

/**
* A `Flag[A]` models a command-line flag that produces a value of type `A`.
Expand Down Expand Up @@ -453,33 +454,89 @@ object Options extends OptionsPlatformSpecific {
args: List[String],
conf: CliConfig
): IO[ValidationError, (List[String], Predef.Map[String, String])] = {
def processArguments(

def extractArgOptKeyValuePairs(
input: List[String],
first: String,
conf: CliConfig
): (List[String], Predef.Map[String, String]) = {
val r = input.span(s => !s.startsWith("-") || supports(s, conf))
(r._2, createMapFromStringList(r._1) + createMapEntry(first))
}
): (List[String], List[(String, String)]) = {

def supports(s: String, conf: CliConfig): Boolean = {
val argumentNames =
makeFullName(self.argumentOption.name) :: self.argumentOption.aliases.map(makeFullName).toList
val caseSensitive = conf.caseSensitive

if (conf.caseSensitive) argumentNames.contains(s) else argumentNames.exists(_.equalsIgnoreCase(s))
}
def withHyphen(argOpt: String) =
(if (argOpt.length == 1) "-" else "--") +
(if (caseSensitive) argOpt else argOpt.toLowerCase)

val argOptSwitchNameAndAliases =
(self.argumentOption.name +: self.argumentOption.aliases)
.map(withHyphen)
.toSet

def isSwitch(s: String) = {
val switch =
if (caseSensitive) s
else s.toLowerCase

argOptSwitchNameAndAliases.contains(switch)
}

@tailrec
def loop(
acc: (List[String], List[(String, String)])
): (List[String], List[(String, String)]) = {
val (input, pairs) = acc
input match {
case Nil => acc
// `input` can be in the form of "-d key1=value1 -d key2=value2"
case switch :: keyValueString :: tail if isSwitch(switch.trim) =>
keyValueString.trim.split("=") match {
case Array(key, value) =>
loop(tail -> ((key, value) :: pairs))
case _ =>
acc
}
// Or, it can be in the form of "-d key1=value1 key2=value2"
case keyValueString :: tail =>
keyValueString.trim.split("=") match {
case Array(key, value) =>
loop(tail -> ((key, value) :: pairs))
case _ =>
acc
}
// Otherwise we give up and keep what remains as the leftover.
case _ => acc
}
}

def makeFullName(s: String): String = (if (s.length == 1) "-" else "--") + s
loop(input -> Nil)
}

def createMapFromStringList(input: List[String]): Predef.Map[String, String] =
input.filterNot(_.startsWith("-")).map(createMapEntry).toMap
def processArguments(
input: List[String],
first: String,
conf: CliConfig
): IO[ValidationError, (List[String], Predef.Map[String, String])] = (
first.trim.split("=") match {
case Array(key, value) =>
ZIO.succeed(key -> value)
case _ =>
ZIO.fail(
ValidationError(
ValidationErrorType.InvalidArgument,
p(error(s"Expected a key/value pair but got '$first'."))
)
)
}
).map { first =>
val (remains, pairs) = extractArgOptKeyValuePairs(input, conf)

def createMapEntry(input: String): (String, String) = {
val arr = input.split("=").take(2)
arr.head -> arr(1)
(remains, (first :: pairs).toMap)
}

self.argumentOption.validate(args, conf).map(tuple => processArguments(tuple._1, tuple._2, conf))
self.argumentOption
.validate(args, conf)
.flatMap { case (input, first) =>
processArguments(input, first, conf)
}
}

override private[cli] def modifySingle(f: SingleModifier) =
Expand Down
38 changes: 38 additions & 0 deletions zio-cli/shared/src/test/scala/zio/cli/OptionsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,44 @@ object OptionsSpec extends ZIOSpecDefault {
val r = m.validate(List("--defs", "key1=v1", "key2=v2", "--verbose"), CliConfig.default)

assertZIO(r)(equalTo(List("--verbose") -> Map("key1" -> "v1", "key2" -> "v2")))
},
test(
"validate should keep non-key-value parameters that follow the key-value pairs (each preceded by alias -d)"
) {
val r = m.validate(
List("-d", "key1=val1", "-d", "key2=val2", "-d", "key3=val3", "arg1", "arg2", "--verbose"),
CliConfig.default
)

assertZIO(r)(
equalTo(List("arg1", "arg2", "--verbose") -> Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"))
)
},
test(
"validate should keep non-key-value parameters that follow the key-value pairs (only the first key/value pair is preceded by alias)"
) {
val r = m.validate(
List("-d", "key1=val1", "key2=val2", "key3=val3", "arg1", "arg2", "--verbose"),
CliConfig.default
)

assertZIO(r)(
equalTo(List("arg1", "arg2", "--verbose") -> Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3"))
)
},
test(
"validate should keep non-key-value parameters that follow the key-value pairs (with a 'mixed' style of proceeding -- name or alias)"
) {
val r = m.validate(
List("-d", "key1=val1", "key2=val2", "--defs", "key3=val3", "key4=", "arg1", "arg2", "--verbose"),
CliConfig.default
)

assertZIO(r)(
equalTo(
List("key4=", "arg1", "arg2", "--verbose") -> Map("key1" -> "val1", "key2" -> "val2", "key3" -> "val3")
)
)
}
)
)
Expand Down

0 comments on commit 3d2ac54

Please sign in to comment.