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 }