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 }