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 }