-
Notifications
You must be signed in to change notification settings - Fork 1k
/
newMain.scala
389 lines (335 loc) · 16.4 KB
/
newMain.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
/* __ *\
** ________ ___ / / ___ Scala API **
** / __/ __// _ | / / / _ | (c) 2002-2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ **
** /____/\___/_/ |_/____/_/ | | **
** |/ **
\* */
package scala.annotation
import scala.collection.mutable
import scala.util.CommandLineParser.FromString
import scala.annotation.meta.param
/**
* The annotation that designates a main function.
* Main functions are entry points for Scala programs. They can be called through a command line interface by using
* the `scala` command, followed by their name and, optionally, their parameters.
*
* The parameters of a main function may have any type `T`, as long as there exists a
* `given util.CommandLineParser.FromString[T]` in the scope. It will be used for parsing the string given as input
* into the correct argument type.
* These types already have parsers defined:
* - String,
* - Boolean,
* - Byte, Short, Int, Long, Float, Double.
*
* The parameters of a main function may be passed either by position, or by name. Passing an argument positionally
* means that you give the arguments in the same order as the function's signature. Passing an argument by name means
* that you give the argument right after giving its name. Considering the function
* `@newMain def foo(i: Int, str: String)`, we may have arguments passed:
* - by position: `scala foo 1 abc`,
* - by name: `scala foo -i 1 --str abc` or `scala foo --str abc -i 1`.
*
* A mixture of both is also possible: `scala foo --str abc 1` is equivalent to all previous examples.
*
* Note that main function overloading is not currently supported, i.e. you cannot define two main methods that have
* the same name in the same project.
*
* Special arguments are used to display help regarding a main function: `--help` and `-h`. If used as argument, the program
* will display some useful information about the main function. This help directly uses the ScalaDoc comment
* associated with the function, more precisely its description and the description of the parameters documented with
* `@param`. Note that if a parameter is named `help` or `h`, or if one of the parameters has as alias one of those names,
* the help displaying will be disabled for that argument.
* For example, for `@newMain def foo(help: Boolean)`, `scala foo -h` will display the help, but `scala foo --help` will fail,
* as it will expect a Boolean value after `--help`.
*
* Parameters may be given annotations to add functionalities to the main function:
* - `main.alias` adds other names to a parameter. For example, if a parameter `node` has as aliases
* `otherNode` and `n`, it may be addressed using `--node`, `--otherNode` or `-n`.
*
* Here is an example of a main function with annotated parameters:
* `@newMain def foo(@newMain.alias("x") number: Int, @newMain.alias("explanation") s: String)`. The following commands are
* equivalent:
* - `scala foo --number 1 -s abc`
* - `scala foo -x 1 -s abc`
* - `scala foo --number 1 --explanation abc`
* - `scala foo -x 1 --explanation abc`
*
* Boolean parameters are considered flags that do not require the "true" or "false" value to be passed.
* For example, `@newMain def foo(i: Boolean)` can be called as `foo` (where `i=false`) or `foo -i` (where `i=true`).
*
* The special `--` marker can be used to indicate that all following arguments are passed verbatim as positional parameters.
* For example, `@newMain def foo(args: String*)` can be called as `scala foo a b -- -c -d` which implies that `args=Seq("a", "b", "-c", "-d")`.
*/
@experimental
final class newMain extends MainAnnotation[FromString, Any]:
import newMain._
import MainAnnotation._
private val longArgRegex = "--[a-zA-Z][a-zA-Z0-9]+".r
private val shortArgRegex = "-[a-zA-Z]".r
// TODO: what should be considered as an invalid argument?
// Consider argument `-3.14`, `--i`, `-name`
private val illFormedName = "--[a-zA-Z]|-[a-zA-Z][a-zA-Z0-9]+".r
/** After this marker, all arguments are positional */
private inline val positionArgsMarker = "--"
extension (param: Parameter)
private def aliasNames: Seq[String] =
param.annotations.collect{ case alias: alias => getNameWithMarker(alias.name) }
private def isFlag: Boolean =
param.typeName == "scala.Boolean"
private def getNameWithMarker(name: String): String =
if name.length > 1 then s"--$name"
else if name.length == 1 then s"-$name"
else assert(false, "invalid name")
def command(info: Info, args: Seq[String]): Option[Seq[String]] =
val names = Names(info)
if Help.shouldPrintDefaultHelp(names, args) then
Help.printUsage(info)
Help.printExplain(info)
None
else
preProcessArgs(info, names, args).orElse {
Help.printUsage(info)
None
}
end command
def argGetter[T](param: Parameter, arg: String, defaultArgument: Option[() => T])(using p: FromString[T]): () => T = {
if arg.nonEmpty then parse[T](param, arg)
else
assert(param.hasDefault)
defaultArgument.get
}
def varargGetter[T](param: Parameter, args: Seq[String])(using p: FromString[T]): () => Seq[T] = {
val getters = args.map(arg => parse[T](param, arg))
() => getters.map(_())
}
def run(execProgram: () => Any): Unit =
if !hasParseErrors then execProgram()
private def preProcessArgs(info: Info, names: Names, args: Seq[String]): Option[Seq[String]] =
var hasError: Boolean = false
def error(msg: String): Unit = {
hasError = true
println(s"Error: $msg")
}
val (positionalArgs, byNameArgsMap) =
val positionalArgs = List.newBuilder[String]
val byNameArgs = List.newBuilder[(String, String)]
val flagsAdded = mutable.Set.empty[String]
// TODO: once we settle on a spec, we should implement this in a more elegant way
var i = 0
while i < args.length do
args(i) match
case name @ (longArgRegex() | shortArgRegex()) =>
if names.isFlagName(name) then
val canonicalName = names.canonicalName(name).get
flagsAdded += canonicalName
byNameArgs += ((canonicalName, "true"))
else if i == args.length - 1 then // last argument -x ot --xyz
error(s"missing argument for ${name}")
else args(i + 1) match
case longArgRegex() | shortArgRegex() | `positionArgsMarker` =>
error(s"missing argument for ${name}")
case value =>
names.canonicalName(name) match
case Some(canonicalName) =>
byNameArgs += ((canonicalName, value))
case None =>
error(s"unknown argument name: $name")
i += 1 // consume `value`
case name @ illFormedName() =>
error(s"ill-formed argument name: $name")
case `positionArgsMarker` =>
i += 1 // skip `--`
// all args after `--` are positional args
while i < args.length do
positionalArgs += args(i)
i += 1
case value =>
positionalArgs += value
i += 1
end while
// Add "false" for all flags not present in the arguments
for
param <- info.parameters
if param.isFlag
name = getNameWithMarker(param.name)
if !flagsAdded.contains(name)
do
byNameArgs += ((name, "false"))
(positionalArgs.result(), byNameArgs.result().groupMap(_._1)(_._2))
// List of arguments in the order they should be passed to the main function
val orderedArgs: List[String] =
def rec(params: List[Parameter], acc: List[String], remainingArgs: List[String]): List[String] =
params match
case Nil =>
for (remainingArg <- remainingArgs) error(s"unused argument: $remainingArg")
acc.reverse
case param :: tailParams =>
if param.isVarargs then // also last arguments
byNameArgsMap.get(param.name) match
case Some(byNameVarargs) => acc.reverse ::: byNameVarargs.toList ::: remainingArgs
case None => acc.reverse ::: remainingArgs
else byNameArgsMap.get(getNameWithMarker(param.name)) match
case Some(argValues) =>
assert(argValues.nonEmpty, s"${param.name} present in byNameArgsMap, but it has no argument value")
if argValues.length > 1 then
error(s"more than one value for ${param.name}: ${argValues.mkString(", ")}")
rec(tailParams, argValues.last :: acc, remainingArgs)
case None =>
remainingArgs match
case arg :: rest =>
rec(tailParams, arg :: acc, rest)
case Nil =>
if !param.hasDefault then
error(s"missing argument for ${param.name}")
rec(tailParams, "" :: acc, Nil)
rec(info.parameters.toList, Nil, positionalArgs)
if hasError then None
else Some(orderedArgs)
end preProcessArgs
private var hasParseErrors: Boolean = false
private def parse[T](param: Parameter, arg: String)(using p: FromString[T]): () => T =
p.fromStringOption(arg) match
case Some(t) =>
() => t
case None =>
/** Issue an error, and return an uncallable getter */
println(s"Error: could not parse argument for `${param.name}` of type ${param.typeName.split('.').last}: $arg")
hasParseErrors = true
() => throw new AssertionError("trying to get invalid argument")
@experimental // MiMa does not check scope inherited @experimental
private object Help:
/** The name of the special argument to display the method's help.
* If one of the method's parameters is called the same, will be ignored.
*/
private inline val helpArg = "--help"
/** The short name of the special argument to display the method's help.
* If one of the method's parameters uses the same short name, will be ignored.
*/
private inline val shortHelpArg = "-h"
private inline val maxUsageLineLength = 120
def printUsage(info: Info): Unit =
def argsUsage: Seq[String] =
for (param <- info.parameters)
yield {
val canonicalName = getNameWithMarker(param.name)
val namesPrint = (canonicalName +: param.aliasNames).mkString("[", " | ", "]")
val shortTypeName = param.typeName.split('.').last
if param.isVarargs then s"[<$shortTypeName> [<$shortTypeName> [...]]]"
else if param.hasDefault then s"[$namesPrint <$shortTypeName>]"
else if param.isFlag then s"$namesPrint"
else s"$namesPrint <$shortTypeName>"
}
def wrapArgumentUsages(argsUsage: Seq[String], maxLength: Int): Seq[String] = {
def recurse(args: Seq[String], currentLine: String, acc: Vector[String]): Seq[String] =
(args, currentLine) match {
case (Nil, "") => acc
case (Nil, l) => (acc :+ l)
case (arg +: t, "") => recurse(t, arg, acc)
case (arg +: t, l) if l.length + 1 + arg.length <= maxLength => recurse(t, s"$l $arg", acc)
case (arg +: t, l) => recurse(t, arg, acc :+ l)
}
recurse(argsUsage, "", Vector()).toList
}
val printUsageBeginning = s"Usage: ${info.name} "
val argsOffset = printUsageBeginning.length
val printUsages = wrapArgumentUsages(argsUsage, maxUsageLineLength - argsOffset)
println(printUsageBeginning + printUsages.mkString("\n" + " " * argsOffset))
end printUsage
def printExplain(info: Info): Unit =
def shiftLines(s: Seq[String], shift: Int): String = s.map(" " * shift + _).mkString("\n")
def wrapLongLine(line: String, maxLength: Int): List[String] = {
def recurse(s: String, acc: Vector[String]): Seq[String] =
val lastSpace = s.trim.nn.lastIndexOf(' ', maxLength)
if ((s.length <= maxLength) || (lastSpace < 0))
acc :+ s
else {
val (shortLine, rest) = s.splitAt(lastSpace)
recurse(rest.trim.nn, acc :+ shortLine)
}
recurse(line, Vector()).toList
}
println()
if (info.documentation.nonEmpty)
println(wrapLongLine(info.documentation, maxUsageLineLength).mkString("\n"))
if (info.parameters.nonEmpty) {
val argNameShift = 2
val argDocShift = argNameShift + 2
println("Arguments:")
for param <- info.parameters do
val canonicalName = getNameWithMarker(param.name)
val otherNames = param.aliasNames match {
case Seq() => ""
case names => names.mkString("(", ", ", ") ")
}
val argDoc = StringBuilder(" " * argNameShift)
argDoc.append(s"$canonicalName $otherNames- ${param.typeName.split('.').last}")
if param.isVarargs then argDoc.append(" (vararg)")
else if param.hasDefault then argDoc.append(" (optional)")
if (param.documentation.nonEmpty) {
val shiftedDoc =
param.documentation.split("\n").nn
.map(line => shiftLines(wrapLongLine(line.nn, maxUsageLineLength - argDocShift), argDocShift))
.mkString("\n")
argDoc.append("\n").append(shiftedDoc)
}
println(argDoc)
}
end printExplain
def shouldPrintDefaultHelp(names: Names, args: Seq[String]): Boolean =
val helpIsOverridden = names.canonicalName(helpArg).isDefined
val shortHelpIsOverridden = names.canonicalName(shortHelpArg).isDefined
(!helpIsOverridden && args.contains(helpArg)) ||
(!shortHelpIsOverridden && args.contains(shortHelpArg))
end Help
@experimental // MiMa does not check scope inherited @experimental
private class Names(info: Info):
checkNames()
checkFlags()
private lazy val namesToCanonicalName: Map[String, String] =
info.parameters.flatMap(param =>
val canonicalName = getNameWithMarker(param.name)
(canonicalName -> canonicalName) +: param.aliasNames.map(_ -> canonicalName)
).toMap
private lazy val canonicalFlagsNames: Set[String] =
info.parameters.collect {
case param if param.isFlag => getNameWithMarker(param.name)
}.toSet
def canonicalName(name: String): Option[String] = namesToCanonicalName.get(name)
def isFlagName(name: String): Boolean =
namesToCanonicalName.get(name).map(canonicalFlagsNames.contains).contains(true)
override def toString(): String = s"Names($namesToCanonicalName)"
private def checkNames(): Unit =
def checkDuplicateNames() =
val nameAndCanonicalName = info.parameters.flatMap { paramInfo =>
(getNameWithMarker(paramInfo.name) +: paramInfo.aliasNames).map(_ -> paramInfo.name)
}
val nameToNames = nameAndCanonicalName.groupMap(_._1)(_._2)
for (name, canonicalNames) <- nameToNames if canonicalNames.length > 1 do
throw IllegalArgumentException(s"$name is used for multiple parameters: ${canonicalNames.mkString(", ")}")
def checkValidNames() =
def isValidArgName(name: String): Boolean =
longArgRegex.matches(s"--$name") || shortArgRegex.matches(s"-$name")
for param <- info.parameters do
if !isValidArgName(param.name) then
throw IllegalArgumentException(s"The following argument name is invalid: ${param.name}")
for annot <- param.annotations do
annot match
case alias: alias if !isValidArgName(alias.name) =>
throw IllegalArgumentException(s"The following alias is invalid: ${alias.name}")
case _ =>
checkValidNames()
checkDuplicateNames()
private def checkFlags(): Unit =
for param <- info.parameters if param.isFlag && param.hasDefault do
throw IllegalArgumentException(s"@newMain flag parameters cannot have a default value. `${param.name}` has a default value.")
end Names
end newMain
object newMain:
/** Alias name for the parameter.
*
* If the name has one character, then it is a short name (e.g. `-i`).
* If the name has more than one characters, then it is a long name (e.g. `--input`).
*/
@experimental
final class alias(val name: String) extends MainAnnotation.ParameterAnnotation
end newMain