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