Skip to content

Commit d6615d1

Browse files
committed
Refactor argument preprocessing logic
1 parent 25cef2e commit d6615d1

File tree

2 files changed

+138
-119
lines changed

2 files changed

+138
-119
lines changed

library/src/scala/annotation/newMain.scala

Lines changed: 112 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -96,89 +96,17 @@ final class newMain extends MainAnnotation[FromString, Any]:
9696
private def shortNameWithMarker(name: String): String = shortArgMarker + name
9797

9898
def command(info: Info, args: Seq[String]): Option[Seq[String]] =
99-
checkNames(info)
100-
10199
val canonicalNames = CanonicalNames(info)
102-
103-
var hasError: Boolean = false
104-
def error(msg: String): Unit = {
105-
hasError = true
106-
println(s"Error: $msg")
107-
}
108-
100+
canonicalNames.checkNames()
109101
if Help.hasHelpArg(canonicalNames, args) then
110-
val help = new Help(info)
111-
help.printUsage()
112-
println()
113-
help.printExplain()
102+
Help.printUsage(info)
103+
Help.printExplain(info)
114104
None
115105
else
116-
val (positionalArgs, byNameArgs) = {
117-
def recurse(remainingArgs: Seq[String], pa: mutable.Queue[String], bna: Seq[(String, String)]): (mutable.Queue[String], Seq[(String, String)]) =
118-
remainingArgs match {
119-
case Seq() =>
120-
(pa, bna)
121-
case (argName @ longArgRegex()) +: argValue +: rest =>
122-
canonicalNames.getName(argName) match
123-
case Some(canonicalName) => recurse(rest, pa, bna :+ (canonicalName -> argValue))
124-
case None =>
125-
error(s"unknown argument name: $argName")
126-
recurse(rest, pa, bna)
127-
case (argName @ shortArgRegex()) +: argValue +: rest =>
128-
canonicalNames.getShortName(argName) match
129-
case Some(canonicalName) => recurse(rest, pa, bna :+ (canonicalName -> argValue))
130-
case None =>
131-
error(s"unknown argument name: $argName")
132-
recurse(rest, pa, bna)
133-
case arg +: rest =>
134-
recurse(rest, pa :+ arg, bna)
135-
}
136-
137-
val (pa, bna) = recurse(args.toSeq, mutable.Queue.empty, Vector())
138-
val nameToArgValues: Map[String, Seq[String]] = if bna.isEmpty then Map.empty else bna.groupMapReduce(_._1)(p => List(p._2))(_ ++ _)
139-
(pa, nameToArgValues)
140-
}
141-
142-
val argStrings: Seq[Seq[String]] =
143-
for paramInfo <- info.parameters yield {
144-
if (paramInfo.isVarargs) {
145-
val byNameGetters = byNameArgs.getOrElse(paramInfo.name, Seq())
146-
val positionalGetters = positionalArgs.removeAll()
147-
// First take arguments passed by name, then those passed by position
148-
byNameGetters ++ positionalGetters
149-
} else {
150-
byNameArgs.get(paramInfo.name) match
151-
case Some(Nil) =>
152-
throw AssertionError(s"${paramInfo.name} present in byNameArgs, but it has no argument value")
153-
case Some(argValues) =>
154-
if argValues.length > 1 then
155-
// Do not accept multiple values
156-
// Remove this test to take last given argument
157-
error(s"more than one value for ${paramInfo.name}: ${argValues.mkString(", ")}")
158-
Nil
159-
else
160-
List(argValues.last)
161-
case None =>
162-
if positionalArgs.length > 0 then
163-
List(positionalArgs.dequeue())
164-
else if paramInfo.hasDefault then
165-
List("")
166-
else
167-
error(s"missing argument for ${paramInfo.name}")
168-
Nil
169-
}
170-
}
171-
172-
// Handle unused and invalid args
173-
for (remainingArg <- positionalArgs) error(s"unused argument: $remainingArg")
174-
175-
if hasError then
176-
val help = new Help(info)
177-
help.printUsage()
106+
preProcessArgs(info, canonicalNames, args).orElse {
107+
Help.printUsage(info)
178108
None
179-
else
180-
Some(argStrings.flatten)
181-
end if
109+
}
182110
end command
183111

