1 /** 2 Date: 2015-2017, Joakim Brännström 3 License: MPL-2, Mozilla Public License 2.0 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 */ 6 module application.app_main; 7 8 import logger = std.experimental.logger; 9 10 import colorlog : VerboseMode; 11 import dextool.type : ExitStatusType; 12 13 import application.cli_help; 14 15 version (unittest) { 16 import unit_threaded : shouldEqual; 17 } 18 19 private enum CLICategoryStatus { 20 Category, 21 Help, 22 Version, 23 NoCategory, 24 UnknownPlugin, 25 PluginList, 26 } 27 28 private struct CLIResult { 29 CLICategoryStatus status; 30 string category; 31 VerboseMode confLog; 32 string[] args; 33 } 34 35 /** Parse the raw command line. 36 */ 37 auto parseMainCLI(const string[] args) { 38 import std.algorithm : among, filter, findAmong; 39 import std.array : array, empty; 40 41 auto loglevel = findAmong(args, ["-d", "--debug"]).empty ? VerboseMode.info : VerboseMode.trace; 42 // -d/--debug interferes with -h/--help/help and cli category therefore 43 // remove 44 string[] rem_args = args.dup.filter!(a => !a.among("-d", "--debug")).array(); 45 46 CLICategoryStatus state; 47 48 if (rem_args.length <= 1) { 49 state = CLICategoryStatus.NoCategory; 50 } else if (rem_args.length >= 2 && rem_args[1].among("help", "-h", "--help")) { 51 state = CLICategoryStatus.Help; 52 } else if (rem_args.length >= 2 && rem_args[1].among("--version")) { 53 state = CLICategoryStatus.Version; 54 } else if (rem_args.length >= 2 && rem_args[1].among("--plugin-list")) { 55 state = CLICategoryStatus.PluginList; 56 } 57 58 string category = rem_args.length >= 2 ? rem_args[1] : null; 59 60 return CLIResult(state, category, loglevel); 61 } 62 63 version (unittest) { 64 import std.algorithm : findAmong; 65 import std.array : empty; 66 67 // May seem unnecessary testing to test the CLI but bugs have been 68 // introduced accidentally in parseMainCLI. 69 // It is also easier to test "main CLI" here because it takes the least 70 // setup and has no side effects. 71 72 @("Should be no category") 73 unittest { 74 parseMainCLI(["dextool"]).status.shouldEqual(CLICategoryStatus.NoCategory); 75 } 76 77 @("Should flag that debug mode is to be activated") 78 unittest { 79 foreach (getValue; ["-d", "--debug"]) { 80 auto result = parseMainCLI(["dextool", getValue]); 81 result.confLog.shouldEqual(VerboseMode.trace); 82 } 83 } 84 85 @("Should be the version category") 86 unittest { 87 auto result = parseMainCLI(["dextool", "--version"]); 88 result.status.shouldEqual(CLICategoryStatus.Version); 89 } 90 91 @("Should be the help category") 92 unittest { 93 foreach (getValue; ["help", "-h", "--help"]) { 94 auto result = parseMainCLI(["dextool", getValue]); 95 result.status.shouldEqual(CLICategoryStatus.Help); 96 } 97 } 98 } 99 100 ExitStatusType runPlugin(CLIResult cli, string[] args) { 101 import std.stdio : writeln; 102 import application.plugin; 103 104 auto exit_status = ExitStatusType.Errors; 105 106 auto plugins = scanForExecutables.filterValidPluginsThisExecutable 107 .toPlugins!executePluginForShortHelp; 108 109 final switch (cli.status) with (CLICategoryStatus) { 110 case Help: 111 writeln(mainOptions, plugins.toShortHelp, commandGrouptHelp); 112 exit_status = ExitStatusType.Ok; 113 break; 114 case Version: 115 import dextool.utility : dextoolVersion; 116 117 writeln("dextool version ", dextoolVersion); 118 exit_status = ExitStatusType.Ok; 119 break; 120 case NoCategory: 121 logger.error("No plugin specified"); 122 writeln("Available plugins:"); 123 writeln(plugins.toShortHelp); 124 writeln("-h for further help"); 125 exit_status = ExitStatusType.Errors; 126 break; 127 case UnknownPlugin: 128 logger.errorf("No such plugin found: '%s'", cli.category); 129 writeln("Available plugins:"); 130 writeln(plugins.toShortHelp); 131 writeln("-h for further help"); 132 exit_status = ExitStatusType.Errors; 133 break; 134 case PluginList: 135 // intended to be used in automation. Akin to git "porcelain" commands" 136 foreach (const ref p; plugins) { 137 writeln(p.name); 138 } 139 exit_status = ExitStatusType.Ok; 140 break; 141 case Category: 142 import std.algorithm : filter; 143 import std.process : spawnProcess, wait; 144 import std.range : takeOne; 145 146 bool match_found; 147 148 // dfmt off 149 // find the first plugin matching the category 150 foreach (p; plugins 151 .filter!(p => p.name == cli.category) 152 .takeOne) { 153 auto pid = spawnProcess([cast(string) p.path] ~ (args.length > 2 ? args[2 .. $] : null)); 154 exit_status = wait(pid) == 0 ? ExitStatusType.Ok : ExitStatusType.Errors; 155 match_found = true; 156 } 157 // dfmt on 158 159 if (!match_found) { 160 // print error message to user as if no category was found 161 cli.status = CLICategoryStatus.UnknownPlugin; 162 exit_status = runPlugin(cli, args); 163 } 164 165 break; 166 } 167 168 return exit_status; 169 } 170 171 int rmain(string[] args) nothrow { 172 import std.exception : collectException; 173 import colorlog : confLogger; 174 175 auto exit_status = ExitStatusType.Errors; 176 177 try { 178 auto parsed = parseMainCLI(args); 179 confLogger(parsed.confLog); 180 181 exit_status = runPlugin(parsed, args); 182 } catch (Exception ex) { 183 logger.trace(ex).collectException; 184 exit_status = ExitStatusType.Errors; 185 } 186 187 if (exit_status != ExitStatusType.Ok) { 188 logger.errorf("exiting...").collectException; 189 } 190 191 return cast(int) exit_status; 192 }