1 module jcli.commandgraph.cli;
2 
3 import jcli.commandgraph;
4 import jcli.commandgraph.internal;
5 import jcli.commandgraph.graph;
6 
7 import std.stdio : writefln, writeln;
8 
9 private alias State = MatchAndExecuteState;
10 
11 /// Don't forget to cut off the executable name: args = arg[1 .. $];
12 int executeSingleCommand(TCommand)(scope string[] args)
13 {
14     auto defaultHandler = DefaultParseErrorHandler();
15     return executeSingleCommand!TCommand(args, defaultHandler);
16 }
17 
18 /// ditto
19 int executeSingleCommand
20 (
21     TCommand,
22     TErrorHandler
23 )
24 (
25     scope string[] args,
26     ref scope TErrorHandler errorHandler
27 )
28 {
29     alias bindArgument = jcli.argbinder.bindArgument!();
30     alias Graph = DegenerateCommandTypeGraph!TCommand;
31     alias TypeContext = MatchAndExecuteTypeContext!(bindArgument, CommandTypeContext!Graph);
32 
33     TypeContext.ParsingContext parsingContext;
34     MatchAndExecuteContext context;
35     auto tokenizer = argTokenizer(args);
36     
37     // This basically fools the state machine into thinking it's already matched the root command.
38     {
39         enum commandTypeIndex = 0;
40         TypeContext.setMatchedRootCommand!commandTypeIndex(context, parsingContext);
41     }
42 
43     // Do the normal workflow until we reach a terminal state.
44     while (!(context._state & State.terminalStateBit))
45     {
46         TypeContext.advanceState(context, parsingContext, tokenizer, errorHandler);
47     }
48 
49     switch (context._state)
50     {
51         default:
52             assert(0, "Not all terminal states handled.");
53 
54         // This one is only issued when matching the root command, so it should never be hit.
55         case State.firstTokenNotCommand:
56 
57         case State.invalid:
58         case State.notMatchedNextCommand:
59         case State.notMatchedRootCommand:
60             assert(0, "The context came to an invalid state — internal API misuse.");
61 
62         case State.specialThing:
63         {
64             switch (context._specialThing)
65             {
66                 default:
67                     assert(0, "Some special thing was not been properly handled");
68 
69                 case SpecialThings.help:
70                 {
71                     import jcli.helptext;
72                     // writeHelpText!TCommand()
73                     CommandHelpText!TCommand help;
74                     writeln(help.generate());
75                     return 0;
76                 }
77             }
78         }
79         case State.finalExecutionResult:
80         {
81             if (context._executeCommandResult.exception !is null)
82             {
83                 writeln(context._executeCommandResult.exception);
84                 return -1;
85             }
86             return context._executeCommandResult.exitCode;
87         }
88         case State.tokenizerError:
89         case State.commandParsingError:
90         {
91             return -1;
92         }
93     }
94 }
95 
96 unittest
97 {
98     @Command("Hello")
99     static struct A
100     {
101         @ArgPositional("Error to return")
102         int err;
103 
104         int onExecute() { return err; }
105     }
106 
107     {
108         auto errorHandler = ErrorCodeHandler();
109         assert(executeSingleCommand!A(["11"], errorHandler) == 11);
110     }
111     {
112         auto errorHandler = ErrorCodeHandler();
113         assert(executeSingleCommand!A(["0"], errorHandler) == 0);
114     }
115     {
116         auto errorHandler = ErrorCodeHandler();
117         assert(executeSingleCommand!A([], errorHandler) != 0);
118         assert(errorHandler.hasError(CommandParsingErrorCode.tooFewPositionalArgumentsError));
119     }
120     {
121         auto errorHandler = ErrorCodeHandler();
122         assert(executeSingleCommand!A(["A"], errorHandler) != 0);
123         assert(errorHandler.hasError(CommandParsingErrorCode.bindError));
124     }
125     {
126         auto errorHandler = ErrorCodeHandler();
127         // Will print some garbage, but it's fine.
128         assert(executeSingleCommand!A(["-h"], errorHandler) == 0);
129     }
130 }
131 
132 unittest
133 {    
134     @Command("Hello")
135     static struct A
136     {
137         @("Error to return")
138         int err;
139 
140         int onExecute() { return err; }
141     }
142     
143     {
144         auto errorHandler = ErrorCodeHandler();
145         assert(executeSingleCommand!A(["-err", "11"], errorHandler) == 11);
146     }
147     {
148         auto errorHandler = ErrorCodeHandler();
149         assert(executeSingleCommand!A(["-err", "0"], errorHandler) == 0);
150     }
151     {
152         auto errorHandler = ErrorCodeHandler();
153         assert(executeSingleCommand!A(["-err"], errorHandler) != 0);
154         assert(errorHandler.hasError(CommandParsingErrorCode.noValueForNamedArgumentError));
155     }
156     {
157         auto errorHandler = ErrorCodeHandler();
158         assert(CommandArgumentsInfo!A.named[0].flags.has(ArgFlags._requiredBit));
159         assert(executeSingleCommand!A([], errorHandler) != 0);
160         assert(errorHandler.hasError(CommandParsingErrorCode.missingNamedArgumentsError));
161     }
162 }
163 
164 mixin template SingleCommandMain(TCommand)
165 {
166     int main(string[] args)
167     {
168         return executeSingleCommand!TCommand(args[1 .. $]);
169     }
170 }
171 
172 
173 /// Uses the bottom-up command gathering approach.
174 template matchAndExecuteAcrossModules(Modules...)
175 {
176     alias bind = bindArgumentAcrossModules!Modules;
177     alias Types = AllCommandsOf!Modules;
178     alias matchAndExecuteAcrossModules = matchAndExecute!(bind, BottomUpCommandTypeGraph!Types);
179 }
180 
181 /// Uses the top-down approach, and the given bind argument function.
182 /// You don't need to scan the modules to do the top-down approach.
183 template matchAndExecuteFromRootCommands(alias bindArgument, RootCommandTypes...)
184 {
185     alias matchAndExecuteFromRootCommands = matchAndExecute!(bindArgument, TopDownCommandTypeGraph!RootCommandTypes);
186 }
187 
188 /// Constructs the graph of the given command types, ...
189 template matchAndExecute(alias bindArgument, alias Graph)
190 {
191     private alias _matchAndExecute = .matchAndExecute!(bindArgument, Graph);
192     private alias _CommandTypeContext = CommandTypeContext!(Graph);
193 
194     /// Forwards.
195     int matchAndExecute
196     (
197         Tokenizer : ArgTokenizer!T, T,
198         TErrorHandler
199     )
200     (
201         scope ref Tokenizer tokenizer,
202         scope ref TErrorHandler errorHandler
203     )
204     {
205         return MatchAndExecuteTypeContext!(bindArgument, _CommandTypeContext)
206             .matchAndExecute(tokenizer, errorHandler);
207     }
208 
209     /// Resolves the invoked command by parsing the arguments array.
210     /// Executes the `onExecute` method of any intermediate commands (command groups).
211     /// Returns the result of the execution, and whether there were errors.
212     int matchAndExecute(scope string[] args)
213     {
214         auto tokenizer = argTokenizer(args);
215         return _matchAndExecute(tokenizer);
216     }
217     
218     /// ditto
219     /// Uses the default error handler.
220     int matchAndExecute(Tokenizer : ArgTokenizer!T, T)
221     (
222         scope ref Tokenizer tokenizer
223     )
224     {
225         auto handler = DefaultParseErrorHandler();
226         return _matchAndExecute(tokenizer, handler);
227     }
228 }