184112
def argGetter[T](param: Parameter, arg: String, defaultArgument: Option[() => T])(using p: FromString[T]): () => T = {
@@ -197,6 +125,71 @@ final class newMain extends MainAnnotation[FromString, Any]:
197125
def run(execProgram: () => Any): Unit =
198126
if !hasParseErrors then execProgram()
199127

128+
private def preProcessArgs(info: Info, canonicalNames: CanonicalNames, args: Seq[String]): Option[Seq[String]] =
129+
var hasError: Boolean = false
130+
def error(msg: String): Unit = {
131+
hasError = true
132+
println(s"Error: $msg")
133+
}
134+
135+
val (positionalArgs, byNameArgsMap) =
136+
val positionalArgs = List.newBuilder[String]
137+
val byNameArgs = List.newBuilder[(String, String)]
138+
var i = 0
139+
while i < args.length do
140+
args(i) match
141+
case name @ (longArgRegex() | shortArgRegex()) =>
142+
if i == args.length - 1 then // last argument -x ot --xyz
143+
error(s"missing argument for ${name}")
144+
else args(i + 1) match
145+
case longArgRegex() | shortArgRegex() =>
146+
error(s"missing argument for ${name}")
147+
case value =>
148+
canonicalNames.getName(name) match
149+
case Some(canonicalName) =>
150+
byNameArgs += ((canonicalName, value))
151+
case None =>
152+
error(s"unknown argument name: $name")
153+
i += 1 // consume `value`
154+
case value =>
155+
positionalArgs += value
156+
i += 1
157+
end while
158+
(positionalArgs.result(), byNameArgs.result().groupMap(_._1)(_._2))
159+
160+
// List of arguments in the order they should be passed to the main function
161+
val orderedArgs: List[String] =
162+
def rec(params: List[Parameter], acc: List[String], remainingArgs: List[String]): List[String] =
163+
params match
164+
case Nil =>
165+
for (remainingArg <- remainingArgs) error(s"unused argument: $remainingArg")
166+
acc.reverse
167+
case param :: tailParams =>
168+
if param.isVarargs then // also last arguments
169+
byNameArgsMap.get(param.name) match
170+
case Some(byNameVarargs) => acc ::: byNameVarargs.toList ::: remainingArgs
171+
case None => acc ::: remainingArgs
172+
else byNameArgsMap.get(param.name) match
173+
case Some(argValues) =>
174+
assert(argValues.nonEmpty, s"${param.name} present in byNameArgsMap, but it has no argument value")
175+
if argValues.length > 1 then
176+
error(s"more than one value for ${param.name}: ${argValues.mkString(", ")}")
177+
rec(tailParams, argValues.last :: acc, remainingArgs)
178+
179+
case None =>
180+
remainingArgs match
181+
case arg :: rest =>
182+
rec(tailParams, arg :: acc, rest)
183+
case Nil =>
184+
if !param.hasDefault then
185+
error(s"missing argument for ${param.name}")
186+
rec(tailParams, "" :: acc, Nil)
187+
rec(info.parameters.toList, Nil, positionalArgs)
188+
189+
if hasError then None
190+
else Some(orderedArgs)
191+
end preProcessArgs
192+
200193
private var hasParseErrors: Boolean = false
201194

202195
/** Issue an error, and return an uncallable getter */
@@ -210,31 +203,22 @@ final class newMain extends MainAnnotation[FromString, Any]:
210203
case Some(t) => () => t
211204
case None => parseError(s"could not parse argument for `${param.name}` of type ${param.typeName.split('.').last}: $arg")
212205

213-
private def checkNames(info: Info): Unit =
214-
def checkDuplicateNames() =
215-
val nameAndCanonicalName = info.parameters.flatMap { paramInfo =>
216-
(getNameWithMarker(paramInfo.name) +: paramInfo.longAliases ++: paramInfo.shortAliases).map(_ -> paramInfo.name)
217-
}
218-
val nameToCanonicalNames = nameAndCanonicalName.groupMap(_._1)(_._2)
219-
for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 do
220-
throw IllegalArgumentException(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}")
221-
def checkValidNames() =
222-
def isValidArgName(name: String): Boolean =
223-
longArgNameRegex.matches(name) || shortArgNameRegex.matches(name)
224-
for param <- info.parameters do
225-
if !isValidArgName(param.name) then
226-
throw IllegalArgumentException(s"The following argument name is invalid: ${param.name}")
227-
for alias <- param.aliasNames if !isValidArgName(alias) do
228-
throw IllegalArgumentException(s"The following alias is invalid: $alias")
229-
230-
checkValidNames()
231-
checkDuplicateNames()
232-
233-
private class Help(info: Info):
206+
207+
private object Help:
208+
209+
/** The name of the special argument to display the method's help.
210+
* If one of the method's parameters is called the same, will be ignored.
211+
*/
212+
private inline val helpArg = "help"
213+
214+
/** The short name of the special argument to display the method's help.
215+
* If one of the method's parameters uses the same short name, will be ignored.
216+
*/
217+
private inline val shortHelpArg = 'h'
234218

235219
private inline val maxUsageLineLength = 120
236220

237-
def printUsage(): Unit =
221+
def printUsage(info: Info): Unit =
238222
def argsUsage: Seq[String] =
239223
for (param <- info.parameters)
240224
yield {
@@ -266,7 +250,7 @@ final class newMain extends MainAnnotation[FromString, Any]:
266250
println(printUsageBeginning + printUsages.mkString("\n" + " " * argsOffset))
267251
end printUsage
268252

269-
def printExplain(): Unit =
253+
def printExplain(info: Info): Unit =
270254
def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n")
271255

272256
def wrapLongLine(line: String, maxLength: Int): List[String] = {
@@ -282,6 +266,8 @@ final class newMain extends MainAnnotation[FromString, Any]:
282266
recurse(line, Vector()).toList
283267
}
284268

269+
println()
270+
285271
if (info.documentation.nonEmpty)
286272
println(wrapLongLine(info.documentation, maxUsageLineLength).mkString("\n"))
287273
if (info.parameters.nonEmpty) {
@@ -312,25 +298,16 @@ final class newMain extends MainAnnotation[FromString, Any]:
312298
}
313299
end printExplain
314300

315-
private object Help:
316-
/** The name of the special argument to display the method's help.
317-
* If one of the method's parameters is called the same, will be ignored.
318-
*/
319-
private inline val helpArg = "help"
320-
321-
/** The short name of the special argument to display the method's help.
322-
* If one of the method's parameters uses the same short name, will be ignored.
323-
*/
324-
private inline val shortHelpArg = 'h'
325-
326301
def hasHelpArg(canonicalNames: CanonicalNames, args: Seq[String]): Boolean =
327302
val helpIsOverridden = canonicalNames.getName(argMarker + helpArg).isDefined
328303
val shortHelpIsOverridden = canonicalNames.getShortName(shortArgMarker + shortHelpArg).isDefined
329304
(!helpIsOverridden && args.contains(longNameWithMarker(helpArg))) ||
330305
(!shortHelpIsOverridden && args.contains(shortNameWithMarker(shortHelpArg.toString)))
306+
331307
end Help
332308

333309
private class CanonicalNames(info: Info):
310+
334311
private val namesToCanonicalName: Map[String, String] = info.parameters.flatMap(
335312
param =>
336313
val names = param.longAliases.map(_.drop(2))
@@ -347,10 +324,32 @@ final class newMain extends MainAnnotation[FromString, Any]:
347324
else names.map(_ -> canonicalName)
348325
).toMap
349326

350-
def getName(name: String): Option[String] = namesToCanonicalName.get(name.drop(2))
327+
def getName(name: String): Option[String] = namesToCanonicalName.get(name.drop(2)).orElse(getShortName(name))
351328

352329
def getShortName(name: String): Option[String] = shortNamesToCanonicalName.get(name.drop(1))
353330

331+
override def toString(): String =
332+
s"CanonicalNames($namesToCanonicalName, $shortNamesToCanonicalName)"
333+
334+
def checkNames(): Unit =
335+
def checkDuplicateNames() =
336+
val nameAndCanonicalName = info.parameters.flatMap { paramInfo =>
337+
(getNameWithMarker(paramInfo.name) +: paramInfo.longAliases ++: paramInfo.shortAliases).map(_ -> paramInfo.name)
338+
}
339+
val nameToCanonicalNames = nameAndCanonicalName.groupMap(_._1)(_._2)
340+
for (name, canonicalNames) <- nameToCanonicalNames if canonicalNames.length > 1 do
341+
throw IllegalArgumentException(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}")
342+
def checkValidNames() =
343+
def isValidArgName(name: String): Boolean =
344+
longArgNameRegex.matches(name) || shortArgNameRegex.matches(name)
345+
for param <- info.parameters do
346+
if !isValidArgName(param.name) then
347+
throw IllegalArgumentException(s"The following argument name is invalid: ${param.name}")
348+
for alias <- param.aliasNames if !isValidArgName(alias) do
349+
throw IllegalArgumentException(s"The following alias is invalid: $alias")
350+
351+
checkValidNames()
352+
checkDuplicateNames()
354353
end CanonicalNames
355354

356355
end newMain

tests/run/main-annotation-help-override.check

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,33 @@ Usage: helpOverride1 [--notHelp] <Int>
44
A method that should let --help and -h display help.
55
Arguments:
66
--notHelp - Int
7-
Error: could not parse argument for `help` of type Int: --help
7+
Error: missing argument for --help
8+
Error: missing argument for help
9+
Usage: helpOverride2 [--help] <Int>
810
Usage: helpOverride3 [-h] <Int>
911

1012
A method that should let --help display help, but not -h.
1113
Arguments:
1214
-h - Int
15+
Error: missing argument for --help
16+
Error: missing argument for help
1317
Error: missing argument for h
1418
Usage: helpOverride4 [--help] <Int> [-h] <Int>
15-
Error: could not parse argument for `notHelp` of type Int: --help
19+
Error: missing argument for --help
20+
Error: missing argument for notHelp
21+
Usage: helpOverride5 [--notHelp | --help] <Int>
1622
Usage: helpOverride6 [--notHelp | -h] <Int>
1723

1824
A method that should let --help display help, but not -h.
1925
Arguments:
2026
--notHelp (-h) - Int
27+
Error: missing argument for --help
28+
Error: missing argument for notHelp
2129
Error: missing argument for notH
2230
Usage: helpOverride7 [--notHelp | --help] <Int> [--notH | -h] <Int>
23-
Error: could not parse argument for `notHelp` of type Int: --help
31+
Error: missing argument for --help
32+
Error: missing argument for notHelp
33+
Usage: helpOverride8 [--notHelp | --help | -h] <Int>
2434
##### -h
2535
Usage: helpOverride1 [--notHelp] <Int>
2636

@@ -32,15 +42,25 @@ Usage: helpOverride2 [--help] <Int>
3242
A method that should let -h display help, but not --help.
3343
Arguments:
3444
--help - Int
35-
Error: could not parse argument for `h` of type Int: -h
45+
Error: missing argument for -h
46+
Error: missing argument for h
47+
Usage: helpOverride3 [-h] <Int>
48+
Error: missing argument for -h
49+
Error: missing argument for help
3650
Error: missing argument for h
3751
Usage: helpOverride4 [--help] <Int> [-h] <Int>
3852
Usage: helpOverride5 [--notHelp | --help] <Int>
3953

4054
A method that should let -h display help, but not --help.
4155
Arguments:
4256
--notHelp (--help) - Int
43-
Error: could not parse argument for `notHelp` of type Int: -h
57+
Error: missing argument for -h
58+
Error: missing argument for notHelp
59+
Usage: helpOverride6 [--notHelp | -h] <Int>
60+
Error: missing argument for -h
61+
Error: missing argument for notHelp
4462
Error: missing argument for notH
4563
Usage: helpOverride7 [--notHelp | --help] <Int> [--notH | -h] <Int>
46-
Error: could not parse argument for `notHelp` of type Int: -h
64+
Error: missing argument for -h
65+
Error: missing argument for notHelp
66+
Usage: helpOverride8 [--notHelp | --help | -h] <Int>

0 commit comments

Comments
 (0)