1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  *
6  * Description:
7  * All command line interface options.
8  */
9 module button.cli.options;
10 
11 import std.meta : AliasSeq;
12 
13 import darg;
14 
15 struct Command
16 {
17     string name;
18 }
19 
20 struct Description
21 {
22     string description;
23 }
24 
25 struct GlobalOptions
26 {
27     @Option("help")
28     @Help("Prints help on command line usage.")
29     OptionFlag help;
30 
31     @Option("version")
32     @Help("Prints version information.")
33     OptionFlag version_;
34 
35     @Argument("command", Multiplicity.optional)
36     string command;
37 
38     @Argument("args", Multiplicity.zeroOrMore)
39     const(string)[] args;
40 }
41 
42 // Generate usage and help strings at compile-time.
43 immutable globalUsage = usageString!GlobalOptions("button");
44 immutable globalHelp  = helpString!GlobalOptions();
45 
46 @Command("help")
47 @Description("Displays help on a given command.")
48 struct HelpOptions
49 {
50     @Argument("command", Multiplicity.optional)
51     @Help("Command to get help on.")
52     string command;
53 }
54 
55 @Command("version")
56 @Description("Prints the current version of the program.")
57 struct VersionOptions
58 {
59 }
60 
61 @Command("build")
62 @Description("Runs a build.")
63 struct BuildOptions
64 {
65     @Option("file", "f")
66     @Help("Path to the build description.")
67     string path;
68 
69     @Option("dryrun", "n")
70     @Help("Don't make any functional changes. Just print what might happen.")
71     OptionFlag dryRun;
72 
73     @Option("threads", "j")
74     @Help("The number of threads to use. Default is the number of logical
75             cores.")
76     @MetaVar("N")
77     size_t threads;
78 
79     @Option("color")
80     @Help("When to colorize the output.")
81     @MetaVar("{auto,never,always}")
82     string color = "auto";
83 
84     @Option("verbose", "v")
85     @Help("Display additional information such as how long each task took to"~
86           " complete.")
87     OptionFlag verbose;
88 
89     @Option("autopilot")
90     @Help("After building, continue watching for changes to inputs and"~
91           " building again as necessary.")
92     OptionFlag autopilot;
93 
94     @Option("watchdir")
95     @Help("Used with `--autopilot`. Directory to watch for changes in. Since"~
96           " FUSE does not work with inotify, this is useful to use when"~
97           " building in a union file system.")
98     string watchDir = ".";
99 
100     @Option("delay")
101     @Help("Used with `--autopilot`. The number of milliseconds to wait for"~
102           " additional changes after receiving a change event before starting"~
103           " a build.")
104     size_t delay = 50;
105 }
106 
107 @Command("graph")
108 @Description("Generates a graph for input into GraphViz.")
109 struct GraphOptions
110 {
111     import button.edgedata : EdgeType;
112 
113     @Option("file", "f")
114     @Help("Path to the build description.")
115     string path;
116 
117     @Option("changes", "C")
118     @Help("Only display the subgraph that will be traversed on an update.")
119     OptionFlag changes;
120 
121     @Option("cached")
122     @Help("Display the cached graph from the previous build.")
123     OptionFlag cached;
124 
125     @Option("full")
126     @Help("Display the full name of each vertex.")
127     OptionFlag full;
128 
129     @Option("edges", "e")
130     @MetaVar("{explicit,implicit,both}")
131     @Help("Type of edges to show.")
132     EdgeType edges = EdgeType.explicit;
133 
134     @Option("threads", "j")
135     @Help("The number of threads to use. Default is the number of logical
136             cores.")
137     @MetaVar("N")
138     size_t threads;
139 }
140 
141 @Command("status")
142 @Description("Prints the status of the build. That is, which files have been
143         modified and which tasks are pending.")
144 struct StatusOptions
145 {
146     @Option("file", "f")
147     @Help("Path to the build description.")
148     string path;
149 
150     @Option("cached")
151     @Help("Display the cached graph from the previous build.")
152     OptionFlag cached;
153 
154     @Option("color")
155     @Help("When to colorize the output.")
156     @MetaVar("{auto,never,always}")
157     string color = "auto";
158 
159     @Option("threads", "j")
160     @Help("The number of threads to use. Default is the number of logical
161             cores.")
162     @MetaVar("N")
163     size_t threads;
164 }
165 
166 @Command("clean")
167 @Description("Deletes all build outputs.")
168 struct CleanOptions
169 {
170     @Option("file", "f")
171     @Help("Path to the build description.")
172     string path;
173 
174     @Option("dryrun", "n")
175     @Help("Don't make any functional changes. Just print what might happen.")
176     OptionFlag dryRun;
177 
178     @Option("threads", "j")
179     @Help("The number of threads to use. Default is the number of logical
180             cores.")
181     @MetaVar("N")
182     size_t threads;
183 
184     @Option("color")
185     @Help("When to colorize the output.")
186     @MetaVar("{auto,never,always}")
187     string color = "auto";
188 
189     @Option("purge")
190     @Help("Delete the build state too.")
191     OptionFlag purge;
192 }
193 
194 @Command("init")
195 @Description("Initializes a directory with an initial build description.")
196 struct InitOptions
197 {
198     @Argument("dir", Multiplicity.optional)
199     @Help("Directory to initialize")
200     string dir = ".";
201 }
202 
203 enum ConvertFormat
204 {
205     bash,
206 }
207 
208 @Command("convert")
209 @Description("Converts the build description to another format for other build systems.")
210 struct ConvertOptions
211 {
212     @Option("file", "f")
213     @Help("Path to the build description.")
214     string path;
215 
216     @Option("format")
217     @Help("Format of build description to convert to. Default is 'bash'.")
218     @MetaVar("{bash}")
219     ConvertFormat type;
220 
221     @Argument("output")
222     @Help("Path to the output file.")
223     @MetaVar("FILE")
224     string output;
225 }
226 
227 @Command("gc")
228 @Description("EXPERIMENTAL")
229 struct GCOptions
230 {
231     @Option("file", "f")
232     @Help("Path to the build description.")
233     string path;
234 
235     @Option("dryrun", "n")
236     @Help("Don't make any functional changes. Just print what might happen.")
237     OptionFlag dryRun;
238 
239     @Option("color")
240     @Help("When to colorize the output.")
241     @MetaVar("{auto,never,always}")
242     string color = "auto";
243 }
244 
245 /**
246  * List of all options structs.
247  */
248 alias OptionsList = AliasSeq!(
249         HelpOptions,
250         VersionOptions,
251         BuildOptions,
252         GraphOptions,
253         StatusOptions,
254         CleanOptions,
255         InitOptions,
256         GCOptions,
257         ConvertOptions,
258         );
259 
260 /**
261  * Thrown when an invalid command name is given to $(D runCommand).
262  */
263 class InvalidCommand : Exception
264 {
265     this(string msg)
266     {
267         super(msg);
268     }
269 }
270 
271 /**
272  * Using the list of command functions, runs a command from the specified
273  * string.
274  *
275  * Throws: InvalidCommand if the given command name is not valid.
276  */
277 int runCommand(Funcs...)(string name, GlobalOptions opts)
278 {
279     import std.traits : Parameters, getUDAs;
280     import std.format : format;
281 
282     foreach (F; Funcs)
283     {
284         alias Options = Parameters!F[0];
285 
286         alias Commands = getUDAs!(Options, Command);
287 
288         foreach (C; Commands)
289         {
290             if (C.name == name)
291                 return F(parseArgs!Options(opts.args), opts);
292         }
293     }
294 
295     throw new InvalidCommand("button: '%s' is not a valid command. See 'button help'."
296             .format(name));
297 }