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 : 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     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     @Values("-d", "--debug")
79     unittest {
80         auto result = parseMainCLI(["dextool", getValue!string]);
81         result.confLog.shouldEqual(VerboseMode.trace);
82     }
83 
84     @("Should be the version category")
85     unittest {
86         auto result = parseMainCLI(["dextool", "--version"]);
87         result.status.shouldEqual(CLICategoryStatus.Version);
88     }
89 
90     @("Should be the help category")
91     @Values("help", "-h", "--help")
92     unittest {
93         auto result = parseMainCLI(["dextool", getValue!string]);
94         result.status.shouldEqual(CLICategoryStatus.Help);
95     }
96 }
97 
98 ExitStatusType runPlugin(CLIResult cli, string[] args) {
99     import std.stdio : writeln;
100     import application.plugin;
101 
102     auto exit_status = ExitStatusType.Errors;
103 
104     auto plugins = scanForExecutables.filterValidPluginsThisExecutable
105         .toPlugins!executePluginForShortHelp;
106 
107     final switch (cli.status) with (CLICategoryStatus) {
108     case Help:
109         writeln(mainOptions, plugins.toShortHelp, commandGrouptHelp);
110         exit_status = ExitStatusType.Ok;
111         break;
112     case Version:
113         import dextool.utility : dextoolVersion;
114 
115         writeln("dextool version ", dextoolVersion);
116         exit_status = ExitStatusType.Ok;
117         break;
118     case NoCategory:
119         logger.error("No plugin specified");
120         writeln("Available plugins:");
121         writeln(plugins.toShortHelp);
122         writeln("-h for further help");
123         exit_status = ExitStatusType.Errors;
124         break;
125     case UnknownPlugin:
126         logger.errorf("No such plugin found: '%s'", cli.category);
127         writeln("Available plugins:");
128         writeln(plugins.toShortHelp);
129         writeln("-h for further help");
130         exit_status = ExitStatusType.Errors;
131         break;
132     case PluginList:
133         // intended to be used in automation. Akin to git "porcelain" commands"
134         foreach (const ref p; plugins) {
135             writeln(p.name);
136         }
137         exit_status = ExitStatusType.Ok;
138         break;
139     case Category:
140         import std.algorithm : filter;
141         import std.process : spawnProcess, wait;
142         import std.range : takeOne;
143 
144         bool match_found;
145 
146         // dfmt off
147         // find the first plugin matching the category
148         foreach (p; plugins
149                  .filter!(p => p.name == cli.category)
150                  .takeOne) {
151             auto pid = spawnProcess([cast(string) p.path] ~ (args.length > 2 ? args[2 .. $] : null));
152             exit_status = wait(pid) == 0 ? ExitStatusType.Ok : ExitStatusType.Errors;
153             match_found = true;
154         }
155         // dfmt on
156 
157         if (!match_found) {
158             // print error message to user as if no category was found
159             cli.status = CLICategoryStatus.UnknownPlugin;
160             exit_status = runPlugin(cli, args);
161         }
162 
163         break;
164     }
165 
166     return exit_status;
167 }
168 
169 int rmain(string[] args) nothrow {
170     import std.exception : collectException;
171     import colorlog : confLogger;
172 
173     auto exit_status = ExitStatusType.Errors;
174 
175     try {
176         auto parsed = parseMainCLI(args);
177         confLogger(parsed.confLog);
178 
179         exit_status = runPlugin(parsed, args);
180     } catch (Exception ex) {
181         logger.trace(ex).collectException;
182         exit_status = ExitStatusType.Errors;
183     }
184 
185     if (exit_status != ExitStatusType.Ok) {
186         logger.errorf("exiting...").collectException;
187     }
188 
189     return cast(int) exit_status;
190 }