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 dextool.logger_conf : ConfigureLog; 11 import dextool.type : FileName, ExitStatusType; 12 13 import application.cli_help; 14 15 version (unittest) { 16 import unit_threaded : shouldEqual, Values, getValue; 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 ConfigureLog 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 ConfigureLog loglevel = findAmong(args, ["-d", "--debug"]).empty 42 ? ConfigureLog.info : ConfigureLog.debug_; 43 // -d/--debug interferes with -h/--help/help and cli category therefore 44 // remove 45 string[] rem_args = args.dup.filter!(a => !a.among("-d", "--debug")).array(); 46 47 CLICategoryStatus state; 48 49 if (rem_args.length <= 1) { 50 state = CLICategoryStatus.NoCategory; 51 } else if (rem_args.length >= 2 && rem_args[1].among("help", "-h", "--help")) { 52 state = CLICategoryStatus.Help; 53 } else if (rem_args.length >= 2 && rem_args[1].among("--version")) { 54 state = CLICategoryStatus.Version; 55 } else if (rem_args.length >= 2 && rem_args[1].among("--plugin-list")) { 56 state = CLICategoryStatus.PluginList; 57 } 58 59 string category = rem_args.length >= 2 ? rem_args[1] : null; 60 61 return CLIResult(state, category, loglevel); 62 } 63 64 version (unittest) { 65 import std.algorithm : findAmong; 66 import std.array : empty; 67 68 // May seem unnecessary testing to test the CLI but bugs have been 69 // introduced accidentally in parseMainCLI. 70 // It is also easier to test "main CLI" here because it takes the least 71 // setup and has no side effects. 72 73 @("Should be no category") 74 unittest { 75 parseMainCLI(["dextool"]).status.shouldEqual(CLICategoryStatus.NoCategory); 76 } 77 78 @("Should flag that debug mode is to be activated") 79 @Values("-d", "--debug") 80 unittest { 81 auto result = parseMainCLI(["dextool", getValue!string]); 82 result.confLog.shouldEqual(ConfigureLog.debug_); 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 @Values("help", "-h", "--help") 93 unittest { 94 auto result = parseMainCLI(["dextool", getValue!string]); 95 result.status.shouldEqual(CLICategoryStatus.Help); 96 } 97 } 98 99 ExitStatusType runPlugin(CLIResult cli, string[] args) { 100 import std.stdio : writeln; 101 import application.plugin; 102 103 auto exit_status = ExitStatusType.Errors; 104 105 auto plugins = scanForExecutables.filterValidPluginsThisExecutable 106 .toPlugins!executePluginForShortHelp; 107 108 final switch (cli.status) with (CLICategoryStatus) { 109 case Help: 110 writeln(mainOptions, plugins.toShortHelp, commandGrouptHelp); 111 exit_status = ExitStatusType.Ok; 112 break; 113 case Version: 114 import dextool.utility : dextoolVersion; 115 116 writeln("dextool version ", dextoolVersion); 117 exit_status = ExitStatusType.Ok; 118 break; 119 case NoCategory: 120 logger.error("No plugin specified"); 121 writeln("Available plugins:"); 122 writeln(plugins.toShortHelp); 123 writeln("-h for further help"); 124 exit_status = ExitStatusType.Errors; 125 break; 126 case UnknownPlugin: 127 logger.errorf("No such plugin found: '%s'", cli.category); 128 writeln("Available plugins:"); 129 writeln(plugins.toShortHelp); 130 writeln("-h for further help"); 131 exit_status = ExitStatusType.Errors; 132 break; 133 case PluginList: 134 // intended to be used in automation. Akin to git "porcelain" commands" 135 foreach (const ref p; plugins) { 136 writeln(p.name); 137 } 138 exit_status = ExitStatusType.Ok; 139 break; 140 case Category: 141 import std.algorithm : filter; 142 import std.process : spawnProcess, wait; 143 import std.range : takeOne; 144 145 bool match_found; 146 147 // dfmt off 148 // find the first plugin matching the category 149 foreach (p; plugins 150 .filter!(p => p.name == cli.category) 151 .takeOne) { 152 auto pid = spawnProcess([cast(string) p.path] ~ (args.length > 2 ? args[2 .. $] : null)); 153 exit_status = wait(pid) == 0 ? ExitStatusType.Ok : ExitStatusType.Errors; 154 match_found = true; 155 } 156 // dfmt on 157 158 if (!match_found) { 159 // print error message to user as if no category was found 160 cli.status = CLICategoryStatus.UnknownPlugin; 161 exit_status = runPlugin(cli, args); 162 } 163 164 break; 165 } 166 167 return exit_status; 168 } 169 170 int rmain(string[] args) nothrow { 171 import std.conv : text; 172 import std.exception : collectException; 173 import dextool.logger_conf : confLogLevel; 174 175 ExitStatusType exit_status = ExitStatusType.Errors; 176 177 try { 178 auto parsed = parseMainCLI(args); 179 confLogLevel(parsed.confLog); 180 logger.trace(parsed); 181 182 exit_status = runPlugin(parsed, args); 183 } 184 catch (Exception ex) { 185 collectException(logger.trace(text(ex))); 186 exit_status = ExitStatusType.Errors; 187 } 188 189 if (exit_status != ExitStatusType.Ok) { 190 logger.errorf("exiting...").collectException; 191 } 192 193 return cast(int) exit_status; 194 }