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 }