1 /*******************************************************************************
2 
3     Application extension to parse command line arguments.
4 
5     Copyright:
6         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
7         All rights reserved.
8 
9     License:
10         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
11         Alternatively, this file may be distributed under the terms of the Tango
12         3-Clause BSD License (see LICENSE_BSD.txt for details).
13 
14 *******************************************************************************/
15 
16 module ocean.util.app.ext.ArgumentsExt;
17 
18 
19 
20 
21 import ocean.util.app.model.ExtensibleClassMixin;
22 import ocean.util.app.model.IApplicationExtension;
23 import ocean.util.app.ext.model.IArgumentsExtExtension;
24 
25 import ocean.text.Arguments;
26 import ocean.io.Stdout : Stdout, Stderr;
27 
28 import ocean.meta.types.Qualifiers;
29 import ocean.io.stream.Format : FormatOutput;
30 
31 
32 
33 /*******************************************************************************
34 
35     Application extension to parse command line arguments.
36 
37     This extension is an extension itself, providing new hooks via
38     IArgumentsExtExtension.
39 
40     By default it adds a --help command line argument to show a help message.
41 
42 *******************************************************************************/
43 
44 class ArgumentsExt : IApplicationExtension
45 {
46 
47     /***************************************************************************
48 
49         Adds a list of extensions (this.extensions) and methods to handle them.
50         See ExtensibleClassMixin documentation for details.
51 
52     ***************************************************************************/
53 
54     mixin ExtensibleClassMixin!(IArgumentsExtExtension);
55 
56 
57     /***************************************************************************
58 
59         Formatted output stream to use to print normal messages.
60 
61     ***************************************************************************/
62 
63     protected FormatOutput stdout;
64 
65 
66     /***************************************************************************
67 
68         Formatted output stream to use to print error messages.
69 
70     ***************************************************************************/
71 
72     protected FormatOutput stderr;
73 
74 
75     /***************************************************************************
76 
77         Command line arguments parser and storage.
78 
79     ***************************************************************************/
80 
81     public Arguments args;
82 
83 
84     /***************************************************************************
85 
86         Constructor.
87 
88         See ocean.text.Arguments for details on format of the parameters.
89 
90         Params:
91             name = Name of the application (to show in the help message)
92             desc = Short description of what the program does (should be
93                          one line only, preferably less than 80 characters)
94             usage = How the program is supposed to be invoked
95             help = Long description of what the program does and how to use it
96             stdout = Formatted output stream to use to print normal messages
97             stderr = Formatted output stream to use to print error messages
98 
99     ***************************************************************************/
100 
101     public this ( istring name = null, istring desc = null,
102             istring usage = null, istring help = null,
103             FormatOutput stdout = Stdout,
104             FormatOutput stderr = Stderr )
105     {
106         this.stdout = stdout;
107         this.stderr = stderr;
108         this.args = new Arguments(name, desc, usage, help);
109     }
110 
111 
112     /***************************************************************************
113 
114         Extension order. This extension uses -100_000 because it should be
115         called very early.
116 
117         Returns:
118             the extension order
119 
120     ***************************************************************************/
121 
122     public override int order ( )
123     {
124         return -100_000;
125     }
126 
127 
128     /***************************************************************************
129 
130         Setup, parse, validate and process command line args (Application hook).
131 
132         This function does all the extension processing invoking all the
133         extension hooks. It also adds the --help option, which when present,
134         shows the help and exits the program.
135 
136         If argument parsing or validation fails (including extensions
137         validation), it also prints an error message and exits. Note that if
138         argument parsing fails, validation is not performed.
139 
140         Params:
141             app = the application instance
142             cl_args = command line arguments
143 
144     ***************************************************************************/
145 
146     public override void preRun ( IApplication app, istring[] cl_args )
147     {
148         auto args = this.args;
149 
150         args("help").aliased('h').params(0)
151             .help("display this help message and exit");
152 
153         foreach (ext; this.extensions)
154         {
155             ext.setupArgs(app, args);
156         }
157 
158         cstring[] errors;
159         auto args_ok = args.parse(cl_args[1 .. $]);
160 
161         if ( args.exists("help") )
162         {
163             args.displayHelp(this.stdout);
164             app.exit(0);
165         }
166 
167         foreach (ext; this.extensions)
168         {
169             ext.preValidateArgs(app, args);
170         }
171 
172         if ( args_ok )
173         {
174             foreach (ext; this.extensions)
175             {
176                 auto error = ext.validateArgs(app, args);
177                 if (error != "")
178                 {
179                     errors ~= error;
180                     args_ok = false;
181                 }
182             }
183         }
184 
185         if (!args_ok)
186         {
187             auto ocean_stderr = cast (typeof(Stderr)) this.stderr;
188             if (ocean_stderr !is null)
189                 ocean_stderr.red;
190             args.displayErrors(this.stderr);
191             foreach (error; errors)
192             {
193                 this.stderr.format("{}", error).newline;
194             }
195             if (ocean_stderr !is null)
196                 ocean_stderr.default_colour;
197             this.stderr.formatln("\nType {} -h for help", app.name);
198             app.exit(2);
199         }
200 
201         foreach (ext; this.extensions)
202         {
203             ext.processArgs(app, args);
204         }
205     }
206 
207 
208     /***************************************************************************
209 
210         Unused IApplicationExtension methods.
211 
212         We just need to provide an "empty" implementation to satisfy the
213         interface.
214 
215     ***************************************************************************/
216 
217     public override void postRun ( IApplication app, istring[] args, int status )
218     {
219         // Unused
220     }
221 
222     public override void atExit ( IApplication app, istring[] args, int status,
223             ExitException exception )
224     {
225         // Unused
226     }
227 
228     public override ExitException onExitException ( IApplication app,
229             istring[] args, ExitException exception )
230     {
231         // Unused
232         return exception;
233     }
234 
235 }
236 
237 
238 
239 /*******************************************************************************
240 
241     Tests
242 
243 *******************************************************************************/
244 
245 version (unittest)
246 {
247     import ocean.core.Test;
248     import ocean.util.app.Application;
249     import ocean.io.device.MemoryDevice;
250     import ocean.io.stream.Text : TextOutput;
251     import ocean.core.Array : find;
252 
253     class App : Application
254     {
255         this ( ) { super("app", "A test application"); }
256         protected override int run ( istring[] args ) { return 10; }
257     }
258 }
259 
260 
261 /*******************************************************************************
262 
263     Test --help can be used even when required arguments are not specified
264 
265 *******************************************************************************/
266 
267 unittest
268 {
269 
270     auto stdout_dev = new MemoryDevice;
271     auto stdout = new TextOutput(stdout_dev);
272 
273     auto stderr_dev = new MemoryDevice;
274     auto stderr = new TextOutput(stderr_dev);
275 
276     istring usage_text = "test: usage";
277     istring help_text = "test: help";
278     auto arg = new ArgumentsExt("test-name", "test-desc", usage_text, help_text,
279             stdout, stderr);
280     arg.args("--required").params(1).required;
281 
282     auto app = new App;
283 
284     try
285     {
286         arg.preRun(app, ["./app", "--help"]);
287         test(false, "An ExitException should have been thrown");
288     }
289     catch (ExitException e)
290     {
291         // Status should be 0 (success)
292         test!("==")(e.status, 0);
293         // No errors should be printed
294         test!("==")(stderr_dev.bufferSize, 0);
295         // Help should be printed to stdout
296         auto s = cast(mstring) stdout_dev.peek();
297         test(s.length > 0,
298                 "Stdout should have some help message but it's empty");
299         test(s.find(arg.args.short_desc) < s.length,
300              "No application description found in help message:\n" ~ s);
301         test(s.find(usage_text) < s.length,
302              "No usage text found in help message:\n" ~ s);
303         test(s.find(help_text) < s.length,
304              "No help text found in help message:\n" ~ s);
305         test(s.find("--help"[]) < s.length,
306              "--help should be found in help message:\n" ~ s);
307     }
308 }