1
+ import annotation .StaticAnnotation
2
+ import collection .mutable
3
+
4
+ /** MainAnnotation provides the functionality for a compiler-generated main class.
5
+ * It links a compiler-generated main method (call it compiler-main) to a user
6
+ * written main method (user-main).
7
+ * The protocol of calls from compiler-main is as follows:
8
+ *
9
+ * - create a `command` with the command line arguments,
10
+ * - for each parameter of user-main, a call to `command.argGetter`,
11
+ * or `command.argsGetter` if is a final varargs parameter,
12
+ * - a call to `command.run` with the closure of user-main applied to all arguments.
13
+ */
14
+ trait MainAnnotation extends StaticAnnotation :
15
+
16
+ /** The class used for argument string parsing. E.g. `scala.util.FromString`,
17
+ * but could be something else
18
+ */
19
+ type ArgumentParser [T ]
20
+
21
+ /** The required result type of the main function */
22
+ type MainResultType
23
+
24
+ /** A new command with arguments from `args` */
25
+ def command (args : Array [String ]): Command
26
+
27
+ /** A class representing a command to run */
28
+ abstract class Command :
29
+
30
+ /** The getter for the next argument of type `T` */
31
+ def argGetter [T ](argName : String , fromString : ArgumentParser [T ], defaultValue : Option [T ] = None ): () => T
32
+
33
+ /** The getter for a final varargs argument of type `T*` */
34
+ def argsGetter [T ](argName : String , fromString : ArgumentParser [T ]): () => Seq [T ]
35
+
36
+ /** Run `program` if all arguments are valid,
37
+ * or print usage information and/or error messages.
38
+ */
39
+ def run (program : => MainResultType , progName : String , docComment : String ): Unit
40
+ end Command
41
+ end MainAnnotation
42
+
43
+ // Sample main class, can be freely implemented:
44
+
45
+ class main extends MainAnnotation :
46
+
47
+ type ArgumentParser [T ] = util.FromString [T ]
48
+ type MainResultType = Any
49
+
50
+ def command (args : Array [String ]): Command = new Command :
51
+
52
+ /** A buffer of demanded argument names, plus
53
+ * "?" if it has a default
54
+ * "*" if it is a vararg
55
+ * "" otherwise
56
+ */
57
+ private var argInfos = new mutable.ListBuffer [(String , String )]
58
+
59
+ /** A buffer for all errors */
60
+ private var errors = new mutable.ListBuffer [String ]
61
+
62
+ /** Issue an error, and return an uncallable getter */
63
+ private def error (msg : String ): () => Nothing =
64
+ errors += msg
65
+ () => assertFail(" trying to get invalid argument" )
66
+
67
+ /** The next argument index */
68
+ private var argIdx : Int = 0
69
+
70
+ private def argAt (idx : Int ): Option [String ] =
71
+ if idx < args.length then Some (args(idx)) else None
72
+
73
+ private def nextPositionalArg (): Option [String ] =
74
+ while argIdx < args.length && args(argIdx).startsWith(" --" ) do argIdx += 2
75
+ val result = argAt(argIdx)
76
+ argIdx += 1
77
+ result
78
+
79
+ private def convert [T ](argName : String , arg : String , p : ArgumentParser [T ]): () => T =
80
+ p.fromStringOption(arg) match
81
+ case Some (t) => () => t
82
+ case None => error(s " invalid argument for $argName: $arg" )
83
+
84
+ def argGetter [T ](argName : String , p : ArgumentParser [T ], defaultValue : Option [T ] = None ): () => T =
85
+ argInfos += ((argName, if defaultValue.isDefined then " ?" else " " ))
86
+ val idx = args.indexOf(s " -- $argName" )
87
+ val argOpt = if idx >= 0 then argAt(idx + 1 ) else nextPositionalArg()
88
+ argOpt match
89
+ case Some (arg) => convert(argName, arg, p)
90
+ case None => defaultValue match
91
+ case Some (t) => () => t
92
+ case None => error(s " missing argument for $argName" )
93
+
94
+ def argsGetter [T ](argName : String , p : ArgumentParser [T ]): () => Seq [T ] =
95
+ argInfos += ((argName, " *" ))
96
+ def remainingArgGetters (): List [() => T ] = nextPositionalArg() match
97
+ case Some (arg) => convert(arg, argName, p) :: remainingArgGetters()
98
+ case None => Nil
99
+ val getters = remainingArgGetters()
100
+ () => getters.map(_())
101
+
102
+ def run (f : => MainResultType , progName : String , docComment : String ): Unit =
103
+ def usage (): Unit =
104
+ println(s " Usage: $progName ${argInfos.map(_ + _).mkString(" " )}" )
105
+
106
+ def explain (): Unit =
107
+ if docComment.nonEmpty then println(docComment) // todo: process & format doc comment
108
+
109
+ def flagUnused (): Unit = nextPositionalArg() match
110
+ case Some (arg) =>
111
+ error(s " unused argument: $arg" )
112
+ flagUnused()
113
+ case None =>
114
+ for
115
+ arg <- args
116
+ if arg.startsWith(" --" ) && ! argInfos.map(_._1).contains(arg.drop(2 ))
117
+ do
118
+ error(s " unknown argument name: $arg" )
119
+ end flagUnused
120
+
121
+ if args.isEmpty || args.contains(" --help" ) then
122
+ usage()
123
+ explain()
124
+ else
125
+ flagUnused()
126
+ if errors.nonEmpty then
127
+ for msg <- errors do println(s " Error: $msg" )
128
+ usage()
129
+ else f match
130
+ case n : Int if n < 0 => System .exit(- n)
131
+ case _ =>
132
+ end run
133
+ end command
134
+ end main
135
+
136
+ // Sample main method
137
+
138
+ object myProgram :
139
+
140
+ /** Adds two numbers */
141
+ @ main def add (num : Int , inc : Int = 1 ): Unit =
142
+ println(s " $num + $inc = ${num + inc}" )
143
+
144
+ end myProgram
145
+
146
+ // Compiler generated code:
147
+
148
+ object add extends main :
149
+ def main (args : Array [String ]) =
150
+ val cmd = command(args)
151
+ val arg1 = cmd.argGetter[Int ](" num" , summon[ArgumentParser [Int ]])
152
+ val arg2 = cmd.argGetter[Int ](" inc" , summon[ArgumentParser [Int ]], Some (1 ))
153
+ cmd.run(myProgram.add(arg1(), arg2()), " add" , " Adds two numbers" )
154
+ end add
155
+
156
+ /** --- Some scenarios ----------------------------------------
157
+
158
+ > java add 2 3
159
+ 2 + 3 = 5
160
+ > java add 4
161
+ 4 + 1 = 5
162
+ > java add --num 10 --inc -2
163
+ 10 + -2 = 8
164
+ > java add --num 10
165
+ 10 + 1 = 11
166
+ > java add --help
167
+ Usage: add num inc?
168
+ Adds two numbers
169
+ > java add
170
+ Usage: add num inc?
171
+ Adds two numbers
172
+ > java add 1 2 3 4
173
+ Error: unused argument: 3
174
+ Error: unused argument: 4
175
+ Usage: add num inc?
176
+ > java add -n 1 -i 10
177
+ Error: invalid argument for num: -n
178
+ Error: unused argument: -i
179
+ Error: unused argument: 10
180
+ Usage: add num inc?
181
+ > java add --n 1 --i 10
182
+ Error: missing argument for num
183
+ Error: unknown argument name: --n
184
+ Error: unknown argument name: --i
185
+ Usage: add num inc?
186
+ > java add true 10
187
+ Error: invalid argument for num: true
188
+ Usage: add num inc?
189
+ > java add true false
190
+ Error: invalid argument for num: true
191
+ Error: invalid argument for inc: false
192
+ Usage: add num inc?
193
+ > java add true false 10
194
+ Error: invalid argument for num: true
195
+ Error: invalid argument for inc: false
196
+ Error: unused argument: 10
197
+ Usage: add num inc?
198
+ > java add --inc 10 --num 20
199
+ 20 + 10 = 30
200
+ > java add binary 10 01
201
+ Error: invalid argument for num: binary
202
+ Error: unused argument: 01
203
+ Usage: add num inc?
204
+
205
+ */
0 commit comments