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