1 /**
2  * Copyright: Copyright (c) 2017 Jacob Carlborg. All rights reserved.
3  * Authors: Jacob Carlborg
4  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
5  */
6 module dstep.driver.CommandLine;
7 
8 import std.typecons : tuple, Tuple;
9 import std.getopt;
10 
11 import dstep.Configuration;
12 import dstep.translator.Options;
13 import dstep.core.Exceptions;
14 
15 /**
16  *  Processes command-line arguments
17  *
18  *  Params:
19  *      args = command-line arguments
20  *
21  *  Returns:
22  *      2-element tuple with first element being aggregate struct of app
23  *      configuration and second - getopt parse result
24  */
25 auto parseCommandLine(string[] args)
26 {
27     import std.array : split;
28     import std.getopt;
29 
30     Configuration config;
31 
32     // Parse dstep own parameters:
33 
34     void parseLanguage (string param, string value)
35     {
36         config.clangParams ~= "-x";
37         config.clangParams ~= value;
38 
39         switch (value)
40         {
41             case "c":
42             case "c-header":
43                 config.language = Language.c;
44                 break;
45             case "objective-c":
46             case "objective-c-header":
47                 config.language = Language.objC;
48                 break;
49             default:
50                 throw new DStepException(`Unrecognized language "` ~ value ~ `"`);
51         }
52     }
53 
54     bool forceObjectiveC;
55 
56     auto splittedArgs = args.split("--");
57 
58     if (splittedArgs.length == 1)
59         args = splittedArgs[0];
60     else if (splittedArgs.length == 2)
61     {
62         args = splittedArgs[0];
63         config.clangParams = splittedArgs[1];
64     }
65 
66     auto helpInformation = getopt(
67         args,
68         std.getopt.config.passThrough,
69         std.getopt.config.caseSensitive,
70         "output|o", &config.output,
71         "objective-c", &forceObjectiveC,
72         "language|x", &parseLanguage,
73         makeGetOptArgs!config);
74 
75     // remove dstep binary name (args[0])
76     args = args[1 .. $];
77 
78     // Separate input files from clang parameters:
79     foreach (arg; args)
80     {
81         if (arg[0] == '-')
82             config.clangParams ~= arg;
83         else
84             config.inputFiles ~= arg;
85     }
86 
87     // Post-processing of CLI
88 
89     import std.algorithm : canFind;
90 
91     if (forceObjectiveC)
92         config.clangParams ~= "-ObjC";
93 
94     if (config.clangParams.canFind("-ObjC"))
95         config.language = Language.objC;
96 
97     return tuple(config, helpInformation);
98 }
99 
100 unittest
101 {
102     import std.algorithm.searching : find;
103     import std.range.primitives : empty;
104     import std.meta : AliasSeq;
105 
106     Configuration config;
107     GetoptResult getoptResult;
108 
109     AliasSeq!(config, getoptResult) = parseCommandLine(
110         [ "dstep", "-Xpreprocessor", "-lsomething", "-x", "c-header", "file.h" ]);
111     assert(config.language == Language.c);
112     assert(config.inputFiles == [ "file.h" ]);
113     assert(config.clangParams == [ "-x", "c-header", "-Xpreprocessor", "-lsomething" ]);
114     assert(config.output == "");
115 
116     AliasSeq!(config, getoptResult) = parseCommandLine(
117         [ "dstep", "-ObjC", "file2.h", "--output=folder", "file.h" ]);
118     assert(config.language == Language.objC);
119     assert(config.inputFiles == [ "file2.h", "file.h" ]);
120     assert(config.clangParams == [ "-ObjC" ]);
121     assert(config.output == "folder");
122 
123     AliasSeq!(config, getoptResult) = parseCommandLine(
124         [ "dstep", "file.h", "--skip-definition", "foo" ]);
125     assert(!config.skipDefinitions.find("foo").empty);
126 
127     AliasSeq!(config, getoptResult) = parseCommandLine(
128         [ "dstep", "file.h", "--skip", "foo" ]);
129     assert(!config.skipSymbols.find("foo").empty);
130 
131     AliasSeq!(config, getoptResult) = parseCommandLine(
132         [ "dstep", "file.h", "--skip", "foo", "--skip-definition", "bar" ]);
133     assert(!config.skipDefinitions.find("bar").empty);
134     assert(!config.skipSymbols.find("foo").empty);
135 
136     AliasSeq!(config, getoptResult) = parseCommandLine(
137         [ "dstep", "foo.h", "--", "-include", "bar.h" ]);
138     assert(config.inputFiles == [ "foo.h" ]);
139     assert(config.clangParams == [ "-include", "bar.h" ]);
140 }
141 
142 /**
143  *  "Real" application entry point, handles CLI/config and forwards to
144  *  dstep.driver.Application to do actual work.
145  *  Called by main.
146  */
147 int run(string[] args)
148 {
149     import std.stdio;
150     import std..string;
151     import clang.Util;
152 
153     auto parseResult = parseCommandLine(args);
154     Configuration config = parseResult[0];
155     GetoptResult getoptResult = parseResult[1];
156 
157     if (getoptResult.helpWanted || args.length == 1)
158     {
159         showHelp(config, getoptResult);
160         return 0;
161     }
162 
163     if (config.dstepVersion)
164     {
165         writeln(strip(config.Version));
166         return 0;
167     }
168 
169     if (config.clangVersion)
170     {
171         writeln(clangVersionString());
172         return 0;
173     }
174 
175     import dstep.driver.Application;
176 
177     auto application = new Application(config);
178 
179     try
180     {
181         application.run();
182     }
183     catch (DStepException e)
184     {
185         write(e.msg);
186         return -1;
187     }
188     catch (Throwable e)
189     {
190         writeln("dstep: an unknown error occurred: ", e);
191         throw e;
192     }
193 
194     return 0;
195 
196 }
197 
198 void showHelp (Configuration config, GetoptResult getoptResult)
199 {
200     import std.stdio;
201     import std..string;
202     import std.range;
203     import std.algorithm;
204 
205     struct Entry
206     {
207         this(string option, string help)
208         {
209             this.option = option;
210             this.help = help;
211         }
212 
213         this(Option option)
214         {
215             if (option.optShort && option.optLong)
216                 this.option = format("%s, %s", option.optShort, option.optLong);
217             else if (option.optShort)
218                 this.option = option.optShort;
219             else
220                 this.option = option.optLong;
221 
222             auto pair = findSplitAfter(option.help, "!");
223 
224             if (!pair[0].empty)
225             {
226                 this.option ~= pair[0][0 .. $ - 1];
227                 this.help = pair[1];
228             }
229             else
230             {
231                 this.help = option.help;
232             }
233         }
234 
235         string option;
236         string help;
237     }
238 
239     auto customEntries = [
240         Entry("-o, --output <file>", "Write output to <file>."),
241         Entry("-o, --output <directory>", "Write all the files to <directory>, in case of multiple input files."),
242         Entry("-ObjC, --objective-c", "Treat source input file as Objective-C input."),
243         Entry("-x, --language", "Treat subsequent input files as having type <language>.")];
244 
245     auto generatedEntries = getoptResult.options
246         .filter!(option => !option.help.empty)
247         .map!(option => Entry(option));
248 
249     auto entries = chain(customEntries, generatedEntries);
250 
251     auto maxLength = entries.map!(entry => entry.option.length).array.reduce!max;
252 
253     auto helpString = appender!string();
254 
255     helpString.put("Usage: dstep [options] <input>\n");
256     helpString.put(format("Version: %s\n\n", strip(config.Version)));
257     helpString.put("Options:\n");
258 
259     foreach (entry; entries)
260         helpString.put(format("    %-*s %s\n", cast(int) maxLength + 1, entry.option, entry.help));
261 
262     helpString.put(
263         "\nAll options that Clang accepts can be used as well.\n" ~
264         "Use the `-h' flag for help.");
265 
266     writeln(helpString.data);
267 }