caseapp.core.parser.ParserMethods.scala Maven / Gradle / Ivy
The newest version!
package caseapp.core.parser
import caseapp.core.{Arg, Error, Indexed}
import caseapp.core.RemainingArgs
import caseapp.core.util.Formatter
import caseapp.Name
import caseapp.core.complete.Completer
import caseapp.core.complete.CompletionItem
import scala.annotation.tailrec
trait ParserMethods[+T] { parser: Parser[T @Internal.uncheckedVarianceScala2] =>
import Parser.Step
/** Initial value used to accumulate parsed arguments.
*/
def init: D
def step(
args: List[String],
index: Int,
d: D
): Either[(Error, Arg, List[String]), Option[(D, Arg, List[String])]] =
step(args, index, d, defaultNameFormatter)
/** Process the next argument.
*
* If some arguments were successfully processed (third case in return below), the returned
* remaining argument sequence must be shorter than the passed `args`.
*
* This method doesn't fully process `args`. It tries just to parse *one* argument (typically one
* option `--foo` and its value `bar`, so two elements from `args` - it can also be only one
* element in case of a flag), if possible. If you want to fully process a sequence of arguments,
* see `parse` or `detailedParse`.
*
* @param args:
* arguments to process
* @param d:
* current intermediate result
* @param nameFormatter:
* formats name to the appropriate format
* @return
* if no argument were parsed, `Right(None)`; if an error occurred, an error message wrapped in
* [[caseapp.core.Error]] and [[scala.Left]]; else the next intermediate value and the
* remaining arguments wrapped in [[scala.Some]] and [[scala.Right]].
*/
def step(
args: List[String],
index: Int,
d: D,
nameFormatter: Formatter[Name]
): Either[(Error, Arg, List[String]), Option[(D, Arg, List[String])]]
/** Get the final result from the final intermediate value.
*
* Typically fails if some mandatory arguments were not specified, so are missing in `d`,
* preventing building a `T` out of it.
*
* @param d:
* final intermediate value
* @return
* in case of success, a `T` wrapped in [[scala.Right]]; else, an error message, wrapped in
* [[caseapp.core.Error]] and [[scala.Left]]
*/
final def get(d: D): Either[Error, T] = get(d, defaultNameFormatter)
/** Get the final result from the final intermediate value.
*
* Typically fails if some mandatory arguments were not specified, so are missing in `d`,
* preventing building a `T` out of it.
*
* @param d:
* final intermediate value
* @param nameFormatter:
* formats names to the appropriate format
* @return
* in case of success, a `T` wrapped in [[scala.Right]]; else, an error message, wrapped in
* [[caseapp.core.Error]] and [[scala.Left]]
*/
def get(d: D, nameFormatter: Formatter[Name]): Either[Error, T]
/** Arguments this parser accepts.
*
* Used to generate help / usage messages.
*/
def args: Seq[Arg]
def defaultStopAtFirstUnrecognized: Boolean =
false
def defaultIgnoreUnrecognized: Boolean =
false
def defaultNameFormatter: Formatter[Name] =
Formatter.DefaultNameFormatter
final def parse(args: Seq[String]): Either[Error, (T, Seq[String])] =
detailedParse(args)
.map {
case (t, rem) =>
(t, rem.all)
}
/** Keeps the remaining args before and after a possible -- separated */
final def detailedParse(args: Seq[String]): Either[Error, (T, RemainingArgs)] =
detailedParse(
args,
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)] = {
val (res, remArgs, _) = scan(args, stopAtFirstUnrecognized, ignoreUnrecognized)
res
.left.map(_._1)
.map((_, remArgs))
}
final def scan(
args: Seq[String],
stopAtFirstUnrecognized: Boolean,
ignoreUnrecognized: Boolean
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) = {
def runHelper(
current: D,
args: List[String],
extraArgsReverse: List[Indexed[String]],
reverseSteps: List[Step],
index: Int
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) =
helper(current, args, extraArgsReverse, reverseSteps, index)
@tailrec
def helper(
current: D,
args: List[String],
extraArgsReverse: List[Indexed[String]],
reverseSteps: List[Step],
index: Int
): (Either[(Error, Either[D, T]), T], RemainingArgs, List[Step]) = {
def done = {
val remArgs = RemainingArgs(extraArgsReverse.reverse, Nil)
val res = get(current)
.left.map((_, Left(current)))
(res, remArgs, reverseSteps.reverse)
}
def stopParsing(tailArgs: List[String]) = {
val remArgs =
if (stopAtFirstUnrecognized)
// extraArgsReverse should be empty anyway here
RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
else
RemainingArgs(extraArgsReverse.reverse, Indexed.seq(tailArgs, index + 1))
val res = get(current)
.left.map((_, Left(current)))
val reverseSteps0 = Step.DoubleDash(index) :: reverseSteps.reverse
(res, remArgs, reverseSteps0.reverse)
}
def unrecognized(headArg: String, tailArgs: List[String]) =
if (stopAtFirstUnrecognized) {
// extraArgsReverse should be empty anyway here
val remArgs = RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
val res = get(current)
.left.map((_, Left(current)))
val reverseSteps0 = Step.FirstUnrecognized(index, isOption = true) :: reverseSteps
(res, remArgs, reverseSteps0.reverse)
}
else {
val err = Error.UnrecognizedArgument(headArg)
val (remaining, remArgs, steps) = runHelper(
current,
tailArgs,
extraArgsReverse,
Step.Unrecognized(index, err) :: reverseSteps,
index + 1
)
val res = Left((
remaining.fold(t => err.append(t._1), _ => err),
remaining.fold(_._2, Right(_))
))
(res, remArgs, steps)
}
def stoppingAtUnrecognized = {
// extraArgsReverse should be empty anyway here
val remArgs = RemainingArgs(extraArgsReverse.reverse ::: Indexed.list(args, index), Nil)
val res = get(current)
.left.map((_, Left(current)))
val reverseSteps0 = Step.FirstUnrecognized(index, isOption = false) :: reverseSteps
(res, remArgs, reverseSteps0.reverse)
}
args match {
case Nil => done
case headArg :: tailArgs =>
step(args, index, current) match {
case Right(None) =>
if (headArg == "--")
stopParsing(tailArgs)
else if (headArg.startsWith("-") && headArg != "-")
if (ignoreUnrecognized)
helper(
current,
tailArgs,
Indexed(index, 1, headArg) :: extraArgsReverse,
Step.IgnoredUnrecognized(index) :: reverseSteps,
index + 1
)
else
unrecognized(headArg, tailArgs)
else if (stopAtFirstUnrecognized)
stoppingAtUnrecognized
else
helper(
current,
tailArgs,
Indexed(index, 1, headArg) :: extraArgsReverse,
Step.StandardArgument(index) :: reverseSteps,
index + 1
)
case Right(Some((newC, matchedArg, newArgs))) =>
assert(
newArgs != args,
s"From $args, an ArgParser is supposed to have consumed arguments, but returned the same argument list"
)
val consumed0 = Parser.consumed(args, newArgs)
assert(consumed0 > 0)
helper(
newC,
newArgs.toList,
extraArgsReverse,
Step.MatchedOption(index, consumed0, matchedArg) :: reverseSteps,
index + consumed0
)
case Left((msg, matchedArg, rem)) =>
val consumed0 = Parser.consumed(args, rem)
assert(consumed0 > 0)
val (remaining, remArgs, steps) = runHelper(
current,
rem,
extraArgsReverse,
Step.ErroredOption(index, consumed0, matchedArg, msg) :: reverseSteps,
index + consumed0
)
val res = Left((
remaining.fold(errs => msg.append(errs._1), _ => msg),
remaining.fold(_._2, Right(_))
))
(res, remArgs, steps)
}
}
}
helper(init, args.toList, Nil, Nil, 0)
}
def complete(
args: Seq[String],
index: Int,
completer: Completer[T],
stopAtFirstUnrecognized: Boolean,
ignoreUnrecognized: Boolean
): List[CompletionItem] = {
val args0 = if (index < args.length) args else args ++ Seq.fill(index + 1 - args.length)("")
val (res, remArgs, steps) = scan(args0, stopAtFirstUnrecognized, ignoreUnrecognized)
lazy val stateOpt = res match {
case Left((_, Left(state))) => get(state).toOption
case Left((_, Right(t))) => Some(t)
case Right(t) => Some(t)
}
assert(index >= 0)
assert(index < args0.length)
val prefix = args0(index)
val stepOpt = steps.find { step =>
step.index <= index && index < step.index + step.consumed
}
val value = args0(index)
stepOpt match {
case None =>
val isAfterDoubleDash = steps.lastOption.exists {
case Step.DoubleDash(ddIdx) => ddIdx < index
case _ => false
}
if (isAfterDoubleDash)
completer.postDoubleDash(stateOpt, remArgs)
.map { completer =>
if (value.startsWith("-"))
completer.optionName(value, stateOpt, remArgs)
else
completer.argument(value, stateOpt, remArgs)
}
.getOrElse(Nil)
else
Nil
case Some(step) =>
val shift = index - step.index
step match {
case Step.DoubleDash(_) =>
completer.optionName(value, stateOpt, remArgs)
case Step.ErroredOption(_, _, _, _) if shift == 0 =>
completer.optionName(value, stateOpt, remArgs)
case Step.ErroredOption(_, consumed, arg, _) if consumed == 2 && shift == 1 =>
completer.optionValue(arg, value, stateOpt, remArgs)
case Step.ErroredOption(_, _, _, _) =>
// should not happen
Nil
case Step.FirstUnrecognized(_, true) =>
completer.optionName(value, stateOpt, remArgs)
case Step.FirstUnrecognized(_, false) =>
completer.argument(value, stateOpt, remArgs)
case Step.IgnoredUnrecognized(_) =>
completer.optionName(value, stateOpt, remArgs)
case Step.Unrecognized(_, _) =>
completer.optionName(value, stateOpt, remArgs)
case Step.StandardArgument(idx) if value == "-" =>
completer.optionName(value, stateOpt, remArgs)
case Step.MatchedOption(_, consumed, arg) if shift == 0 =>
completer.optionName(value, stateOpt, remArgs)
case Step.MatchedOption(_, consumed, arg) if consumed == 2 && shift == 1 =>
completer.optionValue(arg, value, stateOpt, remArgs)
case Step.MatchedOption(_, _, _) =>
// should not happen
Nil
case Step.StandardArgument(_) =>
completer.argument(value, stateOpt, remArgs)
}
}
}
}