├── LICENSE ├── README.md └── argon.d /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Mark Stephen Laker 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Argon 2 | A processor for command-line arguments, an alternative to Getopt, written in D 3 | 4 | ```d 5 | #!/usr/bin/rdmd --shebang -unittest -g -debug -w 6 | 7 | import argon; 8 | 9 | import std.stdio; 10 | 11 | // Imagine a program that creates widgets of some kind. 12 | 13 | enum Colours {black, blue, green, cyan, red, magenta, yellow, white} 14 | 15 | // Write a class that inherits from argon.Handler: 16 | 17 | class MyHandler: argon.Handler { 18 | 19 | // Inside your class, define a set of data members. 20 | // Argon will copy user input into these variables. 21 | 22 | uint size; 23 | Colours colour; 24 | bool winged; 25 | uint nr_windows; 26 | string name; 27 | argon.Indicator got_name; 28 | 29 | // In your constructor, make a series of calls to Named(), 30 | // Pos() and (not shown here) Incremental(). These calls tell Argon 31 | // what kind of input to expect and where to deposit the input after 32 | // decoding and checking it. 33 | 34 | this() { 35 | // The first argument is positional (meaning that the user specifies 36 | // it just after the command name, with an --option-name), because we 37 | // called Pos(). It's mandatory, because the Pos() invocation doesn't 38 | // specify a default value or an indicator. (Indicators are explained 39 | // below.) The AddRange() call rejects user input that isn't between 40 | // 1 and 20, inclusive. 41 | Pos("size of the widget", size).AddRange(1, 20); 42 | 43 | // The second argument is also positional, but it's optional, because 44 | // we specified a default colour: by default, our program will create 45 | // a green widget. The user specifies colours by their names ('black', 46 | // 'blue', etc.), or any unambiguous abbreviation. 47 | Pos("colour of the widget", colour, Colours.green); 48 | 49 | // The third argument is a Boolean option that is named, as all 50 | // Boolean arguments are. That means a user who wants to override 51 | // the default has to specify it by typing "--winged", or some 52 | // unambiguous abbreviation of it. We've also provided a -w shortcut. 53 | // 54 | // All Boolean arguments are optional. 55 | Named("winged", winged) ('w'); 56 | 57 | // The fourth argument, the number of windows, is a named argument, 58 | // with a long name of --windows and a short name of -i, and it's 59 | // optional. A user who doesn't specify a window count gets six 60 | // windows. Our AddRange() call ensures that no widget has more 61 | // than twelve and, because we pass in a uint, Argon will reject 62 | // all negative numbers. The string "number of windows" is called a 63 | // description, and helps Argon auto-generate a more helpful 64 | // syntax summary. 65 | Named("windows", nr_windows, 6) ('i') ("number of windows").AddRange(0, 12); 66 | 67 | // The user can specify a name for the new widget. Since the user 68 | // could explicitly specify an empty name, our program uses an 69 | // indicator, got_name, to determine whether a name was specified or 70 | // not, rather than checking whether the name is empty. 71 | Named("name", name, got_name) ('n').LimitLength(0, 20); 72 | } 73 | 74 | // Now write a separate method that calls Parse() and does something with 75 | // the user's input. If the input is valid, your class's data members will 76 | // be populated; otherwise, Argon will throw an exception. 77 | 78 | auto Run(string[] args) { 79 | try { 80 | Parse(args); 81 | writeln("Size: ", size); 82 | writeln("Colour: ", colour); 83 | writeln("Wings? ", winged); 84 | writeln("Windows: ", nr_windows); 85 | if (got_name) 86 | writeln("Name: ", name); 87 | 88 | return 0; 89 | } 90 | catch (argon.ParseException x) { 91 | stderr.writeln(x.msg); 92 | stderr.writeln(BuildSyntaxSummary); 93 | return 1; 94 | } 95 | } 96 | } 97 | 98 | int main(string[] args) { 99 | auto handler = new MyHandler; 100 | return handler.Run(args); 101 | } 102 | ``` 103 | 104 | There's plenty more that Argon can do. 105 | 106 | Features for you: 107 | 108 | * You can mix mandatory and optional arguments 109 | * You can freely mix named arguments (specified with `--option` syntax) and positional arguments (defined by their position on the command line) 110 | * Argon has first-class support for positional arguments: they're validated, range-checked and deposited into type-safe variables; you don't end up picking them out of `argv` and converting them manually after named arguments have been removed 111 | * Argon can either pass through unused arguments (as long as they don't look like `--option`s) or fail the parse, whichever you prefer, although its first-class support for positional arguments makes the need for pass-through rare 112 | * Optional arguments have caller-defined default values 113 | * Argon can open files specified at the command line, with an open mode and error-handling protocol that you choose and a convention that `"-"` stands for either `stdin` or `stdout`, depending on the open mode 114 | * An argument can have a different default value if an option is specified at the end of a command line, so that `list-it --as-text` can be short for `list-it --as-text -`, which opens `stdout`, whereas `list-it --as-text output.txt` creates `output.txt` 115 | * You can tell unambiguously whether a user specified an argument, without having to pick a default value and hope the user doesn't guess it 116 | * You can specify range-checks for numerical arguments and length limits for string arguments 117 | * Argon supports non-Ascii long and short option names and enum names 118 | * Argon auto-generates syntax summaries (help text), with support for optional argument descriptions and undocumented arguments; letting Argon generate syntax summaries makes it more likely that they'll stay current under maintenance 119 | * Argument groups enable flexible specification of which combinations of arguments are allowed together, so that you don't have to write error-checking code yourself 120 | * You can choose decimal, hex, octal or even binary as the default radix for individual numeric arguments -- though the user can always override your default 121 | * String arguments can be validated by regular expressions with associated user-friendly error messages; these regexes can capture segments of the user's input, so that it's ready to use after a successful parse 122 | * Incremental arguments increment a counter by 1 each time they're used, as in `--verbose --verbose --verbose` or `-vvv` 123 | * An argument can have any number of names, so you can please everyone by supporting both `--colour` and `--color` 124 | * Argon gently encourages you to move your command-line processing into a class of its own for better modularity and lifetime-management, although you *can* use it without deriving a class if you wish 125 | 126 | Features for your users: 127 | 128 | * The ability to abbreviate option names 129 | * Flexible syntax: a user can say `-t5`, `-t=5` or `-t 5` 130 | * Bundling of short names, enabled by default, so that `-a -b -c 5` can be written as `-abc5` 131 | * The `--` options terminator, which forces all subsequent tokens to be interpreted as positional arguments 132 | * The ability to abbreviate option names and `enum` values 133 | * The ability to specify numeric arguments in binary, octal or hex as an alternative to decimal 134 | * Better error messages than it would be worth writing for most programs 135 | 136 | Some of this functionality is available in `std.getopt`; some isn't. Stick with `getopt` if you need the following features, which are not currently implemented in Argon: 137 | 138 | * Array arguments 139 | * Hash arguments 140 | * Callback options 141 | * Case-insensitivity: a mix of Unix-style dashes and double dashes with DOS-style case-insensitivity 142 | * The ability to ignore and pass back unused arguments that look like `--option`s: only tokens resembling positional arguments can be passed back 143 | 144 | Future directions: 145 | 146 | * Make Handler.BuildSyntaxSummary() reflect the first-or-none behaviour of consecutive positional, optional args, as well as first-or-none and all-or-none argument groups. 147 | * An optional, positional File argument already supports a simplified version of what `cat`(1) does and what many Perl programs do: read from a file specified at the command line, or stdin otherwise. It would be good to generalise this functionality to be able to read from any number of input files, as Perl's diamond operator does. 148 | 149 | Argon parses Unix-style syntax, as in `ls -lah --author foo*`. If you wanted to support other syntaxes, such as MS-DOS-style `dir foo.* /s /b`, replacing struct Parser and a few parts of class Handler, along with all the unit tests that follow them, should get you most of the way. The rest of the code ought to be reusable in more or less its current form. The author has no plans to support MS-DOS-style syntax. 150 | -------------------------------------------------------------------------------- /argon.d: -------------------------------------------------------------------------------- 1 | #!/usr/bin/rdmd --shebang -unittest -g -debug --main 2 | 3 | module argon; 4 | 5 | /+ 6 | Copyright (c) 2016, Mark Stephen Laker 7 | 8 | Permission to use, copy, modify, and/or distribute this software for any 9 | purpose with or without fee is hereby granted, provided that the above 10 | copyright notice and this permission notice appear in all copies. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 13 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 14 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 15 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 16 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 17 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 18 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 | +/ 20 | 21 | import core.exception; 22 | import std.algorithm.iteration; 23 | import std.algorithm.searching; 24 | import std.algorithm.sorting; 25 | import std.array; 26 | import std.conv; 27 | import std.exception; 28 | import std.regex; 29 | import std.stdio; 30 | import std.traits; 31 | import std.uni; 32 | 33 | @safe { 34 | 35 | // These filenames are used for unittest blocks. Feel free to increase test 36 | // coverage by adding stanzas for other operating systems. 37 | 38 | version(linux) { 39 | immutable existent_file = "/dev/zero"; 40 | immutable nonexistent_file = "/dev/onshire-ice-cream"; 41 | } 42 | else { 43 | immutable existent_file = ""; 44 | immutable nonexistent_file = ""; 45 | } 46 | 47 | /++ 48 | + If the user specifies invalid input at the command line, Argon will throw 49 | + a ParseException. This class is a subclass of Exception, and so you can 50 | + extract the text of the exception by calling `.msg()` on it. 51 | +/ 52 | 53 | class ParseException: Exception { 54 | this(A...) (A a) { 55 | super(text(a)); 56 | } 57 | } 58 | 59 | /++ 60 | + You can optionally use an indicator to tell whether an argument was supplied 61 | + at the command line. 62 | +/ 63 | 64 | enum Indicator { 65 | NotSeen, /// The user didn't specify the argument 66 | UsedEolDefault, /// The user specified the named option at the end of the line and relied on the end-of-line default 67 | UsedEqualsDefault, /// The user specified the named option but didn't follow it with `=' and a value 68 | Seen /// The user specified the argument 69 | } 70 | 71 | // FArg -- formal argument (supplied in code); AArg -- actual argument (supplied 72 | // by the user at runtime). 73 | 74 | // This is the base class for all FArg classes: 75 | 76 | class FArgBase { 77 | private: 78 | string[] names; // All long names, without dashes 79 | string description; // The meaning of an argument: used when we auto-generate an element of a syntax summary from a named option 80 | dchar shortname; // A single-char shortcut 81 | bool needs_aarg; // Always true except for bool fargs 82 | Indicator *p_indicator; // Pointer to the caller's indicator, or null 83 | bool seen; // Have we matched an AArg with this FArg? 84 | bool positional; // Is this positional -- identified by its position on the command line, matchable without an option name? 85 | bool mandatory; // If true, some AArg must match this FArg or parsing will fail 86 | bool documented = true; // True if this FArg should appear in syntax summaries 87 | bool has_eol_default; // Has an end-of-line default value 88 | bool has_equals_default; // Any actual arg must be attached with `=', and --switch without `=' is a shorthand for equals_default 89 | bool is_incremental; // Is an incremental argument, which increments its receiver each time it's seen 90 | 91 | auto SetSeen(in Indicator s) { 92 | seen = s != Indicator.NotSeen; 93 | if (p_indicator) 94 | *p_indicator = s; 95 | } 96 | 97 | protected: 98 | this(in string nms, in bool na, Indicator *pi) { 99 | names = nms.splitter('|').filter!(name => !name.empty).array; 100 | needs_aarg = na; 101 | p_indicator = pi; 102 | 103 | if (pi) 104 | *pi = Indicator.NotSeen; 105 | } 106 | 107 | auto MarkSeen() { SetSeen(Indicator.Seen); } 108 | auto MarkSeenWithEolDefault() { SetSeen(Indicator.UsedEolDefault); } 109 | auto MarkSeenWithEqualsDefault() { SetSeen(Indicator.UsedEqualsDefault); } 110 | auto MarkUnseen() { SetSeen(Indicator.NotSeen); } 111 | auto SetShortName(in dchar snm) { shortname = snm; } 112 | auto SetDescription(in string desc) { description = desc; } 113 | auto MarkUndocumented() { documented = false; } 114 | auto MarkIncremental() { is_incremental = true; } 115 | 116 | auto MarkEolDefault() { 117 | assert(!HasEqualsDefault, "A single argument can't have both an end-of-line default and an equals default"); 118 | has_eol_default = true; 119 | } 120 | 121 | auto MarkEqualsDefault() { 122 | assert(!HasEolDefault, "A single argument can't have both an end-of-line default and an equals default"); 123 | has_equals_default = true; 124 | } 125 | 126 | public: 127 | auto GetFirstName() const { return names[0]; } 128 | auto GetNames() const { return names; } 129 | auto HasShortName() const { return shortname != shortname.init; } 130 | auto GetShortName() const { return shortname; } 131 | auto GetDescription() const { return description; } 132 | auto NeedsAArg() const { return needs_aarg; } 133 | auto IsPositional() const { return positional; } 134 | auto IsNamed() const { return !positional; } 135 | auto IsMandatory() const { return mandatory; } 136 | auto HasBeenSeen() const { return seen; } 137 | auto IsDocumented() const { return documented; } 138 | auto HasEolDefault() const { return has_eol_default; } 139 | auto IsIncremental() const { return is_incremental; } 140 | auto HasEqualsDefault() const { return has_equals_default; } 141 | 142 | /++ 143 | + Adds a single-letter short name (not necessarily Ascii) to avoid the need 144 | + for users to type out the long name. 145 | + 146 | + Only a named option can have a short name. A positional argument 147 | + doesn't need one, because its name is never typed in by users. 148 | +/ 149 | 150 | auto Short(this T) (in dchar snm) { 151 | assert(!positional, "A positional argument can't have a short name"); 152 | assert(!std.uni.isWhite(snm), "An option's short name can't be a whitespace character"); 153 | assert(snm != '-', "An option's short name can't be a dash, because '--' would be interpreted as the end-of-options token"); 154 | assert(snm != '=', "An option's short name can't be an equals sign, because '=' is used to conjoin values with short option names and would be confusing with bundling enabled"); 155 | SetShortName(snm); 156 | return cast(T) this; 157 | } 158 | 159 | /// Sugar for adding a single-character short name: 160 | auto opCall(this T) (in dchar snm) { 161 | return cast(T) Short(snm); 162 | } 163 | 164 | /++ 165 | + Adds a description, which is used in error messages. It's also used when 166 | + syntax summaries are auto-generated, so that we can generate something 167 | + maximally descriptive like `--windows `. Without 168 | + this, we'd either generate the somewhat opaque `--windows ` 169 | + or have to impose extra typing on the user by renaming the option as 170 | + `--number-of-windows`. 171 | + 172 | + The description needn't be Ascii. 173 | + 174 | + Only a named option can have a description. For a positional 175 | + argument, the name is never typed by the user and it does double duty 176 | + as a description. 177 | +/ 178 | 179 | auto Description(this T) (in string desc) { 180 | assert(!positional, "A positional argument doesn't need a description: use the ordinary name field for that"); 181 | FArgBase.SetDescription(desc); 182 | return cast(T) this; 183 | } 184 | 185 | /// Sugar for adding a description: 186 | auto opCall(this T) (in string desc) { 187 | return cast(T) Description(desc); 188 | } 189 | 190 | /// Excludes an argument from auto-generated syntax summaries: 191 | auto Undocumented(this T) () { 192 | MarkUndocumented(); 193 | return cast(T) this; 194 | } 195 | 196 | // Indicate that this can be a positional argument: 197 | auto MarkPositional() { positional = true; } 198 | 199 | // Indicate that this argument is mandatory: 200 | auto MarkMandatory() { mandatory = true; } 201 | 202 | // The framework tells us to set default values before we start: 203 | abstract void SetFArgToDefault(); 204 | 205 | // We've been passed an actual argument: 206 | enum InvokedBy {LongName, ShortName, Position}; 207 | 208 | abstract void See(in string aarg, in InvokedBy); 209 | abstract void SeeEolDefault(); 210 | abstract void SeeEqualsDefault(); 211 | 212 | // Overridden by BoolFArg and IncrementalFArg only: 213 | void See() { 214 | assert(false); 215 | } 216 | 217 | // Produce a string such as "the --alpha option (also known as --able and 218 | // --alfie and --alfred)". 219 | 220 | auto DisplayAllNames() const { 221 | string result = "the --" ~ names[0] ~ " option"; 222 | if (names.length > 1) { 223 | auto prefix = " (also known as --"; 224 | foreach (name; names[1..$]) { 225 | result ~= prefix; 226 | result ~= name; 227 | prefix = " and --"; 228 | } 229 | result ~= ')'; 230 | } 231 | 232 | return result; 233 | } 234 | 235 | // Not capitalised; don't use the description at the start of a sentence: 236 | auto DescribeArgumentForError(in InvokedBy invocation) const { 237 | final switch (invocation) { 238 | case InvokedBy.LongName: 239 | assert(!positional); 240 | return !description.empty? description: DisplayAllNames; 241 | case InvokedBy.ShortName: 242 | assert(!positional); 243 | assert(HasShortName); 244 | return !description.empty? description: text("the -", shortname, " option"); 245 | case InvokedBy.Position: 246 | assert(positional); 247 | return text("the ", names[0]); 248 | } 249 | } 250 | 251 | auto DescribeArgumentOptimallyForError() const { 252 | immutable invocation = IsPositional? InvokedBy.Position: InvokedBy.LongName; 253 | return DescribeArgumentForError(invocation); 254 | } 255 | 256 | static FirstNonEmpty(string a, string b) { 257 | return !a.empty? a: b; 258 | } 259 | 260 | auto BuildBareSyntaxElement() const { 261 | if (IsPositional) 262 | return text('<', GetFirstName, '>'); 263 | 264 | immutable stump = text("--", GetFirstName); 265 | if (!NeedsAArg) 266 | return stump; 267 | 268 | return text(stump, " <", FirstNonEmpty(GetDescription, GetFirstName), '>'); 269 | } 270 | 271 | auto BuildSyntaxElement() const { 272 | return IsMandatory? BuildBareSyntaxElement: 273 | IsIncremental? BuildBareSyntaxElement ~ '*': 274 | text('[', BuildBareSyntaxElement, ']'); 275 | } 276 | 277 | // After all aargs have been seen, every farg gets the chance to postprocess 278 | // any aarg or default it's been given. 279 | void Transform() { } 280 | } 281 | 282 | // Holding the receiver pointer in a templated base class separate from the 283 | // rest of the inherited functionality makes it easier for the compiler to 284 | // avoid bloat by reducing the amount of templated code. 285 | 286 | class HasReceiver(FArg): FArgBase { 287 | protected: 288 | FArg *p_receiver; 289 | FArg dfault; 290 | FArg special_default; // Either an EOL default or an equals default -- no argument can have both 291 | 292 | this(in string name, in bool needs_aarg, Indicator *p_indicator, FArg *pr, FArg df) { 293 | super(name, needs_aarg, p_indicator); 294 | p_receiver = pr; 295 | dfault = df; 296 | } 297 | 298 | // Returns a non-empty error message on failure or null on success: 299 | string ViolatesConstraints(in FArg, in InvokedBy) { 300 | return null; 301 | } 302 | 303 | abstract FArg Parse(in char[] aarg, in InvokedBy invocation); 304 | 305 | override void SeeEolDefault() { 306 | *p_receiver = special_default; 307 | MarkSeenWithEolDefault; 308 | } 309 | 310 | override void SeeEqualsDefault() { 311 | *p_receiver = special_default; 312 | MarkSeenWithEqualsDefault; 313 | } 314 | 315 | void SetEolDefault(FArg def) { 316 | assert(IsNamed, "Only a named option can have an end-of-line default; for a positional argument, use an ordinary default"); 317 | special_default = def; 318 | MarkEolDefault; 319 | } 320 | 321 | void SetEqualsDefault(FArg def) { 322 | assert(IsNamed, "Only a named option can have an equals default; for a positional argument, use an ordinary default"); 323 | special_default = def; 324 | MarkEqualsDefault; 325 | } 326 | 327 | public: 328 | final override void SetFArgToDefault() { 329 | *p_receiver = dfault; 330 | MarkUnseen; 331 | } 332 | 333 | override void See(in string aarg, in InvokedBy invocation) { 334 | *p_receiver = Parse(aarg, invocation); 335 | if (const msg = ViolatesConstraints(*p_receiver, invocation)) 336 | throw new ParseException(msg); 337 | 338 | MarkSeen; 339 | } 340 | } 341 | 342 | /++ 343 | + Adds the ability to set an end-of-line and equals defaults and return this. 344 | + The only FArg classes that don't get this are Booleans (for which the idea of 345 | + an EOL default makes no sense) and File (whose FArg makes its own provision). 346 | +/ 347 | 348 | mixin template CanSetSpecialDefaults() { 349 | /++ 350 | + Provides an end-of-line default for a named option. This default is 351 | + used only if the user specifies the option name as the last token on 352 | + the command line and doesn't follow it with a value. For example: 353 | + ---- 354 | + class MyHandler: argon.Handler { 355 | + uint width; 356 | + 357 | + this() { 358 | + Named("wrap", width, 0).SetEolDefault(80); 359 | + } 360 | + 361 | + // ... 362 | + } 363 | + ---- 364 | + Suppose your command is called `list-it`. If the user runs `list-it` 365 | + with no command line arguments, `width` will be zero. If the user runs 366 | + `list-it --wrap` then `width` will equal 80. If the user runs 367 | + `list-it --wrap 132` then `width` will equal 132. 368 | + 369 | + An end-of-line default provides the only way for a user to type the name 370 | + of a non-Boolean option without providing a value. 371 | +/ 372 | 373 | auto EolDefault(T) (T def) { 374 | SetEolDefault(def); 375 | return this; 376 | } 377 | 378 | /++ 379 | + Provides an equals default for a named option, which works like 380 | + grep's --colour option: 381 | + 382 | + * Omitting --colour is equivalent to --colour=none 383 | + 384 | + * Supplying --colour on its own is equivalent to --colour=auto 385 | + 386 | + * Any other value must be attached to the --colour switch by one equals 387 | + sign and no space, as in --colour=always 388 | +/ 389 | 390 | auto EqualsDefault(T) (T def) { 391 | SetEqualsDefault(def); 392 | return this; 393 | } 394 | } 395 | 396 | // Holds a FArg for a Boolean AArg. 397 | 398 | class BoolFArg: HasReceiver!bool { 399 | this(in string name, bool *p_receiver, bool dfault) { 400 | super(name, false, null, p_receiver, dfault); 401 | } 402 | 403 | alias See = HasReceiver!bool.See; 404 | 405 | override void See() { 406 | *p_receiver = !dfault; 407 | MarkSeen; 408 | } 409 | 410 | protected: 411 | override bool Parse(in char[] aarg, in InvokedBy invocation) { 412 | if (!aarg.empty) { 413 | if ("no" .startsWith(aarg) || "false".startsWith(aarg) || aarg == "0") 414 | return false; 415 | if ("yes".startsWith(aarg) || "true" .startsWith(aarg) || aarg == "1") 416 | return true; 417 | } 418 | 419 | throw new ParseException("Invalid argument for ", DescribeArgumentForError(invocation)); 420 | } 421 | } 422 | 423 | @system unittest { 424 | // unittest blocks such as this one have to be @system because they take 425 | // the address of a local var, such as `target'. 426 | alias InvokedBy = FArgBase.InvokedBy; 427 | 428 | bool target; 429 | 430 | auto ba0 = new BoolFArg("big red switch setting", &target, false); 431 | assert(ba0.GetFirstName == "big red switch setting"); 432 | assert(!ba0.HasShortName); 433 | assert(!ba0.IsPositional); 434 | assert(!ba0.IsMandatory); 435 | assert(!ba0.HasBeenSeen); 436 | 437 | ba0.MarkMandatory; 438 | assert(ba0.IsMandatory); 439 | 440 | ba0.MarkPositional; 441 | assert(ba0.IsPositional); 442 | 443 | assert(!target); 444 | ba0.See; 445 | assert(target); 446 | ba0.See("n", InvokedBy.Position); 447 | assert(!target); 448 | ba0.See("y", InvokedBy.Position); 449 | assert(target); 450 | ba0.See("no", InvokedBy.Position); 451 | assert(!target); 452 | ba0.See("yes", InvokedBy.Position); 453 | assert(target); 454 | 455 | ba0.See("f", InvokedBy.Position); 456 | assert(!target); 457 | ba0.See("t", InvokedBy.Position); 458 | assert(target); 459 | ba0.See("fa", InvokedBy.Position); 460 | assert(!target); 461 | ba0.See("tr", InvokedBy.Position); 462 | assert(target); 463 | ba0.See("fal", InvokedBy.Position); 464 | assert(!target); 465 | ba0.See("tru", InvokedBy.Position); 466 | assert(target); 467 | ba0.See("false", InvokedBy.Position); 468 | assert(!target); 469 | ba0.See("true", InvokedBy.Position); 470 | assert(target); 471 | 472 | ba0.See("0", InvokedBy.Position); 473 | assert(!target); 474 | ba0.See("1", InvokedBy.Position); 475 | assert(target); 476 | 477 | try { 478 | ba0.See("no!", InvokedBy.Position); 479 | assert(false, "BoolFArg should have thrown a ParseException as a result of an invalid actual argument"); 480 | } 481 | catch (ParseException x) 482 | assert(x.msg == "Invalid argument for the big red switch setting", "Message was: " ~ x.msg); 483 | 484 | auto ba1 = new BoolFArg("big blue switch setting", &target, false); 485 | ba1('j'); 486 | assert(ba1.HasShortName); 487 | assert(ba1.GetShortName == 'j'); 488 | } 489 | 490 | // Common functionality for all numeric FArgs, whether integral or floating: 491 | 492 | class NumericFArgBase(Num, Num RangeInterval): HasReceiver!Num { 493 | private: 494 | // Implements AddRange(), which permits callers to specify one or more 495 | // ranges of acceptable AArgs: 496 | static struct ValRange(Num) { 497 | alias Self = ValRange!Num; 498 | Num minval, maxval; 499 | 500 | auto MergeWith(in ref Self other) { 501 | if (other.minval >= this.maxval && other.minval - this.maxval <= RangeInterval) { 502 | this.maxval = other.maxval; 503 | return true; 504 | } 505 | 506 | return false; 507 | } 508 | 509 | auto opCmp(in ref Self other) const { 510 | return this.minval < other.minval? -1: 511 | this.minval > other.minval? +1: 512 | 0; 513 | } 514 | 515 | auto toString() const { 516 | return minval == maxval? 517 | minval.to!string: 518 | text("between ", minval, " and ", maxval); 519 | } 520 | 521 | auto Matches(Num n) const { 522 | return n >= minval && n <= maxval; 523 | } 524 | } 525 | 526 | alias Range = ValRange!Num; 527 | Range[] vranges; 528 | 529 | auto MergeRanges() { 530 | if (vranges.length < 2) 531 | return; 532 | 533 | Range[] settled = [vranges[0]]; 534 | foreach (const ref vr; vranges[1..$]) 535 | if (!settled[$-1].MergeWith(vr)) 536 | settled ~= vr; 537 | 538 | vranges = settled; 539 | } 540 | 541 | protected: 542 | this(in string name, Num *p_receiver, Indicator *p_indicator, in Num dfault) { 543 | super(name, true, p_indicator, p_receiver, dfault); 544 | } 545 | 546 | final AddRangeRaw(in Num min, in Num max) { 547 | vranges ~= Range(min, max); 548 | sort(vranges); 549 | MergeRanges; 550 | } 551 | 552 | abstract Num ParseNumber(const(char)[] aarg) const; 553 | 554 | final override Num Parse(in char[] aarg, in InvokedBy invocation) { 555 | Num result = void; 556 | try 557 | result = ParseNumber(aarg); 558 | catch (Exception e) 559 | throw new ParseException("Invalid argument for ", DescribeArgumentForError(invocation), ": ", aarg); 560 | 561 | return result; 562 | } 563 | 564 | final RangeErrorMessage(in InvokedBy invocation) const { 565 | assert(!vranges.empty); 566 | string result = "The argument for " ~ DescribeArgumentForError(invocation) ~ " must be "; 567 | foreach (uint index, const ref vr; vranges) { 568 | result ~= vr.toString; 569 | const remaining = cast(uint) vranges.length - index - 1; 570 | if (remaining > 1) 571 | result ~= ", "; 572 | else if (remaining == 1) 573 | result ~= " or "; 574 | } 575 | 576 | return result; 577 | } 578 | 579 | final override string ViolatesConstraints(in Num n, in InvokedBy invocation) { 580 | return 581 | vranges.empty? null: 582 | vranges.any!(vr => vr.Matches(n))? null: 583 | RangeErrorMessage(invocation); 584 | } 585 | } 586 | 587 | // short, ushort, int, uint, etc: 588 | 589 | /++ 590 | + By calling Pos() or Named() with an integral member variable, your program 591 | + creates an instance of (some template specialisation of) class IntegralFArg. 592 | + This class has methods governing the way numbers are interpreted and the 593 | + range of values your program is willing to accept. 594 | +/ 595 | 596 | class IntegralFArg(Num): NumericFArgBase!(Num, 1) if (isIntegral!Num && !is(Num == enum)) { 597 | private: 598 | alias Radix = uint; 599 | Radix def_radix = 10; 600 | 601 | protected: 602 | final override Num ParseNumber(const(char)[] aarg) const { 603 | auto radices = ["0b": 2u, "0o": 8u, "0n": 10u, "0x": 16u]; 604 | Radix radix = def_radix; 605 | if (aarg.length > 2) 606 | if (const ptr = aarg[0..2] in radices) { 607 | radix = *ptr; 608 | aarg = aarg[2..$]; 609 | } 610 | 611 | return aarg.to!Num(radix); 612 | } 613 | 614 | public: 615 | mixin CanSetSpecialDefaults; 616 | 617 | this(in string name, Num *p_receiver, Indicator *p_indicator, in Num dfault) { 618 | super(name, p_receiver, p_indicator, dfault); 619 | } 620 | 621 | /++ 622 | + The AddRange() methods enable you to specify any number of valid ranges 623 | + for the user's input. If you specify two or more overlapping or adjacent 624 | + ranges (in any order), Argon will amalgamate them when displaying error 625 | + messages. If you specify a default value for your argument, it can 626 | + safely lie outside all the ranges you specify; testing for a value 627 | + outside the permitted space is one way to test whether the user specified 628 | + a number explicitly or relied on the default. 629 | + 630 | + The first overload adds a single permissible value. 631 | +/ 632 | 633 | final AddRange(in Num n) { 634 | AddRangeRaw(n, n); 635 | return this; 636 | } 637 | 638 | /// The second overload adds a range of permissible values. 639 | 640 | final AddRange(in Num min, in Num max) { 641 | AddRangeRaw(min, max); 642 | return this; 643 | } 644 | 645 | /++ 646 | + The default radix for integral number is normally decimal; the user can 647 | + specify `0b`, `0o` or `0x` to have Argon parse integral numbers in 648 | + binary, octal or hex. (A leading zero is not enough to force 649 | + interpretation in octal.) 650 | + 651 | + SetDefaultRadix() can changes the default radix to binary, octal or hex 652 | + (or, indeed, back to decimal); a user wishing to specify numbers in 653 | + decimal when that's not the default base must use a `0n` prefix. 654 | + 655 | + Use this facility sparingly and document its use clearly. It's easy to 656 | + take users by surprise. 657 | +/ 658 | 659 | final SetDefaultRadix(in uint dr) { 660 | def_radix = dr; 661 | return this; 662 | } 663 | } 664 | 665 | @system unittest { 666 | alias InvokedBy = FArgBase.InvokedBy; 667 | alias IntArg = IntegralFArg!int; 668 | alias Range = IntArg.Range; 669 | 670 | int target; 671 | Indicator seen; 672 | auto ia0 = new IntArg("fred", &target, &seen, 5); 673 | assert(target == target.init); 674 | assert(seen == Indicator.NotSeen); 675 | assert(ia0.vranges.empty); 676 | 677 | assert(!ia0.HasShortName); 678 | ia0('f'); 679 | assert(ia0.HasShortName); 680 | assert(ia0.GetShortName == 'f'); 681 | 682 | ia0.See("8", InvokedBy.LongName); 683 | assert(seen == Indicator.Seen); 684 | assert(target == 8); 685 | 686 | ia0.AddRange(50, 59); 687 | assert(ia0.vranges == [Range(50, 59)]); 688 | ia0.AddRange(49); 689 | assert(ia0.vranges == [Range(49, 59)]); 690 | ia0.AddRange(60); 691 | assert(ia0.vranges == [Range(49, 60)]); 692 | ia0.AddRange(70, 75); 693 | assert(ia0.vranges == [Range(49, 60), Range(70, 75)]); 694 | ia0.AddRange(42, 44); 695 | assert(ia0.vranges == [Range(42, 44), Range(49, 60), Range(70, 75)]); 696 | ia0.AddRange(45, 47); 697 | assert(ia0.vranges == [Range(42, 47), Range(49, 60), Range(70, 75)]); 698 | ia0.AddRange(48); 699 | assert(ia0.vranges == [Range(42, 60), Range(70, 75)]); 700 | 701 | ia0.AddRange(20, 24); 702 | ia0.AddRange(100); 703 | foreach (i; [20, 24, 42, 60, 70, 75, 100]) 704 | assert(ia0.ViolatesConstraints(i, InvokedBy.LongName) is null); 705 | 706 | foreach (i; [19, 25, 41, 61, 69, 76, 99, 101]) { 707 | immutable error = ia0.ViolatesConstraints(i, InvokedBy.LongName); 708 | assert(error == "The argument for the --fred option must be between 20 and 24, between 42 and 60, between 70 and 75 or 100", "Error was: " ~ error); 709 | } 710 | 711 | auto ia1 = new IntArg("françoise", &target, null, 5); 712 | 713 | foreach (radix, val; [2:4, 8:64, 10:100, 16:256]) { 714 | ia1.SetDefaultRadix(radix); 715 | ia1.See("100", InvokedBy.LongName); 716 | assert(target == val); 717 | 718 | foreach (str, meaning; ["0b100":4, "0o100":64, "0n100":100, "0x100":256]) { 719 | ia1.See(str, InvokedBy.LongName); 720 | assert(target == meaning); 721 | } 722 | } 723 | 724 | try { 725 | ia1.See("o", InvokedBy.LongName); 726 | assert(false); 727 | } 728 | catch (ParseException x) 729 | assert(x.msg == "Invalid argument for the --françoise option: o", "Message was: " ~ x.msg); 730 | } 731 | 732 | 733 | // float, double, real: 734 | 735 | /++ 736 | + By calling Pos() or Named() with a floating-point member variable, your 737 | + program creates an instance of (some template specialisation of) class 738 | + FloatingFArg. Like IntegralFArg, FloatingFArg enables you to limit the range 739 | + of numbers your program will accept. There is no provision, however, for 740 | + users to specify floating point numbers in radices other than 10. 741 | +/ 742 | 743 | class FloatingFArg(Num): NumericFArgBase!(Num, 0.0) if (isFloatingPoint!Num) { 744 | mixin CanSetSpecialDefaults; 745 | 746 | this(in string name, Num *p_receiver, Indicator *p_indicator, in Num dfault) { 747 | super(name, p_receiver, p_indicator, dfault); 748 | } 749 | 750 | protected: 751 | final override Num ParseNumber(const(char)[] aarg) const { 752 | return aarg.to!Num; 753 | } 754 | 755 | public: 756 | /++ 757 | + AddRange() enables you to specify any number of valid ranges 758 | + for the user's input. If you specify two or more overlapping or touching 759 | + ranges (in any order), Argon will amalgamate them when displaying error 760 | + messages. If you specify a default value for your argument, it can 761 | + safely lie outside all the ranges you specify; testing for a value 762 | + outside the permitted space is one way to test whether the user specified 763 | + a number explicitly or relied on the default. 764 | +/ 765 | final AddRange(in Num min, in Num max) { 766 | AddRangeRaw(min, max); 767 | return this; 768 | } 769 | } 770 | 771 | @system unittest { 772 | alias InvokedBy = FArgBase.InvokedBy; 773 | alias DblArg = FloatingFArg!double; 774 | alias Range = DblArg.Range; 775 | 776 | double receiver; 777 | Indicator indicator; 778 | auto da0 = new DblArg("fred", &receiver, &indicator, double.init); 779 | assert(da0.ViolatesConstraints(0.0, InvokedBy.ShortName) is null); 780 | 781 | da0.AddRange(1, 2); 782 | da0.AddRange(0, 1); 783 | assert(da0.vranges == [Range(0, 2)]); 784 | 785 | da0.AddRange(3.5, 4.25); 786 | assert(da0.vranges == [Range(0, 2), Range(3.5, 4.25)]); 787 | 788 | foreach (double f; [0, 1, 2, 3.5, 4.25]) 789 | assert(da0.ViolatesConstraints(f, InvokedBy.LongName) is null); 790 | 791 | foreach (f; [-0.5, 2.5, 3.0, 4.5]) { 792 | immutable error = da0.ViolatesConstraints(f, InvokedBy.LongName); 793 | assert(error == "The argument for the --fred option must be between 0 and 2 or between 3.5 and 4.25", "Error was: " ~ error); 794 | } 795 | } 796 | 797 | // Enumerations: 798 | 799 | class EnumeralFArg(E): HasReceiver!E if (is(E == enum)) { 800 | private: 801 | E *p_receiver; 802 | E dfault; 803 | 804 | protected: 805 | final override E Parse(in char[] aarg, in InvokedBy invocation) { 806 | uint nr_found; 807 | E result; 808 | foreach (val; EnumMembers!E) { 809 | const txt = text(val); 810 | if (txt.startsWith(aarg)) { 811 | result = val; 812 | if (txt.length == aarg.length) 813 | // 'bar' isn't ambiguous if the available values are 'bar' 814 | // and 'barfly': 815 | return val; 816 | 817 | ++nr_found; 818 | } 819 | } 820 | 821 | if (nr_found == 0) 822 | throw new ParseException('\'', aarg, "' is not a permitted value for ", 823 | DescribeArgumentForError(invocation), 824 | "; permitted values are ", RenderValidOptions("")); 825 | 826 | if (nr_found > 1) 827 | throw new ParseException("In ", DescribeArgumentForError(invocation), ", '", aarg, 828 | "' is ambiguous; permitted values starting with those characters are ", 829 | RenderValidOptions(aarg)); 830 | 831 | return result; 832 | } 833 | 834 | public: 835 | mixin CanSetSpecialDefaults; 836 | 837 | this(in string name, E *p_receiver, Indicator *p_indicator, in E dfault) { 838 | super(name, true, p_indicator, p_receiver, dfault); 839 | } 840 | 841 | static RenderValidOptions(in char[] root) { 842 | return [EnumMembers!E] 843 | .map!(e => e.text) 844 | .filter!(str => str.startsWith(root)) 845 | .join(", "); 846 | } 847 | } 848 | 849 | @system unittest { 850 | alias InvokedBy = FArgBase.InvokedBy; 851 | enum Colours {black, red, green, yellow, blue, magenta, cyan, white} 852 | Colours colour; 853 | Indicator cseen; 854 | 855 | alias ColourArg = EnumeralFArg!Colours; 856 | auto ca0 = new ColourArg("fred", &colour, &cseen, Colours.init); 857 | 858 | assert(!ca0.HasShortName); 859 | ca0('ä'); 860 | assert(ca0.HasShortName); 861 | assert(ca0.GetShortName == 'ä'); 862 | 863 | assert(ca0.RenderValidOptions("") == "black, red, green, yellow, blue, magenta, cyan, white"); 864 | assert(ca0.RenderValidOptions("b") == "black, blue"); 865 | assert(ca0.RenderValidOptions("bl") == "black, blue"); 866 | assert(ca0.RenderValidOptions("bla") == "black"); 867 | assert(ca0.RenderValidOptions("X") == ""); 868 | 869 | enum Girls {Zoë, Françoise, Beyoncé} 870 | Girls girl; 871 | Indicator gseen; 872 | alias GirlArg = EnumeralFArg!Girls; 873 | auto ga0 = new GirlArg("name", &girl, &gseen, Girls.init); 874 | assert(ga0.RenderValidOptions("") == "Zoë, Françoise, Beyoncé"); 875 | assert(ga0.RenderValidOptions("F") == "Françoise"); 876 | 877 | assert(cseen == Indicator.NotSeen); 878 | ca0.See("yellow", InvokedBy.LongName); 879 | assert(colour == Colours.yellow); 880 | ca0.See("whi", InvokedBy.LongName); 881 | assert(colour == Colours.white); 882 | ca0.See("g", InvokedBy.LongName); 883 | assert(colour == Colours.green); 884 | 885 | foreach (offering; ["b", "bl"]) 886 | try { 887 | ca0.See(offering, InvokedBy.LongName); 888 | assert(false); 889 | } 890 | catch (ParseException x) 891 | assert(x.msg == "In the --fred option, '" ~ offering ~ "' is ambiguous; permitted values starting with those characters are black, blue", "Message was: " ~ x.msg); 892 | 893 | ca0.See("blu", InvokedBy.LongName); 894 | assert(colour == Colours.blue); 895 | ca0.See("bla", InvokedBy.LongName); 896 | assert(colour == Colours.black); 897 | 898 | assert(gseen == Indicator.NotSeen); 899 | ga0.See("Z", InvokedBy.LongName); 900 | assert(girl == Girls.Zoë); 901 | assert(gseen == Indicator.Seen); 902 | ga0.See("Françoise", InvokedBy.LongName); 903 | assert(girl == Girls.Françoise); 904 | 905 | try { 906 | ga0.See("Jean-Paul", InvokedBy.LongName); 907 | assert(false); 908 | } 909 | catch (ParseException x) 910 | assert(x.msg == "'Jean-Paul' is not a permitted value for the --name option; permitted values are Zoë, Françoise, Beyoncé", "Message was: " ~ x.msg); 911 | 912 | // EOL defaults: 913 | auto ga1 = new GirlArg("name", &girl, &gseen, Girls.init); 914 | ga1.EolDefault(Girls.Françoise); 915 | ga1.SetFArgToDefault; 916 | assert(girl == Girls.init); 917 | assert(gseen == Indicator.NotSeen); 918 | 919 | ga1.SeeEolDefault; 920 | assert(girl == Girls.Françoise); 921 | assert(gseen == Indicator.UsedEolDefault); 922 | 923 | // Can't have an EOL default and an equals default for the same arg: 924 | assertThrown!AssertError(ga1.SetEqualsDefault(Girls.Zoë)); 925 | 926 | // Equals defaults: 927 | auto ga2 = new GirlArg("name", &girl, &gseen, Girls.init); 928 | ga2.EqualsDefault(Girls.Françoise); 929 | ga1.SetFArgToDefault; 930 | assert(girl == Girls.init); 931 | assert(gseen == Indicator.NotSeen); 932 | 933 | ga2.SeeEqualsDefault; 934 | assert(girl == Girls.Françoise); 935 | assert(gseen == Indicator.UsedEqualsDefault); 936 | 937 | // Can't have an EOL default and an equals default for the same arg: 938 | assertThrown!AssertError(ga2.SetEolDefault(Girls.Zoë)); 939 | } 940 | 941 | class IncrementalFArg(Num): HasReceiver!Num if (isIntegral!Num) { 942 | public: 943 | this(in string name, Num *pr) { 944 | super(name, false, null, pr, Num.init); 945 | MarkIncremental; 946 | } 947 | 948 | override void See() { 949 | ++*p_receiver; 950 | } 951 | 952 | override Num Parse(const char[], const InvokedBy) { 953 | return Num.init; 954 | } 955 | } 956 | 957 | // strings: 958 | 959 | // struct Regex won't tolerate being instantiated with a const or immutable 960 | // character type. Given an arbitrary string type, we must therefore find the 961 | // element type and strip it of `const' and `immutable'. 962 | 963 | template BareCharType(Str) { 964 | // Find the qualified element type: 965 | template ElementType(Str: Chr[], Chr) { 966 | alias ElementType = Chr; 967 | } 968 | 969 | // Result: element type without qualifiers: 970 | alias BareCharType = Unqual!(ElementType!Str); 971 | } 972 | 973 | // This class holds one regex and error message, which will be applied to an 974 | // actual string argument at runtime. 975 | 976 | class ArgRegex(Char) { 977 | private: 978 | alias Caps = Captures!(Char[]); 979 | alias AllCaps = Caps[]; 980 | alias Str = const Char[]; 981 | 982 | Regex!Char rx; 983 | string error_msg; 984 | bool snip; 985 | 986 | static Interpolate(in string msg, AllCaps *p_allcaps) { 987 | if (!p_allcaps) 988 | return msg; 989 | 990 | // Receives a string of the form "{2:PORT}". 991 | // Returns p_allcaps[2]["PORT"]. 992 | auto look_up_cap(Captures!string caps) { 993 | immutable rx_no = caps[1].to!uint; 994 | assert(rx_no < p_allcaps.length, "The format {" ~ caps[1] ~ ':' ~ caps[2] ~ "} refers to too high a regex number"); 995 | auto old_caps = (*p_allcaps)[rx_no]; 996 | const cap_name = caps[2]; 997 | return old_caps[cap_name]; 998 | } 999 | 1000 | static rx = ctRegex!(`\{ (\d+) : (\S+?) \}`, "x"); 1001 | return replaceAll!look_up_cap(msg, rx); 1002 | } 1003 | 1004 | public: 1005 | this(in Str rxtext, in string flags, in string err) { 1006 | rx = regex(rxtext, flags); 1007 | error_msg = err; 1008 | } 1009 | 1010 | auto MakeSnip() { 1011 | assert(!snip, "Duplicate call to Snip()"); 1012 | snip = true; 1013 | } 1014 | 1015 | auto FailsToMatch(ref Char[] aarg, AllCaps *p_allcaps) { 1016 | auto caps = aarg.matchFirst(rx); 1017 | if (!caps) 1018 | return Interpolate(error_msg, p_allcaps); 1019 | 1020 | if (p_allcaps) 1021 | *p_allcaps ~= caps; 1022 | 1023 | if (snip) 1024 | aarg = caps.post; 1025 | 1026 | return null; 1027 | } 1028 | } 1029 | 1030 | /++ 1031 | + By calling Pos() or Named() with string member variable, your program 1032 | + creates an instance of (some template specialisation of) class 1033 | + StringFArg. This class enables you to set the minimum and maximum lengths 1034 | + of the input your program will accept. 1035 | +/ 1036 | 1037 | class StringFArg(Str): HasReceiver!Str if (isSomeString!Str) { 1038 | private: 1039 | alias Char = BareCharType!Str; 1040 | alias ARx = ArgRegex!Char; 1041 | alias Caps = Captures!(Char[]); 1042 | alias AllCaps = Caps[]; 1043 | 1044 | size_t min_len = 0; 1045 | size_t max_len = size_t.max; 1046 | ARx[] regexen; 1047 | AllCaps *p_allcaps; 1048 | 1049 | public: 1050 | mixin CanSetSpecialDefaults; 1051 | 1052 | this(in string name, Str *p_receiver, Indicator *p_indicator, in Str dfault) { 1053 | super(name, true, p_indicator, p_receiver, dfault.to!Str); 1054 | } 1055 | 1056 | /++ 1057 | + Sets the minimum and maximum length of the input, in characters, that 1058 | + your program will accept for this argument. You can call 1059 | + SetMinimumLength and SetMaximumLength in either order, but the code 1060 | + asserts that the maximum is no smaller than the minimum. By default, no 1061 | + length restriction is applied. 1062 | +/ 1063 | 1064 | final SetMinimumLength(in size_t min) 1065 | in { 1066 | assert(min <= max_len); 1067 | } 1068 | body { 1069 | min_len = min; 1070 | return this; 1071 | } 1072 | 1073 | /// ditto 1074 | 1075 | final SetMaximumLength(in size_t max) 1076 | in { 1077 | assert(max >= min_len); 1078 | } 1079 | body { 1080 | max_len = max; 1081 | return this; 1082 | } 1083 | 1084 | /// Sets both the minimum and the maximum length in a single operation. 1085 | 1086 | final LimitLength(in size_t min, in size_t max) 1087 | in { 1088 | assert(max >= min); 1089 | } 1090 | body { 1091 | min_len = min; 1092 | max_len = max; 1093 | return this; 1094 | } 1095 | 1096 | /++ 1097 | + You can apply one or more regexes to the user's input. These regexes are 1098 | + applied in order; if any regex doesn't match, the associated error 1099 | + message is displayed and Argon throws a ParseException. 1100 | + 1101 | + A typical user won't understand regexes or a message saying that a regex 1102 | + doesn't match, so use several regexes, each more specific than the last 1103 | + or looking further into the string than the last, and provide error 1104 | + messages in plain language. See the sample code below. 1105 | +/ 1106 | 1107 | auto AddRegex(in Str regex_code, in string regex_flags, in string error_message) { 1108 | regexen ~= new ARx(regex_code, regex_flags, error_message); 1109 | return this; 1110 | } 1111 | 1112 | /++ 1113 | + When you have validated the early part of the string and want to move on 1114 | + to the next part, you can call Snip(), which makes subsequent regexes 1115 | + see only the part of the input that follows the most recent match. 1116 | + Snipping avoids the need to keep rematching the early part of the string 1117 | + once you've proved that it's valid. 1118 | +/ 1119 | 1120 | auto Snip() { 1121 | assert(!regexen.empty, "You must call AddRegex() before calling Snip()"); 1122 | regexen[$-1].MakeSnip; 1123 | return this; 1124 | } 1125 | 1126 | /++ 1127 | + You can use named captures in your regexes and store the results in an 1128 | + an array. Each element of this array stores all the named and numbered 1129 | + captures from a single regex. If you store captures, a later error 1130 | + message can refer back to an earlier successful match: for example, in 1131 | + this code sample, `{0:PORT}` refers to the named `PORT` capture in the 1132 | + zeroth successful match. 1133 | + ---- 1134 | + class MyHandler: argon.Handler { 1135 | + string port; 1136 | + Captures!(char[])[] port_captures; 1137 | + 1138 | + this() { 1139 | + Named("port-name", port, "") // Ethernet or aggregate port name: / ^ (?: eth | agg ) \d{1,3} $ /x 1140 | + .AddRegex(` ^ (?P eth | agg ) (?! \p{alphabetic} ) `, "x", "The port name must begin with 'eth' or 'agg'") .Snip 1141 | + .AddRegex(` ^ (?P \d{1,3} ) (?! \d ) `, "x", "The port type ('{0:TYPE}') must be followed by one, two or three digits").Snip 1142 | + .AddRegex(` ^ $ `, "x", "The port name ('{0:TYPE}{1:NUMBER}') mustn't be followed by any other characters") 1143 | + .StoreCaptures(port_captures); 1144 | + // ... 1145 | + } 1146 | + 1147 | + auto Run(immutable(string)[] args) { 1148 | + Parse(args); 1149 | + } 1150 | + } 1151 | + ---- 1152 | + The regexes are matched in order. If the user's input doesn't start 1153 | + with `eth` or `agg`, the first error message is displayed. If the input 1154 | + starts with `eth` or `agg` but doesn't contain any numbers, the second 1155 | + message is displayed -- but the text `{0:TYPE}` is replaced with whatever 1156 | + the first regex captured. 1157 | + 1158 | + If the user provides a valid port name of, say, `agg4` then, after the 1159 | + parse, all the following will be true: 1160 | + ---- 1161 | + port_name == "agg4" 1162 | + port_captures[0]["TYPE"] == "agg" 1163 | + port_captures[1]["NUMBER"].to!uint == 4 1164 | + ---- 1165 | + If the user supplies invalid input, Argon throws a ParseException and the 1166 | + values of `port_name` and `port_captures` are undefined. 1167 | +/ 1168 | 1169 | @trusted auto StoreCaptures(ref AllCaps ac) { 1170 | // @trusted because we take the address of ac 1171 | p_allcaps = ∾ 1172 | return this; 1173 | } 1174 | 1175 | protected: 1176 | final override Str Parse(in char[] aarg, in InvokedBy) { 1177 | return aarg.to!Str; 1178 | } 1179 | 1180 | final override string ViolatesConstraints(in Str str, in InvokedBy invocation) { 1181 | const len = str.length; 1182 | if (len < min_len) 1183 | return text("The argument to ", DescribeArgumentForError(invocation), " must be at least ", min_len, " characters long"); 1184 | else if (len > max_len) 1185 | return text("The argument to ", DescribeArgumentForError(invocation), " must be at most ", max_len, " characters long"); 1186 | 1187 | if (p_allcaps) 1188 | p_allcaps.length = 0; 1189 | 1190 | if (!regexen.empty) { 1191 | auto mutable_str = str.to!(Char[]); 1192 | foreach (rx; regexen) 1193 | if (auto error_msg = rx.FailsToMatch(mutable_str, p_allcaps)) 1194 | return error_msg; 1195 | } 1196 | 1197 | return null; 1198 | } 1199 | } 1200 | 1201 | @system unittest { 1202 | alias StrFArg = StringFArg!(char[]); 1203 | char[] receiver, dfault; 1204 | Indicator indicator; 1205 | with (new StrFArg("fred", &receiver, &indicator, dfault)) { 1206 | assert(indicator == Indicator.NotSeen); 1207 | 1208 | See("", InvokedBy.LongName); 1209 | assert(indicator == Indicator.Seen); 1210 | assert(receiver == ""); 1211 | 1212 | See("Extrinsic", InvokedBy.LongName); 1213 | assert(receiver == "Extrinsic"); 1214 | 1215 | See("café", InvokedBy.LongName); 1216 | assert(receiver == "café"); 1217 | } 1218 | 1219 | // Regular expressions: 1220 | void test_regexen(Char, Str) () { 1221 | Captures!(Char[])[] captures; 1222 | Str receiver; 1223 | 1224 | with (new StringFArg!Str("fred", &receiver, &indicator, "".to!Str) 1225 | .AddRegex(` ^ (?P eth | agg ) (?! \p{alphabetic} )`, "x", "The port name must start with 'eth' or 'agg'") 1226 | .AddRegex(` ^ (?P eth | agg ) `, "x", "(Checking that the string doesn't change when we don't snip)") .Snip 1227 | .AddRegex(` ^ (?P \d{1,3} ) (?! \d )`, "x", "The port type ('{0:TYPE}') must be followed by one, two or three digits").Snip 1228 | .AddRegex(`^$`, "x", "The port name must contain only 'eth' or 'agg' followed by one, two or three digits") 1229 | .StoreCaptures(captures)) { 1230 | 1231 | immutable invocation = InvokedBy.ShortName; 1232 | assert(ViolatesConstraints("asdfg", invocation) == "The port name must start with 'eth' or 'agg'"); 1233 | assert(ViolatesConstraints("aggie", invocation) == "The port name must start with 'eth' or 'agg'"); 1234 | assert(ViolatesConstraints("eth", invocation) == "The port type ('eth') must be followed by one, two or three digits"); 1235 | assert(ViolatesConstraints("eth4100", invocation) == "The port type ('eth') must be followed by one, two or three digits"); 1236 | assert(ViolatesConstraints("eth410?", invocation) == "The port name must contain only 'eth' or 'agg' followed by one, two or three digits"); 1237 | 1238 | assert(ViolatesConstraints("agg291", invocation) is null); 1239 | assert(captures.length == 4); 1240 | assert(captures[0]["TYPE"] == "agg"); 1241 | assert(captures[1]["TYPE"] == "agg"); 1242 | assert(captures[2]["NUMBER"].to!uint == 291); 1243 | } 1244 | } 1245 | 1246 | test_regexen!(char, string) (); 1247 | test_regexen!(wchar, wstring) (); 1248 | test_regexen!(dchar, dstring) (); 1249 | } 1250 | 1251 | // An argument representing a file that needs to be opened. 1252 | // 1253 | // The caller wants to express a default as a string, not as a ready-opened 1254 | // file. Therefore, this class can't inherit from HasReceiver!File, because 1255 | // that would provide functionality relating to a default File, not a default 1256 | // filename. So it chooses to inherit directly from HasReceiver's base and 1257 | // to reimplement the bits of HasReceiver that it needs. 1258 | 1259 | /++ 1260 | + Calling Named() or Pos() with a File argument creates a FileFArg. 1261 | +/ 1262 | 1263 | class FileFArg: FArgBase { 1264 | private: 1265 | const string open_mode; 1266 | string filename, dfault, special_default; 1267 | string *p_error; 1268 | File *p_receiver; 1269 | 1270 | auto OpenOrThrow(in string name) { 1271 | *p_receiver = File(name, open_mode); 1272 | } 1273 | 1274 | auto OpenRobustly(in string name) { 1275 | *p_error = ""; 1276 | try 1277 | OpenOrThrow(name); 1278 | catch (Exception x) { 1279 | *p_error = x.msg; 1280 | *p_receiver = File(); 1281 | } 1282 | } 1283 | 1284 | public: 1285 | this(in string option_name, File *pr, Indicator *p_indicator, in string mode, string *pe, in string df) { 1286 | super(option_name, true, p_indicator); 1287 | open_mode = mode; 1288 | dfault = df; 1289 | p_error = pe; 1290 | p_receiver = pr; 1291 | } 1292 | 1293 | /++ 1294 | + Sets the default valuefor use if a named argument appears at the end of 1295 | + the command line without an attached value. Must not be empty. 1296 | + 1297 | + An end-of-line default of "-" is useful for producing optional output 1298 | + that goes to stdout unless the user specifies an alternative destination. 1299 | + For example: 1300 | + ---- 1301 | + class MyHandler: argon.Handler { 1302 | + File file; 1303 | + argon.Indicator opened_file; 1304 | + 1305 | + this() { 1306 | + Named("list", file, "wb", opened_file, null).EolDefault("-"); 1307 | + } 1308 | + 1309 | + // ... 1310 | + } 1311 | + ---- 1312 | + Suppose your program is called `foo`. 1313 | + $(UL $(LI If the user runs `foo` with no 1314 | + command line arguments, no file is opened and `opened_file` equals 1315 | + `Indicator.NotSeen`.) 1316 | + $(LI If the user runs `foo --list` then `file` will be a copy of `stdout` 1317 | + and `opened_file` will equal `Indicator.UsedEolDefault`.) 1318 | + $(LI If the user runs `foo --list saved.txt` then `file` will have an 1319 | + open handle to `saved.txt` and `opened_file` will equal `Indicator.Seen`. 1320 | + If `saved.txt` can't be opened, Argon will propagate the exception 1321 | + thrown by `struct File`.)) 1322 | +/ 1323 | 1324 | auto EolDefault(in string ed) { 1325 | assert(!ed.empty, "The end-of-line default filename can't be empty"); 1326 | assert(IsNamed, "Only a named option can have an end-of-line default; for a positional argument, use an ordinary default"); 1327 | assert(!HasEqualsDefault, "No argument can have both an equals default and and end-of-line default"); 1328 | special_default = ed; 1329 | MarkEolDefault; 1330 | return this; 1331 | } 1332 | 1333 | auto EqualsDefault(in string ed) { 1334 | assert(!ed.empty, "The equals default filename can't be empty"); 1335 | assert(IsNamed, "Only a named option can have an equals default; for a positional argument, use an ordinary default"); 1336 | assert(!HasEolDefault, "No argument can have both an equals default and and end-of-line default"); 1337 | special_default = ed; 1338 | MarkEqualsDefault; 1339 | return this; 1340 | } 1341 | 1342 | final override void See(in string aarg, in InvokedBy invocation) { 1343 | if (aarg.empty) 1344 | throw new ParseException("An empty filename isn't permitted for ", DescribeArgumentForError(invocation)); 1345 | 1346 | filename = aarg; 1347 | MarkSeen; 1348 | } 1349 | 1350 | final override void SeeEolDefault() { 1351 | filename = special_default; 1352 | MarkSeenWithEolDefault; 1353 | } 1354 | 1355 | final override void SeeEqualsDefault() { 1356 | filename = special_default; 1357 | MarkSeenWithEqualsDefault; 1358 | } 1359 | 1360 | final override void SetFArgToDefault() { 1361 | filename = dfault; 1362 | MarkUnseen; 1363 | } 1364 | 1365 | // This method is called after all aargs have been seen. This is the point 1366 | // at which the FileFArg can open either the file specified by the user or 1367 | // the default file specified by the caller, as appropriate. 1368 | 1369 | @trusted final override void Transform() { 1370 | // This method has to be @trusted because otherwise: 1371 | // Error: safe function 'Transform' cannot access __gshared data 'stdin' 1372 | // As elsewhere, I'm open to debate about whether I've given away safety 1373 | // too easily here. One alternative to explore would be something like 1374 | // my_file.fdopen(STDOUT_FILENO), but that would require importing 1375 | // unistd.d and wouldn't even compile beyond the home fires of Posix. 1376 | // Besides, buffering would get in the way if we had both the real 1377 | // stdout and a private File object, both writing to the same FD from 1378 | // different threads. 1379 | 1380 | immutable name = HasBeenSeen? filename: dfault; 1381 | if (name.empty) { 1382 | *p_receiver = File(); 1383 | return; 1384 | } 1385 | 1386 | if (name == "-") { 1387 | if (open_mode.startsWith('r')) { 1388 | *p_receiver = stdin; 1389 | return; 1390 | } 1391 | 1392 | if (open_mode.startsWith('w')) { 1393 | *p_receiver = stdout; 1394 | return; 1395 | } 1396 | 1397 | // If the mode is something we don't recognise it, treat the "-" 1398 | // filename non-magically, so that struct File can throw an 1399 | // exception for the mode string if it's really bogus. This 1400 | // decision also sidesteps the question of what opening stdout for 1401 | // appending might mean. 1402 | } 1403 | 1404 | if (p_error) 1405 | OpenRobustly(name); 1406 | else 1407 | OpenOrThrow(name); 1408 | } 1409 | } 1410 | 1411 | @system unittest { 1412 | import std.file; 1413 | alias InvokedBy = FArgBase.InvokedBy; 1414 | 1415 | auto test_fragile_success(in string filename) { 1416 | File file; 1417 | assert(!file.isOpen); 1418 | auto fa = new FileFArg("file", &file, null, "rb", null, ""); 1419 | fa.See(filename, InvokedBy.LongName); 1420 | fa.Transform; 1421 | assert(file.isOpen); 1422 | } 1423 | 1424 | auto test_robust_success(in string filename) { 1425 | File file; 1426 | assert(!file.isOpen); 1427 | string error_msg; 1428 | auto fa = new FileFArg("file", &file, null, "rb", &error_msg, ""); 1429 | fa.See(filename, InvokedBy.LongName); 1430 | fa.Transform; 1431 | assert(file.isOpen); 1432 | assert(error_msg.empty); 1433 | } 1434 | 1435 | auto test_success(in string filename) { 1436 | assert(filename.exists, "unittest block has assumed that file " ~ filename ~ " exists; it doesn't. The bug is in the unittest block, rather than in the code under test."); 1437 | test_fragile_success(filename); 1438 | test_robust_success( filename); 1439 | } 1440 | 1441 | auto test_fragile_failure(in string filename) { 1442 | File file; 1443 | assert(!file.isOpen); 1444 | auto fa = new FileFArg("file", &file, null, "rb", null, ""); 1445 | try { 1446 | fa.See(filename, InvokedBy.LongName); 1447 | fa.Transform; 1448 | assert(false, "FileFArg should have failed when trying to open nonexistent file " ~ filename ~ " for input"); 1449 | } 1450 | catch (Throwable) { } 1451 | 1452 | assert(!file.isOpen); 1453 | } 1454 | 1455 | auto test_robust_failure(in string filename) { 1456 | File file; 1457 | assert(!file.isOpen); 1458 | string error_msg; 1459 | auto fa = new FileFArg("file", &file, null, "rb", &error_msg, ""); 1460 | fa.See(filename, InvokedBy.LongName); 1461 | fa.Transform; 1462 | assert(!file.isOpen); 1463 | assert(!error_msg.empty); 1464 | } 1465 | 1466 | auto test_failure(in string filename) { 1467 | assert(!filename.exists, "unittest block has assumed that file " ~ filename ~ " doesn't exists; but it does. The bug is in the unittest block, rather than in the code under test."); 1468 | test_fragile_failure(filename); 1469 | test_robust_failure( filename); 1470 | } 1471 | 1472 | if (!existent_file.empty) 1473 | test_success(existent_file); 1474 | 1475 | if (!nonexistent_file.empty) 1476 | test_failure(nonexistent_file); 1477 | 1478 | // Remaining tests can be carried out on all platforms, because everyone 1479 | // supports stdin and stdout or can be made to look convincingly like it. 1480 | 1481 | auto test_std_in_or_out(in string mode, ref File expected_file) { 1482 | File file; 1483 | auto fa = new FileFArg("file", &file, null, mode, null, ""); 1484 | fa.See("-", InvokedBy.LongName); 1485 | fa.Transform; 1486 | assert(file == expected_file); 1487 | } 1488 | 1489 | test_std_in_or_out("r", stdin); 1490 | test_std_in_or_out("w", stdout); 1491 | 1492 | // EOL default; 1493 | File file0; 1494 | auto fa0 = new FileFArg("file", &file0, null, "r", null, ""); 1495 | assert(!fa0.HasEolDefault); 1496 | assert(!fa0.HasEqualsDefault); 1497 | assert(!file0.isOpen); 1498 | fa0.EolDefault("-"); 1499 | assert(fa0.HasEolDefault); 1500 | assert(!fa0.HasEqualsDefault); 1501 | assert(!file0.isOpen); 1502 | fa0.SeeEolDefault; 1503 | fa0.Transform; 1504 | assert(file0.isOpen); 1505 | assert(file0 == stdin); 1506 | 1507 | // A single arg can't have special defaults of both kinds: 1508 | assertThrown!AssertError(fa0.EqualsDefault("-")); 1509 | 1510 | // Equals default: 1511 | File file1; 1512 | auto fa1 = new FileFArg("file", &file1, null, "r", null, ""); 1513 | assert(!fa1.HasEolDefault); 1514 | assert(!fa1.HasEqualsDefault); 1515 | assert(!file1.isOpen); 1516 | fa1.EqualsDefault("-"); 1517 | assert(!fa1.HasEolDefault); 1518 | assert(fa1.HasEqualsDefault); 1519 | assert(!file1.isOpen); 1520 | fa1.SeeEqualsDefault; 1521 | fa1.Transform; 1522 | assert(file1.isOpen); 1523 | assert(file1 == stdin); 1524 | 1525 | // A single arg can't have special defaults of both kinds: 1526 | assertThrown!AssertError(fa1.EolDefault("-")); 1527 | } 1528 | 1529 | // An argument group is an object that restricts the combinations of arguments 1530 | // that a user can specify, and fails the parse if its restriction isn't met. 1531 | // 1532 | // This is a base class for all argument groups: 1533 | 1534 | class ArgGroup { 1535 | protected: 1536 | static auto AssertAllOptional(const FArgBase[] args) { 1537 | assert(!args.any!(arg => arg.IsMandatory), "It doesn't make sense to place a mandatory argument into an arg group"); 1538 | } 1539 | 1540 | static auto Describe(const (FArgBase)[] args) { 1541 | string result; 1542 | while (!args.empty) { 1543 | if (!result.empty) 1544 | result ~= args.length == 1? " and ": ", "; 1545 | result ~= args[0].DescribeArgumentOptimallyForError; 1546 | args = args[1..$]; 1547 | } 1548 | 1549 | return result; 1550 | } 1551 | 1552 | abstract public void Check() const; 1553 | } 1554 | 1555 | // This group restricts the number of arguments that can be specified: 1556 | 1557 | class CountingArgGroup: ArgGroup { 1558 | private: 1559 | immutable uint min_count, max_count; 1560 | const (FArgBase)[] fargs; 1561 | 1562 | auto NrSeen() const { 1563 | return cast(uint) fargs.count!(arg => arg.HasBeenSeen); 1564 | } 1565 | 1566 | auto RejectArgCount() const { 1567 | if (min_count == max_count) 1568 | throw new ParseException("Please specify exactly ", min_count, " of ", Describe(fargs)); 1569 | else if (min_count == 0) 1570 | throw new ParseException("Please don't specify more than ", max_count, " of ", Describe(fargs)); 1571 | else 1572 | throw new ParseException("Please specify between ", min_count, " and ", max_count, " of ", Describe(fargs)); 1573 | } 1574 | 1575 | public: 1576 | this(FArgBases...) (in uint min, in uint max, in FArgBase first, in FArgBase second, in FArgBases other_args) { 1577 | assert(min <= max); 1578 | 1579 | min_count = min; 1580 | max_count = max; 1581 | fargs = [first, second]; 1582 | foreach (arg; other_args) 1583 | fargs ~= arg; 1584 | 1585 | AssertAllOptional(fargs); 1586 | 1587 | if (min == 0) { 1588 | assert(max > 0, "The user must be allowed to supply at least one argument"); 1589 | assert(max < fargs.length, "This group does nothing; please specify a non-zero minimum or a lower maximum"); 1590 | } 1591 | else 1592 | assert(max <= fargs.length, "The maximum count is higher than the number of formal arguments you've specified"); 1593 | } 1594 | 1595 | override void Check() const { 1596 | immutable nr_seen = NrSeen; 1597 | if (nr_seen < min_count || nr_seen > max_count) 1598 | RejectArgCount; 1599 | } 1600 | } 1601 | 1602 | // This group demands that the first argument be supplied if any others are 1603 | // supplied: 1604 | 1605 | class FirstOrNoneGroup: ArgGroup { 1606 | private: 1607 | const FArgBase head; 1608 | const (FArgBase)[] tail; 1609 | 1610 | public: 1611 | this(FArgBases...) (in FArgBase hd, in FArgBase tail1, in FArgBases more_tail) { 1612 | head = hd; 1613 | tail = [tail1]; 1614 | foreach (arg; more_tail) 1615 | tail ~= arg; 1616 | 1617 | assert(!head.IsMandatory, "If the first argument in this arg group is mandatory, the arg group will do nothing"); 1618 | AssertAllOptional(tail); 1619 | } 1620 | 1621 | override void Check() const { 1622 | if (!head.HasBeenSeen) 1623 | foreach (arg; tail) 1624 | if (arg.HasBeenSeen) 1625 | throw new ParseException("Please don't specify ", arg .DescribeArgumentOptimallyForError, 1626 | " without also specifying ", head.DescribeArgumentOptimallyForError); 1627 | } 1628 | } 1629 | 1630 | // This argument group insists that all the specified args be supplied if any of 1631 | // them are supplied. 1632 | 1633 | class AllOrNoneGroup: ArgGroup { 1634 | private: 1635 | const (FArgBase)[] fargs; 1636 | 1637 | auto IsViolated() const { 1638 | bool[2] found; 1639 | 1640 | foreach (arg; fargs) { 1641 | found[arg.HasBeenSeen] = true; 1642 | if (found[false] + found[true] == 2) 1643 | return true; 1644 | } 1645 | 1646 | return false; 1647 | } 1648 | 1649 | auto FailTheParse() const { 1650 | immutable selection = fargs.length == 2? "both or neither": "all or none"; 1651 | throw new ParseException("Please specify either ", selection, " of ", Describe(fargs)); 1652 | } 1653 | 1654 | public: 1655 | this(FArgBases...) (in FArgBase first, in FArgBase second, in FArgBases more_args) { 1656 | fargs = [first, second]; 1657 | foreach (arg; more_args) 1658 | fargs ~= arg; 1659 | 1660 | AssertAllOptional(fargs); 1661 | } 1662 | 1663 | override void Check() const { 1664 | if (IsViolated) 1665 | FailTheParse; 1666 | } 1667 | } 1668 | 1669 | /++ 1670 | + It's customary for users to specify options (with single or double dashes) 1671 | + before positional arguments (which don't start with dashes). Your program 1672 | + can control what happens if a user specifies an option after a positional 1673 | + argument. 1674 | + 1675 | + In time-honoured fashion, a command-line token consisting of a lone double 1676 | + dash forces Argon to treat all following tokens as positional arguments, 1677 | + even if they start with dashes. Your program can't override this behaviour. 1678 | +/ 1679 | 1680 | enum OnOptionAfterData { 1681 | AssumeOption, /// Treat it as an option, as Gnu's `ls(1)` does. This is the default behaviour. 1682 | AssumeData, /// Treat it as a positional argument and fail if it can't be assigned. 1683 | Fail /// Throw a syntax-error exception. 1684 | } 1685 | 1686 | /++ 1687 | + Most programs allow single-letter options to be bundled together, so that 1688 | + `-x -y -z 2` (where `2` is an argument to the `-z` option) can be typed more 1689 | + conveniently as `-xyz2`. Your program can disable bundling, as some authors 1690 | + prefer to, but users still won't gain the ability to specify long names with 1691 | + a single dash, and so there's little to be said for doing so. 1692 | +/ 1693 | 1694 | enum CanBundle { 1695 | No, /// Insist on `-x -y -z2`. 1696 | Yes /// Allow `-xyz2`. This is the default behaviour. 1697 | } 1698 | 1699 | /++ 1700 | + If your program uses Argon to handle options but then accepts an arbitrarily 1701 | + long list of, say, filenames, you'll want Argon to hand back any tokens typed 1702 | + by the user that can't be matched to the arguments in your code. In most 1703 | + cases, though, you'll want parsing to fail if the user types in unrecognised 1704 | + arguments. 1705 | + 1706 | + Because Argon offers first-class treatment of positional parameters -- those 1707 | + that aren't preceded by a `--switch-name` -- most programs never need to 1708 | + inspect `argv` and should accept the default behaviour. 1709 | + 1710 | + Note that unrecognised tokens resembling options -- i.e. those starting with 1711 | + a dash, unless a double-dash end-of-options token has appeared earlier -- 1712 | + will always fail the parse and can never be passed back to the caller. Only 1713 | + tokens resembling positional arguments can be passed back. 1714 | +/ 1715 | 1716 | enum PassBackUnusedAArgs { // That spelling is deliberate 1717 | No, /// Fail the parse if the user supplies unexpected arguments. This is the default behaviour. 1718 | Yes /// Pass any unexpected positional arguments back to the caller, after removing any that Argon managed to process. 1719 | } 1720 | 1721 | /++ 1722 | + class Handler provides methods enabling you to specify the arguments that 1723 | + your program will accept from the user, and it parses them at runtime. You 1724 | + can use it in one of two ways: 1725 | + 1726 | + $(UL $(LI You can write a class that inherits from Handler and calls its 1727 | + methods directly, as in the Synopsis above;) 1728 | + $(LI You can create a bare Handler directly and call its methods.)) 1729 | + 1730 | + The first is preferable, because it takes care of variable lifetimes for you: 1731 | + if your derived class calls Pos(), Named() and Incremental(), passing 1732 | + references to its own data members, then there's no danger of calling Parse() 1733 | + after those data members have gone away. 1734 | + 1735 | + After calling Pos(), Named() and Incremental() once for each argument that 1736 | + it wishes to accept, your program can apply non-default values of 1737 | + OnOptionAfterData and CanBundle (if you don't like the recommended defaults). 1738 | + It then calls Parse(). 1739 | +/ 1740 | 1741 | // A command handler sets up FArgs and parses lists of AArgs: 1742 | 1743 | class Handler { 1744 | private: 1745 | FArgBase[] fargs; 1746 | OnOptionAfterData opad; 1747 | CanBundle can_bundle = CanBundle.Yes; 1748 | PassBackUnusedAArgs pass_back; 1749 | ArgGroup[] arg_groups; 1750 | bool preserve; 1751 | 1752 | // Determine which FArg class to use for a given receiver type. Ignore 1753 | // BoolFArg, because that doesn't take an AArg and is handled separately. 1754 | template FindFargType(T) { 1755 | static if (is(T == enum)) 1756 | alias FindFargType = EnumeralFArg!T; 1757 | else static if (isIntegral!T) 1758 | alias FindFargType = IntegralFArg!T; 1759 | else static if (isFloatingPoint!T) 1760 | alias FindFargType = FloatingFArg!T; 1761 | else static if (isSomeString!T) 1762 | alias FindFargType = StringFArg!T; 1763 | else 1764 | static assert(false); // The caller passed a bad receiver type 1765 | } 1766 | 1767 | // Mark the most recently-added FArg as positional: 1768 | auto MarkPositional() 1769 | in { 1770 | assert(!fargs.empty); 1771 | assert(!fargs[$-1].IsPositional); 1772 | } 1773 | body { 1774 | fargs[$-1].MarkPositional; 1775 | } 1776 | 1777 | // In most applications, once command-line arguments have been parsed, this 1778 | // object's state can safely be discarded. 1779 | 1780 | auto FreeMemory() { 1781 | if (!preserve) 1782 | arg_groups.length = fargs.length = 0; 1783 | } 1784 | 1785 | // Assert that the caller hasn't tried to set up a mandatory positional arg 1786 | // after an optional positional arg, because that would require the 1787 | // first (optional) arg to become mandatory in order to satisfy the 1788 | // requirement for the second (mandatory) arg to be specified. 1789 | 1790 | auto AssertNoOptionalPositionalParameters() const { 1791 | assert(! fargs.any!(arg => arg.IsPositional && !arg.IsMandatory), 1792 | "A mandatory positional argument can't follow an optional positional argument on the command line"); 1793 | } 1794 | 1795 | // A positional parameter has only one name, which mustn't include pipe 1796 | // symbols (|). 1797 | 1798 | static AssertNoPipeSymbols(in string name) { 1799 | assert(name.find('|').empty, "A positional parameter has only one name, which mustn't include pipe symbols (|)"); 1800 | } 1801 | 1802 | public: 1803 | /++ 1804 | + Gives your program a named Boolean argument with a default value, which 1805 | + can be true or false. The argument will take the non-default value if 1806 | + the user types in the switch _name at runtime -- unless the user supplies 1807 | + one of the six special syntaxes `--option=no`, `--option=false`, 1808 | +`--option=0`, `--option=yes`, `--option=true` or `--option=1`, in which 1809 | + case the argument will take the value that the user has specified, 1810 | + regardless of the default. Note the equals sign: an explicit Boolean 1811 | + argument, unlike parameters of other types, can't be separated from its 1812 | + option _name by whitespace. The tokens `no`, `false`, `yes` and `true` 1813 | + can be abbreviated. 1814 | + 1815 | + In common with all Named() methods below, this method accepts non-Ascii 1816 | + switch names. 1817 | + 1818 | + The user can use any unambiguous abbreviation of the option _name. If 1819 | + you wish to provide a single-character short _name, call Short() or 1820 | + opCall(char). 1821 | + 1822 | + What are named and positional arguments? Consider the Gnu Linux command 1823 | + `mkfs --verbose -t ext4 /dev/sda2`. This command has one Boolean option 1824 | + (`--verbose`), one string argument (`-t ext4`) and one positional 1825 | + argument (`/dev/sda2`). Unlike Getopt, Argon collects positional 1826 | + argument from `argv`, checks them, converts them and stores them in 1827 | + type-safe variables, just as it does for named arguments. 1828 | + 1829 | + Don't be overwhelmed by the number of overloads of Named() and Pos(). 1830 | + The rules are as follows: 1831 | + $(UL $(LI A Boolean argument is always named and optional, even if you 1832 | + don't specify a default value.) 1833 | + $(LI Any other argument is optional if you provide a default value or an 1834 | + indicator, or mandatory otherwise.) 1835 | + $(LI `File` arguments have separate Named() and Pos() calls so that 1836 | + you can specify an open mode and a failure protocol.)) 1837 | +/ 1838 | 1839 | // Add a named Boolean argument: 1840 | @trusted final Named(Bool) (in string name, ref Bool receiver, in bool dfault) if (is(Bool == bool)) { 1841 | // These methods can't be formally @safe (although they are *safe*) 1842 | // because they appear to take the address of a local variable. They 1843 | // don't really, because `receiver' is a reference, and so taking its 1844 | // address is morally equivalent to passing it in by pointer, but with 1845 | // smoother syntax. Therefore, these methods can be @trusted. 1846 | // 1847 | // I could be argued out of this position, because my use of @trusted 1848 | // seems to give @safe code a way to take a pointer to a local without 1849 | // hitting the tripwire, and there are sound reasons why @safe code 1850 | // doesn't want to do that. If a class passes reference to its own 1851 | // member vars, @trusted won't introduce any new problems, because the 1852 | // only way you could write to the vars concerned after they'd been GCed 1853 | // would be to call Parse() on an object that no longer existed. If 1854 | // you did that, you'd already be in deep trouble. However, guarding 1855 | // against ifs and maybes is just what @safe is for. 1856 | 1857 | auto arg = new BoolFArg(name, &receiver, dfault); 1858 | fargs ~= arg; 1859 | return arg; 1860 | } 1861 | 1862 | /++ 1863 | + Gives your program a named Boolean argument whose default value is 1864 | + `false`. 1865 | +/ 1866 | 1867 | final Named(Bool) (in string name, ref Bool receiver) if (is(Bool == bool)) { 1868 | return Named(name, receiver, bool.init); 1869 | } 1870 | 1871 | /++ 1872 | + Gives your program an optional named argument of any type other than 1873 | + Boolean or File, with a default value of your choice. The user can 1874 | + override this default value using one of two syntaxes: `--size 23` or 1875 | + `--size=23`. Any unambiguous abbreviation of the option _name is 1876 | + acceptable, as in `--si 23` and `--si=23`. If you use one of the 1877 | + methods in FArgCommon to provide a single-character short _name, the user 1878 | + can use additional syntaxes such as `-s 23`, `-s=23` and `-s23`. 1879 | +/ 1880 | 1881 | @trusted final Named(Arg, Dfault) (in string name, ref Arg receiver, in Dfault dfault) if (!is(Arg == bool) && !is(Arg == File)) { 1882 | alias FargType = FindFargType!Arg; 1883 | auto arg = new FargType(name, &receiver, null, dfault); 1884 | fargs ~= arg; 1885 | return arg; 1886 | } 1887 | 1888 | /++ 1889 | + Gives your program an optional named argument of any type other than 1890 | + Boolean or File. Instead of accepting a default value, this overload 1891 | + expects an $(I indicator): a variable that will be set to one of the 1892 | + values in `enum Indicator` after a successful parse, showing you whether 1893 | + the user specified a value or not. 1894 | +/ 1895 | 1896 | @trusted final Named(Arg) (in string name, ref Arg receiver, ref Indicator indicator) if (!is(Arg == bool) && !is(Arg == File)) { 1897 | alias FargType = FindFargType!Arg; 1898 | auto arg = new FargType(name, &receiver, &indicator, Arg.init); 1899 | fargs ~= arg; 1900 | return arg; 1901 | } 1902 | 1903 | /++ 1904 | + Gives your program a mandatory named argument of any type other than 1905 | + Boolean or File. The user is obliged to specify this argument, or Argon 1906 | + will fail the parse and throw a ParseException. Unless your command has 1907 | + a large number of argument and no obvious order in which they should be 1908 | + specified, it's usually better to make mandatory arguments positional: 1909 | + in other words, make users write `cp *.d backup/` rather than 1910 | + `cp --from *.d --to backup/`. To do this, use Pos() instead. 1911 | +/ 1912 | 1913 | @trusted final Named(Arg) (in string name, ref Arg receiver) if (!is(Arg == bool) && !is(Arg == File)) { 1914 | alias FargType = FindFargType!Arg; 1915 | auto arg = new FargType(name, &receiver, null, Arg.init); 1916 | arg.MarkMandatory; 1917 | fargs ~= arg; 1918 | return arg; 1919 | } 1920 | 1921 | /++ 1922 | + Gives your program a mandatory named argument that opens a file. If 1923 | + return_error_msg_can_be_null is null, failure to open the file will 1924 | + propagate the exception thrown by struct File; otherwise, the exception 1925 | + will be caught and the message stored in the pointee string, and an empty 1926 | + string indicates that the file was opened successfully. Open modes are 1927 | + the same as those used by `struct File`. The user can supply the special 1928 | + string `"-"` to mean either `stdin` or `stdout`, depending on the open 1929 | + mode. 1930 | +/ 1931 | 1932 | @trusted final Named(Arg) (in string name, ref Arg receiver, in string open_mode, string *return_error_msg_can_be_null) if (is(Arg == File)) { 1933 | auto arg = new FileFArg(name, &receiver, null, open_mode, return_error_msg_can_be_null, ""); 1934 | arg.MarkMandatory; 1935 | fargs ~= arg; 1936 | return arg; 1937 | } 1938 | 1939 | /++ 1940 | + Gives your program an optional named argument that opens a file. The 1941 | + indicator becomes `Indicator.UsedEolDefault`, 1942 | + `Indicator.UsedEqualsDefault` or `Indicator.Seen` if the 1943 | + user specifies a filename, even if the file can't be opened. 1944 | + 1945 | + If return_error_msg_can_be_null is null, failure to open the file will 1946 | + propagate the exception thrown by `struct File`; otherwise, the exception 1947 | + will be caught and the message stored in the pointee string, and an empty 1948 | + string indicates successful opening. 1949 | +/ 1950 | 1951 | @trusted final Named(Arg) (in string name, ref Arg receiver, in string open_mode, ref Indicator indicator, string *return_error_msg_can_be_null) if (is(Arg == File)) { 1952 | auto arg = new FileFArg(name, &receiver, &indicator, open_mode, null, ""); 1953 | fargs ~= arg; 1954 | return arg; 1955 | } 1956 | 1957 | /++ 1958 | + Gives your program an optional named argument that opens a file. If the 1959 | + user doesn't specify a filename, your default filename is used instead -- 1960 | + one implication being that Argon will always try to open one file or 1961 | + another if you use this overload. 1962 | + 1963 | + If return_error_msg_can_be_null is null, failure to open the file 1964 | + (whether user-specified or default) will propagate the exception thrown 1965 | + by struct File; otherwise, the exception will be caught and the message 1966 | + stored in the pointee string. 1967 | +/ 1968 | 1969 | @trusted final Named(Arg) (in string name, ref Arg receiver, in string open_mode, in string dfault, string *return_error_msg_can_be_null) if (is(Arg == File)) { 1970 | assert(!dfault.empty, "The default filename can't be empty: please use the overload that doesn't specify a default"); 1971 | auto arg = new FileFArg(name, &receiver, null, open_mode, return_error_msg_can_be_null, dfault); 1972 | fargs ~= arg; 1973 | return arg; 1974 | } 1975 | 1976 | /++ 1977 | + Gives your program a mandatory positional argument of any type except 1978 | + Boolean or File. There are no positional or mandatory Boolean 1979 | + arguments. 1980 | + 1981 | + The name you specify here is never typed in by the user: instead, it's 1982 | + displayed in error messages when the argument is missing or the value 1983 | + provided by the user is invalid in some way. It should therefore be a 1984 | + brief description of the argument (such as "widget colour"). It may 1985 | + contain spaces. If displayed, it will be preceded by the word "the", and 1986 | + so your description shouldn't start with "the" or (in most cases) a 1987 | + capital letter. 1988 | +/ 1989 | 1990 | final Pos(Arg) (in string name, ref Arg receiver) if (!is(Arg == bool) && !is(Arg == File)) { 1991 | AssertNoOptionalPositionalParameters; 1992 | AssertNoPipeSymbols(name); 1993 | auto named = Named(name, receiver); 1994 | MarkPositional; 1995 | return named; 1996 | } 1997 | 1998 | /++ 1999 | + Gives your program an optional positional argument of any type except 2000 | + Boolean or File. This overload accepts an $(I indicator): a variable 2001 | + that will be set to one of the values of enum Indicator. 2002 | +/ 2003 | 2004 | final Pos(Arg) (in string name, ref Arg arg, ref Indicator indicator) if (!is(Arg == bool) && !is(Arg == File)) { 2005 | AssertNoPipeSymbols(name); 2006 | auto named = Named(name, arg, indicator); 2007 | MarkPositional; 2008 | return named; 2009 | } 2010 | 2011 | /++ 2012 | + Gives your program an optional positional argument with a default value 2013 | + of your choice and any type except Boolean or File. 2014 | +/ 2015 | 2016 | final Pos(Arg, Dfault) (in string name, ref Arg arg, in Dfault dfault) if (!is(Arg == bool) && !is(Arg == File)) { 2017 | AssertNoPipeSymbols(name); 2018 | auto named = Named(name, arg, dfault); 2019 | MarkPositional; 2020 | return named; 2021 | } 2022 | 2023 | /++ 2024 | + Gives your program a mandatory positional argument that opens a file. If 2025 | + return_error_msg_can_be_null is null, failure to open the file will 2026 | + propagate the exception thrown by struct File; otherwise, the exception 2027 | + will be caught and the message stored in the pointee string, enabling 2028 | + better error messages if your program opens several files. 2029 | +/ 2030 | 2031 | @trusted final Pos(Arg) (in string name, ref Arg receiver, in string open_mode, string *return_error_msg_can_be_null) if (is(Arg == File)) { 2032 | AssertNoPipeSymbols(name); 2033 | AssertNoOptionalPositionalParameters; 2034 | auto named = Named(name, receiver, open_mode, return_error_msg_can_be_null); 2035 | MarkPositional; 2036 | return named; 2037 | } 2038 | 2039 | /++ 2040 | + Gives your program an optional positional argument that opens a file. 2041 | + The indicator becomes `Indicator.UsedEolDefault`, 2042 | + `Indicator.UsedEqualsDefault` or `Indicator.Seen` if 2043 | + the user supplies this option, even if the file can't be opened. If 2044 | + return_error_msg_can_be_null is null, failure to open the file will 2045 | + propagate the exception thrown by `struct File`; otherwise, the exception 2046 | + will be caught and the message stored in the pointee string, leaving it 2047 | + empty if the file was opened successfully. 2048 | +/ 2049 | 2050 | @trusted final Pos(Arg) (in string name, ref Arg receiver, in string open_mode, ref Indicator indicator, string *return_error_msg_can_be_null) if (is(Arg == File)) { 2051 | AssertNoPipeSymbols(name); 2052 | auto named = Named(name, receiver, open_mode, indicator, return_error_msg_can_be_null); 2053 | MarkPositional; 2054 | return named; 2055 | } 2056 | 2057 | /++ 2058 | + Gives your program an optional positional argument that opens a file. If 2059 | + the user doesn't specify a filename, your default filename is used 2060 | + instead. If return_error_msg_can_be_null is null, failure to open the 2061 | + file (whether user-specified or default) will propagate the exception 2062 | + thrown by struct File; otherwise, the exception will be caught and the 2063 | + message stored in the pointee string. 2064 | + 2065 | + By adding an optional, positional File argument as the last argument on 2066 | + your command line and giving it a default value of "-", you can do a 2067 | + simplified version of what cat(1) does and what many Perl programs do: 2068 | + read from a file if a filename is specified, or from stdin otherwise. 2069 | + Argon doesn't currently support the full-blooded version of that 2070 | + functionality, in which the user can specify any number of files at the 2071 | + command line and your program reads from each one in turn. 2072 | +/ 2073 | 2074 | @trusted final Pos(Arg) (in string name, ref Arg receiver, in string open_mode, in string dfault, string *return_error_msg_can_be_null) if (is(Arg == File)) { 2075 | AssertNoPipeSymbols(name); 2076 | auto named = Named(name, receiver, open_mode, dfault, return_error_msg_can_be_null); 2077 | MarkPositional; 2078 | return named; 2079 | } 2080 | 2081 | /++ 2082 | + Gives your program an incremental option, which takes no value on the 2083 | + command line but increments its receiver each time it's specified on the 2084 | + command line, as in `--verbose --verbose --verbose` or `-vvv`. 2085 | +/ 2086 | 2087 | @trusted final Incremental(Arg) (in string name, ref Arg receiver) if (isIntegral!Arg) { 2088 | auto farg = new IncrementalFArg!Arg(name, &receiver); 2089 | fargs ~= farg; 2090 | return farg; 2091 | } 2092 | 2093 | /++ 2094 | + We've already seen that some arguments are mandatory, some are optional 2095 | + and a positional argument can't be specified unless all positional args 2096 | + to its left have also been specified. If you want to impose further 2097 | + restrictions on which combinations of arguments, use one or more argument 2098 | + groups. These come in three flavours: 2099 | + $(UL $(LI Groups that require the user to specify at least $(I N) and 2100 | + not more than $(I M) of a given subset of your program's arguments;) 2101 | + $(LI Groups that specify that one or more arguments can't be supplied 2102 | + unless some other argument is also supplied; and) 2103 | + $(LI Groups that specify that either all of a set of args must be 2104 | + supplied or none must.)) 2105 | + Each of the following functions can accept any number of arguments 2106 | + greater than 1, and each argument can be a member of more than one 2107 | + argument group if you wish. The arguments you pass to these functions 2108 | + are returned by the Named(), Pos() and Incremental() methods. 2109 | + 2110 | + It doesn't make sense to place a mandatory arg into an arg group, and the 2111 | + code will assert that you don't. 2112 | + 2113 | + This _first function sets up an argument group that insists that between 2114 | + $(I N) and $(I M) of the specified arguments be supplied by the user. 2115 | +/ 2116 | 2117 | auto BetweenNAndMOf(FArgBases...) (uint min, uint max, in FArgBase first, in FArgBase second, in FArgBases more_args) { 2118 | arg_groups ~= new CountingArgGroup(min, max, first, second, more_args); 2119 | } 2120 | 2121 | /// Syntactic sugar. 2122 | auto ExactlyNOf(FArgBases...) (uint n, in FArgBase first, in FArgBase second, in FArgBases more_args) { 2123 | BetweenNAndMOf(n, n, first, second, more_args); 2124 | } 2125 | 2126 | /// Syntactic sugar. 2127 | auto ExactlyOneOf(FArgBases...) (in FArgBase first, in FArgBase second, in FArgBases more_args) { 2128 | ExactlyNOf(1, first, second, more_args); 2129 | } 2130 | 2131 | /// Syntactic sugar. 2132 | auto AtMostNOf(FArgBases...) (uint max, in FArgBase first, in FArgBase second, in FArgBases more_args) { 2133 | return BetweenNAndMOf(0, max, first, second, more_args); 2134 | } 2135 | 2136 | /// Syntactic sugar. 2137 | auto AtMostOneOf(FArgBases...) (in FArgBase first, in FArgBase second, in FArgBases more_args) { 2138 | return BetweenNAndMOf(0, 1, first, second, more_args); 2139 | } 2140 | 2141 | /++ 2142 | + Sets up an argment group that refuses to accept any arg in the list 2143 | + unless the first arg is supplied by the user. 2144 | +/ 2145 | 2146 | auto FirstOrNone(FArgBases...) (in FArgBase first, in FArgBase second, in FArgBases more_args) { 2147 | arg_groups ~= new FirstOrNoneGroup(first, second, more_args); 2148 | } 2149 | 2150 | /++ 2151 | + Sets up an argument group that won't accept any of the arguments in the 2152 | + list unless all of them have been supplied by the user. 2153 | +/ 2154 | 2155 | auto AllOrNone(FArgBases...) (in FArgBase first, in FArgBase second, in FArgBases more_args) { 2156 | arg_groups ~= new AllOrNoneGroup(first, second, more_args); 2157 | } 2158 | 2159 | /++ 2160 | + Control what happens if the user specifies an `--option` after a 2161 | + positional parameter: 2162 | +/ 2163 | 2164 | final opCall(in OnOptionAfterData o) { 2165 | opad = o; 2166 | } 2167 | 2168 | /// Control whether your program allows `-xyz2` or insists on `-x -y -z2`: 2169 | 2170 | final opCall(in CanBundle cb) { 2171 | can_bundle = cb; 2172 | } 2173 | 2174 | /++ 2175 | + Control whether any unexpected command-line arguments should be passed 2176 | + back to you or should cause the parse to fail. 2177 | +/ 2178 | 2179 | final opCall(in PassBackUnusedAArgs pb) { 2180 | pass_back = pb; 2181 | } 2182 | 2183 | /++ 2184 | + Parse a command line. If the user's input is correct, populate 2185 | + arguments and indicators. Otherwise, throw a ParseException. 2186 | +/ 2187 | 2188 | final Parse(ref string[] aargs) { 2189 | auto parser = Parser(fargs, opad, can_bundle, pass_back, arg_groups); 2190 | parser.Parse(aargs); 2191 | FreeMemory; 2192 | } 2193 | 2194 | /++ 2195 | + Auto-generate a syntax summary, using names, descriptions and information 2196 | + about whether arguments are mandatory. To reduce clutter, the syntax 2197 | + summary doesn't include short names or default values, both of which 2198 | + should appear in your command's man page. 2199 | + 2200 | + In order to give you maximum control, arguments appear in the summary in 2201 | + the same order as your Named(), Incremental() and Pos() calls: there's no 2202 | + attempt to place named arguments before positional ones, which would be 2203 | + the conventional order for users to adopt. Therefore, if you let Argon 2204 | + generate syntax summaries for you, you should call Named() and 2205 | + Incremental() before Pos() unless you have a good reason not to. It's 2206 | + also good style to place any mandatory named arguments before any 2207 | + optional ones. (With positional arguments, you don't have a choice, 2208 | + because values are always assigned to arguments from left to right.) 2209 | + 2210 | + Generating a syntax summary is affordable but not trivial, so cache the 2211 | + result if you need it more than once. 2212 | +/ 2213 | 2214 | final BuildSyntaxSummary() const { 2215 | return fargs.filter!(farg => farg.IsDocumented) 2216 | .map!(farg => farg.BuildSyntaxElement) 2217 | .join(' '); 2218 | } 2219 | 2220 | /++ 2221 | + Normally, class Handler saves a little memory by deleting its structures 2222 | + after a successful parse. If you want to be able to reuse a Handler -- 2223 | + in a unit test, perhaps -- then calling Preserve() will squelch this 2224 | + behaviour. 2225 | +/ 2226 | 2227 | auto Preserve() { 2228 | preserve = true; 2229 | } 2230 | } 2231 | 2232 | struct Parser { 2233 | private: 2234 | alias InvokedBy = FArgBase.InvokedBy; 2235 | 2236 | FArgBase[] fargs; 2237 | string[] aargs; 2238 | string[] spilt; // AArgs that we don't use ourselves 2239 | immutable OnOptionAfterData opad; 2240 | immutable CanBundle can_bundle; 2241 | immutable PassBackUnusedAArgs pass_back; 2242 | const ArgGroup[] arg_groups; 2243 | bool seen_dbl_dash; 2244 | bool seen_positional; 2245 | 2246 | public: 2247 | this(FArgBase[] f, in OnOptionAfterData o, in CanBundle cb, in PassBackUnusedAArgs pb, in ArgGroup[] ag) { 2248 | fargs = f; 2249 | opad = o; 2250 | can_bundle = cb; 2251 | pass_back = pb; 2252 | arg_groups = ag; 2253 | } 2254 | 2255 | void Parse(ref string[] aa) 2256 | in { 2257 | assert(!aa.empty); 2258 | } 2259 | body { 2260 | seen_dbl_dash = false; 2261 | aargs = aa[1..$]; 2262 | SetFArgsToDefaultValues; 2263 | ParseAllAArgs; 2264 | InsistOnMandatoryAArgs; 2265 | ApplyArgGroups; 2266 | if (pass_back) 2267 | aa = spilt; 2268 | else if (!spilt.empty) { 2269 | immutable any_positional = fargs.any!(farg => farg.IsPositional); 2270 | throw new ParseException("Unexpected text: '", spilt.front, "': ", 2271 | any_positional? "all positional arguments have been used up": "this command has no positional arguments"); 2272 | } 2273 | } 2274 | 2275 | private: 2276 | auto SetFArgsToDefaultValues() { 2277 | foreach (farg; fargs) 2278 | farg.SetFArgToDefault; 2279 | } 2280 | 2281 | auto ParseAllAArgs() { 2282 | while (!aargs.empty) 2283 | ParseNext(); 2284 | } 2285 | 2286 | auto MoveToNextAarg() 2287 | in { 2288 | assert(!fargs.empty); 2289 | } 2290 | body { 2291 | aargs.popFront; 2292 | } 2293 | 2294 | auto OpadAllowsOption() const { 2295 | if (!seen_positional) 2296 | return true; 2297 | else if (opad == OnOptionAfterData.Fail) 2298 | throw new ParseException("No --option is permitted after a positional argument"); 2299 | else 2300 | return opad == OnOptionAfterData.AssumeOption; 2301 | } 2302 | 2303 | auto ParseNext() { 2304 | const aarg = aargs.front; 2305 | if (!seen_dbl_dash) { 2306 | if (aarg.startsWith("--") && OpadAllowsOption) { 2307 | ParseLong; 2308 | return; 2309 | } 2310 | else if (aarg.startsWith("-") && aarg.length > 1 && OpadAllowsOption) { 2311 | ParseShort; 2312 | return; 2313 | } 2314 | } 2315 | 2316 | ParseData; 2317 | } 2318 | 2319 | auto FindUnseenNamedFarg(in string candidate) { 2320 | auto FindFarg() { 2321 | uint nr_results; 2322 | FArgBase result; 2323 | 2324 | // Slight subtlety here: if the user types --col and a given FArg 2325 | // has names --colour and --color, that isn't a conflict; but if 2326 | // two different FArgs have names --colour and --column, that *is* 2327 | // a conflict. 2328 | foreach (farg; fargs) 2329 | if (!farg.IsPositional) { 2330 | const names = farg.GetNames; 2331 | if (!names.find(candidate).empty) 2332 | return farg; 2333 | else if (names.any!(name => name.startsWith(candidate))) { 2334 | ++nr_results; 2335 | result = farg; 2336 | } 2337 | } 2338 | 2339 | switch (nr_results) { 2340 | case 0: 2341 | throw new ParseException("This command has no --", candidate, " option"); 2342 | case 1: 2343 | return result; 2344 | default: 2345 | throw new ParseException("Option name --", candidate, " is ambiguous; please supply more characters"); 2346 | } 2347 | } 2348 | 2349 | auto farg = FindFarg; 2350 | if (farg.HasBeenSeen) 2351 | throw new ParseException("Please don't specify ", farg.DisplayAllNames, " more than once"); 2352 | 2353 | return farg; 2354 | } 2355 | 2356 | auto SeeLongAArg(FArgBase farg, in bool has_aarg, in string arg_name_from_user, string aarg_if_any) { 2357 | if (has_aarg) 2358 | farg.See(aarg_if_any, InvokedBy.LongName); 2359 | else if (farg.HasEqualsDefault) 2360 | farg.SeeEqualsDefault; 2361 | else { 2362 | MoveToNextAarg; 2363 | if (aargs.empty) { 2364 | if (farg.HasEolDefault) { 2365 | farg.SeeEolDefault; 2366 | return; 2367 | } 2368 | else { 2369 | // Use the full name if there's only one, or the name 2370 | // specified by the user (who may not be aware of some 2371 | // aliases) otherwise: 2372 | const arg_name = farg.GetNames.length == 1? farg.GetFirstName: arg_name_from_user; 2373 | throw new ParseException("The --", arg_name, " option must be followed by a piece of data"); 2374 | } 2375 | } 2376 | 2377 | farg.See(aargs.front, InvokedBy.LongName); 2378 | } 2379 | 2380 | MoveToNextAarg; 2381 | } 2382 | 2383 | auto ParseLong() { 2384 | const token = aargs[0][2..$]; 2385 | if (token.empty) { 2386 | seen_dbl_dash = true; 2387 | MoveToNextAarg; 2388 | return; 2389 | } 2390 | 2391 | enum Rgn {name, delim, aarg} 2392 | const regions = token.findSplit("="); 2393 | bool has_delim = !regions[Rgn.delim].empty; 2394 | auto farg = FindUnseenNamedFarg(regions[Rgn.name]); 2395 | 2396 | if (farg.NeedsAArg) 2397 | SeeLongAArg(farg, has_delim, regions[Rgn.name], regions[Rgn.aarg]); 2398 | else { 2399 | if (has_delim) { 2400 | if (farg.IsIncremental) // It can't accept an argument 2401 | throw new ParseException("The --", regions[Rgn.name], " option doesn't accept an argument"); 2402 | else // It's Boolean and will take an argument if it's conjoined 2403 | farg.See(regions[Rgn.aarg], InvokedBy.LongName); 2404 | } 2405 | else // No arg was provided, and none is needed 2406 | farg.See; 2407 | 2408 | MoveToNextAarg; 2409 | } 2410 | } 2411 | 2412 | auto ParseShort() { 2413 | auto aarg = aargs[0][1..$]; 2414 | for (;;) { 2415 | immutable shortname = aarg.front; 2416 | auto farg = FindUnseenNamedFarg(shortname); 2417 | aarg.popFront; 2418 | 2419 | immutable conjoined = aarg.startsWith('='); 2420 | if (farg.HasEqualsDefault && !conjoined) 2421 | farg.SeeEqualsDefault; 2422 | else if (farg.NeedsAArg) { 2423 | SeeShortArg(farg, shortname, aarg); 2424 | return; 2425 | } 2426 | else if (conjoined) { 2427 | if (farg.IsIncremental) 2428 | throw new ParseException("The -", shortname, " option doesn't accept an argument"); 2429 | else { 2430 | farg.See(aarg[1..$], InvokedBy.ShortName); 2431 | break; 2432 | } 2433 | } 2434 | else 2435 | farg.See(); 2436 | 2437 | if (aarg.empty) 2438 | break; 2439 | 2440 | if (can_bundle == CanBundle.No) 2441 | throw new ParseException("Unexpected text after -", shortname); 2442 | } 2443 | 2444 | MoveToNextAarg; 2445 | } 2446 | 2447 | auto FindUnseenNamedFarg(in dchar shortname) { 2448 | foreach (farg; fargs) 2449 | if (farg.GetShortName == shortname) { 2450 | if (farg.HasBeenSeen) 2451 | throw new ParseException("Please don't specify the -", shortname, " option more than once"); 2452 | else 2453 | return farg; 2454 | } 2455 | 2456 | throw new ParseException("This command has no -", shortname, " option"); 2457 | } 2458 | 2459 | auto SeeShortArg(FArgBase farg, in dchar shortname, in string aarg) { 2460 | if (aarg.empty) { 2461 | MoveToNextAarg; 2462 | if (aargs.empty) { 2463 | if (farg.HasEolDefault) { 2464 | farg.SeeEolDefault; 2465 | return; 2466 | } 2467 | else 2468 | throw new ParseException("The -", shortname, " option must be followed by a piece of data"); 2469 | } 2470 | 2471 | farg.See(aargs.front, InvokedBy.ShortName); 2472 | MoveToNextAarg; 2473 | } 2474 | else { 2475 | immutable skip_equals = aarg.front == '='; 2476 | farg.See(aarg[skip_equals .. $], InvokedBy.ShortName); 2477 | MoveToNextAarg; 2478 | } 2479 | } 2480 | 2481 | auto ParseData() { 2482 | if (FArgBase next_farg = FindFirstUnseenPositionalFArg) 2483 | next_farg.See(aargs.front, InvokedBy.Position); 2484 | else 2485 | spilt ~= aargs.front; 2486 | 2487 | seen_positional = true; 2488 | MoveToNextAarg; 2489 | } 2490 | 2491 | auto FindFirstUnseenPositionalFArg() { 2492 | foreach (farg; fargs) 2493 | if (farg.IsPositional) 2494 | if (!farg.HasBeenSeen) 2495 | return farg; 2496 | 2497 | return null; 2498 | } 2499 | 2500 | auto InsistOnMandatoryAArgs() { 2501 | foreach (farg; fargs) { 2502 | farg.Transform; 2503 | if (farg.IsMandatory && !farg.HasBeenSeen) 2504 | throw new ParseException("This command needs ", farg.DescribeArgumentOptimallyForError, " to be specified"); 2505 | } 2506 | } 2507 | 2508 | auto ApplyArgGroups() const { 2509 | foreach (group; arg_groups) 2510 | group.Check; 2511 | } 2512 | } 2513 | 2514 | unittest { 2515 | import std.math; 2516 | 2517 | enum Colours {black, red, green, yellow, blue, magenta, cyan, white} 2518 | immutable fake_program_name = "argon"; 2519 | 2520 | @safe class TestableHandler: Handler { 2521 | auto Run(ref string[] aargs) { 2522 | aargs = [fake_program_name] ~ aargs; 2523 | Parse(aargs); 2524 | } 2525 | 2526 | auto Run(string[] aargs) { 2527 | aargs = [fake_program_name] ~ aargs; 2528 | Parse(aargs); 2529 | } 2530 | 2531 | auto Run(in string joined_args) { 2532 | auto args = joined_args.split; 2533 | Run(args); 2534 | } 2535 | 2536 | auto FailRun(string[] aargs, in string want_error) { 2537 | try { 2538 | aargs = [fake_program_name] ~ aargs; 2539 | Parse(aargs); 2540 | assert(false, "The parse should have failed with this error: " ~ want_error); 2541 | } 2542 | catch (ParseException x) 2543 | if (x.msg != want_error) { 2544 | writeln("Expected this error: ", want_error); 2545 | writeln("Got this error: ", x.msg); 2546 | assert(false); 2547 | } 2548 | } 2549 | 2550 | auto FailRun(in string joined_args, in string want_error) { 2551 | FailRun(joined_args.split, want_error); 2552 | } 2553 | 2554 | auto ExpectSummary(in string expected) { 2555 | immutable summary = BuildSyntaxSummary; 2556 | assert(summary == expected, text("Expected: ", expected, "\nGenerated: ", summary)); 2557 | } 2558 | } 2559 | 2560 | @safe class Fields00: TestableHandler { 2561 | int alpha, bravo, charlie, delta; 2562 | double echo, foxtrot; 2563 | Colours colour; 2564 | char[] name, nino; 2565 | bool turn, twist, tumble; 2566 | Indicator got_alpha, got_bravo, got_charlie, got_delta, got_echo, got_foxtrot; 2567 | Indicator got_colour, got_name; 2568 | 2569 | this() { 2570 | Preserve; 2571 | } 2572 | } 2573 | 2574 | @safe class CP00: Fields00 { 2575 | this() { 2576 | // Test the long way of specifying short names: 2577 | Named ("alpha|able|alfie|aah", alpha, 5) .Short('a'); 2578 | Named ("bravo|baker", bravo, got_bravo).Short('b').Description("call of approbation"); 2579 | Named ("charlie|charles", charlie) .Short('c'); 2580 | Incremental("delta", delta) .Short('d'); 2581 | Named ("echo", echo, 0.0); 2582 | 2583 | ExpectSummary("[--alpha ] [--bravo ] --charlie --delta* [--echo ]"); 2584 | } 2585 | } 2586 | 2587 | // Default values for int receivers and indicators: 2588 | auto cp00 = new CP00; 2589 | cp00.Run("--charlie=32"); 2590 | assert(cp00.alpha == 5); 2591 | assert(cp00.bravo == cp00.bravo.init); 2592 | assert(cp00.got_bravo == Indicator.NotSeen); 2593 | assert(cp00.charlie == 32); 2594 | assert(cp00.delta == 0); 2595 | 2596 | // Non-default values for int receivers and indicators: 2597 | cp00.Run("--alpha 8 --bravo 7 --charlie 9"); 2598 | assert(cp00.alpha == 8); 2599 | assert(cp00.bravo == 7); 2600 | assert(cp00.charlie == 9); 2601 | assert(cp00.got_bravo == Indicator.Seen); 2602 | 2603 | // Abbreviated and short names: 2604 | cp00.Run("--al 23 -c 17"); 2605 | assert(cp00.got_bravo == Indicator.NotSeen); 2606 | assert(cp00.bravo == cp00.bravo.init); 2607 | assert(cp00.alpha == 23); 2608 | assert(cp00.charlie == 17); 2609 | 2610 | // Alternative names, and abbreviated names with the same stem for the same farg: 2611 | cp00.Run("--able 22 --cha 18"); 2612 | assert(cp00.got_bravo == Indicator.NotSeen); 2613 | assert(cp00.bravo == cp00.bravo.init); 2614 | assert(cp00.alpha == 22); 2615 | assert(cp00.charlie == 18); 2616 | 2617 | // Same farg used twice: 2618 | cp00.FailRun("--alpha 4 --bravo 7 -a 8", "Please don't specify the -a option more than once"); 2619 | cp00.FailRun("--alpha 4 --bravo 7 --alpha 8", "Please don't specify the --alpha option (also known as --able and --alfie and --aah) more than once"); 2620 | 2621 | // Bad long option name: 2622 | cp00.FailRun("--alfred 4 --bravo 7 -c 8", "This command has no --alfred option"); 2623 | 2624 | // Bad short name: 2625 | cp00.FailRun("--alpha 4 --bravo 7 -Q 8", "This command has no -Q option"); 2626 | 2627 | // Alternative ways of joining options and data: 2628 | cp00.Run("-a1 --bravo=2 -c=9"); 2629 | assert(cp00.alpha == 1); 2630 | assert(cp00.bravo == 2); 2631 | assert(cp00.charlie == 9); 2632 | assert(cp00.got_bravo == Indicator.Seen); 2633 | 2634 | // Unexpected positional AArg: 2635 | cp00.FailRun("--alpha 4 --charlie 7 5", "Unexpected text: '5': this command has no positional arguments"); 2636 | 2637 | // Invalid argument: 2638 | cp00.FailRun("--alpha papa", "Invalid argument for the --alpha option (also known as --able and --alfie and --aah): papa"); 2639 | 2640 | // Missing required parameter. 2641 | // --charlie has aliases, so we should see back whatever name we used: 2642 | cp00.FailRun("--alpha 6 --bravo 7 --charlie", "The --charlie option must be followed by a piece of data"); 2643 | cp00.FailRun("--alpha 6 --bravo 7 --charles", "The --charles option must be followed by a piece of data"); 2644 | cp00.FailRun("--alpha 6 --bravo 7 --cha", "The --cha option must be followed by a piece of data"); 2645 | cp00.FailRun("--alpha 6 --bravo 7 -c", "The -c option must be followed by a piece of data"); 2646 | 2647 | // --echo has no alias, and so we we should always see back its full name: 2648 | cp00.FailRun("--charlie 3 --echo", "The --echo option must be followed by a piece of data"); 2649 | cp00.FailRun("--charlie 3 --ec", "The --echo option must be followed by a piece of data"); 2650 | 2651 | // Positional parameter passed to a command that doesn't expect any: 2652 | cp00.FailRun("-c0 armadillo", "Unexpected text: 'armadillo': this command has no positional arguments"); 2653 | 2654 | // Incremental parameter: 2655 | cp00.Run("--delta --charlie 13"); 2656 | assert(cp00.charlie == 13); 2657 | assert(cp00.delta == 1); 2658 | 2659 | cp00.Run("--delta -ddd --charlie 13"); 2660 | assert(cp00.charlie == 13); 2661 | assert(cp00.delta == 4); 2662 | 2663 | cp00.FailRun("--charlie 33 --delta 9", "Unexpected text: '9': this command has no positional arguments"); 2664 | cp00.FailRun("--charlie 33 --delta=9", "The --delta option doesn't accept an argument"); 2665 | cp00.FailRun("--charlie 33 -d 9", "Unexpected text: '9': this command has no positional arguments"); 2666 | cp00.FailRun("--charlie 33 -d=9", "The -d option doesn't accept an argument"); 2667 | 2668 | // Mixed-case and non-Ascii names, names that are trunks of others, and 2669 | // the ability to enable or disable bundling: 2670 | class CP01: Fields00 { 2671 | this() { 2672 | // Use the short way of specifying short names: 2673 | Named("liberté", alpha) ('l'); 2674 | Named("égalité", bravo) ('é'); 2675 | Named("fraternité", charlie) ('f'); 2676 | Named("Fraternité", delta) ('F').Undocumented; 2677 | Named("abcd", turn) ('u'); 2678 | Named("abcde", twist) ('w'); 2679 | Named("abcdef", tumble) ('m'); 2680 | 2681 | ExpectSummary("--liberté --égalité <égalité> --fraternité [--abcd] [--abcde] [--abcdef]"); 2682 | } 2683 | } 2684 | 2685 | auto cp01 = new CP01; 2686 | cp01.Run("--liberté 4 --égalité=5 --fr 8 --F=9"); 2687 | assert(cp01.alpha == 4); 2688 | assert(cp01.bravo == 5); 2689 | assert(cp01.charlie == 8); 2690 | assert(cp01.delta == 9); 2691 | 2692 | cp01.Run("-l1 -é3 -F=7 -f 5"); 2693 | assert(cp01.alpha == 1); 2694 | assert(cp01.bravo == 3); 2695 | assert(cp01.charlie == 5); 2696 | assert(cp01.delta == 7); 2697 | 2698 | cp01.FailRun("-l1 -é3 -F=7 -f 5 --abc", "Option name --abc is ambiguous; please supply more characters"); 2699 | 2700 | cp01.Run("-l1 -é3 -F=7 -f 5 --abcd"); 2701 | assert( cp01.turn); 2702 | assert(!cp01.twist); 2703 | assert(!cp01.tumble); 2704 | 2705 | cp01.Run("-l1 -é3 -F=7 -f 5 --abcde"); 2706 | assert(!cp01.turn); 2707 | assert( cp01.twist); 2708 | assert(!cp01.tumble); 2709 | 2710 | cp01.Run("-l1 -é3 -F=7 -f 5 --abcdef"); 2711 | assert(!cp01.turn); 2712 | assert(!cp01.twist); 2713 | assert( cp01.tumble); 2714 | 2715 | cp01.FailRun("-l1 -é3 -F=7 -f 5 --abcefg", "This command has no --abcefg option"); 2716 | 2717 | // Mandatory parameters: 2718 | cp01.FailRun("-l1 -é3 -F=7", "This command needs the --fraternité option to be specified"); 2719 | 2720 | // Bundling: 2721 | cp01.Run("-l1 -é3 -F=7 -f 5 -u"); 2722 | assert( cp01.turn); 2723 | assert(!cp01.twist); 2724 | assert(!cp01.tumble); 2725 | 2726 | cp01.Run("-l1 -é3 -F=7 -f 5 -um"); 2727 | assert( cp01.turn); 2728 | assert(!cp01.twist); 2729 | assert( cp01.tumble); 2730 | 2731 | cp01.Run("-l1 -é3 -F=7 -f 5 -umw"); 2732 | assert( cp01.turn); 2733 | assert( cp01.twist); 2734 | assert( cp01.tumble); 2735 | 2736 | cp01(CanBundle.No); 2737 | cp01.Run("-l1 -é3 -F=7 -f 5 -u"); 2738 | assert( cp01.turn); 2739 | assert(!cp01.twist); 2740 | assert(!cp01.tumble); 2741 | 2742 | cp01.FailRun("-l1 -é3 -F=7 -f 5 -um", "Unexpected text after -u"); 2743 | 2744 | // Special treatment of Boolean options: 2745 | class CP02: Fields00 { 2746 | this() { 2747 | Named("turn", turn) ('u'); 2748 | Named("twist", twist, true) ('w'); 2749 | Named("tumble", tumble, false) ('m'); 2750 | } 2751 | } 2752 | 2753 | // Default Boolean values: 2754 | auto cp02 = new CP02; 2755 | cp02.Run(""); 2756 | assert(!cp02.turn); 2757 | assert( cp02.twist); 2758 | assert(!cp02.tumble); 2759 | 2760 | // If we just specify the bare option name, all flags should be inverted: 2761 | cp02.Run("--turn --tw --tum"); 2762 | assert( cp02.turn); 2763 | assert(!cp02.twist); 2764 | assert( cp02.tumble); 2765 | 2766 | // If we specify =false or similar, all flags should go false, regardless 2767 | // of their default values: 2768 | cp02.Run("--turn=false --tw=0 --tum=no"); 2769 | assert(!cp02.turn); 2770 | assert(!cp02.twist); 2771 | assert(!cp02.tumble); 2772 | 2773 | cp02.Run("-u=false -w=0 -m=no"); 2774 | assert(!cp02.turn); 2775 | assert(!cp02.twist); 2776 | assert(!cp02.tumble); 2777 | 2778 | // Similarly, specifying =true or similar should make them all go true: 2779 | cp02.Run("--turn=tr --tw=1 --tum=yes"); 2780 | assert( cp02.turn); 2781 | assert( cp02.twist); 2782 | assert( cp02.tumble); 2783 | 2784 | cp02.Run("-u=true -w=1 -m=yes"); 2785 | assert( cp02.turn); 2786 | assert( cp02.twist); 2787 | assert( cp02.tumble); 2788 | 2789 | // Any value must be attached to the option by '=', and can't follow in the 2790 | // next token: 2791 | cp02.FailRun("--turn yes", "Unexpected text: 'yes': this command has no positional arguments"); 2792 | 2793 | // If there's a positional string argument, the 'yes' AArg should go there 2794 | // instead: 2795 | class CP03: CP02 { 2796 | this() { 2797 | Pos("name of the oojit", name, ""); 2798 | } 2799 | } 2800 | 2801 | auto cp03 = new CP03; 2802 | cp03.Run("--turn yes"); 2803 | assert(cp03.turn); 2804 | assert(cp03.name == "yes"); 2805 | 2806 | // Test that we can assign short names and call type-specific methods in 2807 | // any order. (Bad style; don't emulate.) 2808 | class CP04: Fields00 { 2809 | this() { 2810 | Named("alpha", alpha, got_alpha) ('a').AddRange(0, 9).AddRange(20, 29) ("transparency"); 2811 | Named("bravo", bravo, -1) .AddRange(0, 9) ('b').AddRange(20, 29) ("señor"); 2812 | Named("name", name, "") ('n') ("nomenclature").LimitLength(1, 20); 2813 | Named("nino", nino, "") .LimitLength(9, 9).Description("national insurance number") ('i'); 2814 | 2815 | ExpectSummary("[--alpha ] [--bravo ] [--name ] [--nino ]"); 2816 | } 2817 | } 2818 | 2819 | auto cp04 = new CP04; 2820 | cp04.Run("-b 24 -n Nancy"); 2821 | assert(cp04.alpha == cp04.alpha.init); 2822 | assert(cp04.got_alpha == Indicator.NotSeen); 2823 | assert(cp04.bravo == 24); 2824 | assert(cp04.name == "Nancy"); 2825 | assert(cp04.nino.empty); // The default value can have a length outside the mandated range 2826 | 2827 | cp04.Run("-a7 -iAB123456X"); 2828 | assert(cp04.alpha == 7); 2829 | assert(cp04.got_alpha == Indicator.Seen); 2830 | assert(cp04.bravo == -1); // Again, a numeric default can lie outside the prescribed range 2831 | assert(cp04.name.empty); 2832 | assert(cp04.nino == "AB123456X"); 2833 | 2834 | // Positional arguments: 2835 | class CP05: Fields00 { 2836 | this() { 2837 | Pos("alpha", alpha); // Mandatory, because no indicator or default value is given 2838 | Pos("bravo", bravo, got_bravo); // Optional: has an indicator 2839 | Pos("charlie", charlie, 23); // Optional: has a default value 2840 | } 2841 | } 2842 | 2843 | auto cp05 = new CP05; 2844 | cp05.Run("14"); 2845 | assert(cp05.alpha == 14); 2846 | assert(cp05.bravo == cp05.bravo.init); 2847 | assert(cp05.got_bravo == Indicator.NotSeen); 2848 | assert(cp05.charlie == 23); 2849 | 2850 | cp05.Run("14 39"); 2851 | assert(cp05.alpha == 14); 2852 | assert(cp05.bravo == 39); 2853 | assert(cp05.got_bravo == Indicator.Seen); 2854 | assert(cp05.charlie == 23); 2855 | 2856 | cp05.Run("14 39 43"); 2857 | assert(cp05.alpha == 14); 2858 | assert(cp05.bravo == 39); 2859 | assert(cp05.got_bravo == Indicator.Seen); 2860 | assert(cp05.charlie == 43); 2861 | 2862 | cp05.FailRun("--alpha 3", "This command has no --alpha option"); 2863 | 2864 | // Mixing named and positional arguments, and passing unused arguments 2865 | // back to the caller: 2866 | class CP06: Fields00 { 2867 | this() { 2868 | Named("alpha", alpha) ("the première").EolDefault(23) ('a'); 2869 | Named("bravo", bravo, got_bravo) .EolDefault(22) ('b'); 2870 | Named("charlie", charlie, 11) .EolDefault(21) ('c'); 2871 | 2872 | Named("turn", turn); 2873 | Named("twist", twist); 2874 | Named("tumble", tumble); 2875 | 2876 | Pos ("delta", delta); 2877 | Pos ("echo", echo, got_echo); 2878 | Pos ("foxtrot", foxtrot, 5.0); 2879 | 2880 | ExpectSummary("--alpha [--bravo ] [--charlie ] [--turn] [--twist] [--tumble] [] []"); 2881 | } 2882 | } 2883 | 2884 | auto cp06 = new CP06; 2885 | with (cp06) { 2886 | Run("--alpha 7 16"); 2887 | assert( alpha == 7); 2888 | assert( bravo == cp06.bravo.init); 2889 | assert( got_bravo == Indicator.NotSeen); 2890 | assert( charlie == 11); 2891 | assert( delta == 16); 2892 | assert( echo.isNaN); 2893 | assert( got_echo == Indicator.NotSeen); 2894 | assert( foxtrot == 5.0); 2895 | assert(!turn); 2896 | assert(!twist); 2897 | assert(!tumble); 2898 | } 2899 | 2900 | with (cp06) { 2901 | Run("16 --alpha 7"); 2902 | assert( alpha == 7); 2903 | assert( bravo == cp06.bravo.init); 2904 | assert( got_bravo == Indicator.NotSeen); 2905 | assert( charlie == 11); 2906 | assert( delta == 16); 2907 | assert( echo.isNaN); 2908 | assert( got_echo == Indicator.NotSeen); 2909 | assert( foxtrot == 5.0); 2910 | assert(!turn); 2911 | assert(!twist); 2912 | assert(!tumble); 2913 | } 2914 | 2915 | with (cp06) { 2916 | Run("16 --alpha 7 4 --bravo 9 --charlie 10 6.25"); 2917 | assert( alpha == 7); 2918 | assert( bravo == 9); 2919 | assert( got_bravo == Indicator.Seen); 2920 | assert( charlie == 10); 2921 | assert( delta == 16); 2922 | assert( echo == 4.0); 2923 | assert( got_echo == Indicator.Seen); 2924 | assert( foxtrot == 6.25); 2925 | assert(!turn); 2926 | assert(!twist); 2927 | assert(!tumble); 2928 | } 2929 | cp06(OnOptionAfterData.AssumeData); 2930 | cp06.FailRun("16 --alpha 7 4 --bravo 9 --charlie 10 6.25", "Invalid argument for the echo: --alpha"); 2931 | 2932 | cp06(OnOptionAfterData.Fail); 2933 | cp06.FailRun("16 --alpha 7 4 --bravo 9 --charlie 10 6.25", "No --option is permitted after a positional argument"); 2934 | 2935 | cp06(OnOptionAfterData.AssumeOption); // Just because that's the default state 2936 | 2937 | // Long description should be used whenever it's available: 2938 | cp06.FailRun("--alpha piper --delta 33", "Invalid argument for the première: piper"); 2939 | 2940 | // Unused arguments should be returned to the caller if requested: 2941 | cp06(PassBackUnusedAArgs.Yes); 2942 | auto aargs = ["--alpha", "9", /* delta: */ "1", /* echo: */ "2", "--bravo", "10", /* foxtrot: */ "3", "--charlie", "14", /* spilt: */ "hello", "--turn", /* spilt: */ "world"]; 2943 | cp06.Run(aargs); 2944 | assert(aargs == ["hello", "world"]); 2945 | cp06(PassBackUnusedAArgs.No); // Return to the default state 2946 | 2947 | // End-of-line defaults: 2948 | with (cp06) { 2949 | Run("16 4.0 --bravo 9 --charlie 10 6.25 --alpha"); 2950 | assert( alpha == 23); 2951 | assert( bravo == 9); 2952 | assert( got_bravo == Indicator.Seen); 2953 | assert( charlie == 10); 2954 | assert( delta == 16); 2955 | assert( echo == 4.0); 2956 | assert( got_echo == Indicator.Seen); 2957 | assert( foxtrot == 6.25); 2958 | assert(!turn); 2959 | assert(!twist); 2960 | assert(!tumble); 2961 | } 2962 | 2963 | with (cp06) { 2964 | Run("16 4.0 --bravo 9 --charlie 10 6.25 -a"); 2965 | assert( alpha == 23); 2966 | assert( bravo == 9); 2967 | assert( got_bravo == Indicator.Seen); 2968 | assert( charlie == 10); 2969 | assert( delta == 16); 2970 | assert( echo == 4.0); 2971 | assert( got_echo == Indicator.Seen); 2972 | assert( foxtrot == 6.25); 2973 | assert(!turn); 2974 | assert(!twist); 2975 | assert(!tumble); 2976 | } 2977 | 2978 | with (cp06) { 2979 | Run("--alpha 29 16 4.0 --charlie 10 6.25 --bravo"); 2980 | assert( alpha == 29); 2981 | assert( bravo == 22); 2982 | assert( got_bravo == Indicator.UsedEolDefault); 2983 | assert( charlie == 10); 2984 | assert( delta == 16); 2985 | assert( echo == 4.0); 2986 | assert( got_echo == Indicator.Seen); 2987 | assert( foxtrot == 6.25); 2988 | assert(!turn); 2989 | assert(!twist); 2990 | assert(!tumble); 2991 | } 2992 | 2993 | with (cp06) { 2994 | Run("--alpha 29 16 4.0 --charlie 10 6.25 -b"); 2995 | assert( alpha == 29); 2996 | assert( bravo == 22); 2997 | assert( got_bravo == Indicator.UsedEolDefault); 2998 | assert( charlie == 10); 2999 | assert( delta == 16); 3000 | assert( echo == 4.0); 3001 | assert( got_echo == Indicator.Seen); 3002 | assert( foxtrot == 6.25); 3003 | assert(!turn); 3004 | assert(!twist); 3005 | assert(!tumble); 3006 | } 3007 | 3008 | with (cp06) { 3009 | Run("--alpha 29 16 4.0 --bravo 10 6.25 --charlie"); 3010 | assert( alpha == 29); 3011 | assert( bravo == 10); 3012 | assert( got_bravo == Indicator.Seen); 3013 | assert( charlie == 21); 3014 | assert( delta == 16); 3015 | assert( echo == 4.0); 3016 | assert( got_echo == Indicator.Seen); 3017 | assert( foxtrot == 6.25); 3018 | assert(!turn); 3019 | assert(!twist); 3020 | assert(!tumble); 3021 | } 3022 | 3023 | with (cp06) { 3024 | Run("--alpha 29 16 4.0 --bravo 10 6.25 -c"); 3025 | assert( alpha == 29); 3026 | assert( bravo == 10); 3027 | assert( got_bravo == Indicator.Seen); 3028 | assert( charlie == 21); 3029 | assert( delta == 16); 3030 | assert( echo == 4.0); 3031 | assert( got_echo == Indicator.Seen); 3032 | assert( foxtrot == 6.25); 3033 | assert(!turn); 3034 | assert(!twist); 3035 | assert(!tumble); 3036 | } 3037 | 3038 | class CP07: Fields00 { 3039 | FArgBase arg_alpha, arg_bravo, arg_charlie, arg_delta; 3040 | FArgBase arg_echo, arg_foxtrot; 3041 | FArgBase arg_turn, arg_twist, arg_tumble; 3042 | FArgBase arg_name, arg_nino; 3043 | 3044 | this() { 3045 | arg_alpha = Named("alpha", alpha, 0) ('a'); 3046 | arg_bravo = Named("bravo", bravo, 0) ('b'); 3047 | arg_charlie = Named("charlie", charlie, 0) ('c'); 3048 | arg_delta = Named("delta", delta, 0) ('d'); 3049 | arg_echo = Named("echo", echo, 0) ('e'); 3050 | arg_foxtrot = Named("foxtrot", foxtrot, 0) ('f'); 3051 | arg_turn = Named("turn", turn) ('u'); 3052 | arg_twist = Named("twist", twist) ('w'); 3053 | arg_tumble = Named("tumble", tumble) ('m'); 3054 | arg_name = Named("name", name, "") ('n'); 3055 | arg_nino = Named("nino", nino, "") ('N'); 3056 | } 3057 | } 3058 | 3059 | with (new CP07) { 3060 | // This is a gross encapsulation violation. Normally, the Handler 3061 | // subclass would write something like this in its constructor: 3062 | // 3063 | // BetweenNAndMOf(2, 3, 3064 | // Named(...), 3065 | // Named(...), 3066 | // Named(...), 3067 | // Named(...)); 3068 | // 3069 | // All those arg_xxx variables would normally be necessary only when an 3070 | // argument was a member of two or more argument groups, and that's rare 3071 | // in practice. 3072 | 3073 | BetweenNAndMOf(2, 3, arg_alpha, arg_bravo, arg_charlie, arg_delta); 3074 | FailRun("--alpha 23", "Please specify between 2 and 3 of the --alpha option, the --bravo option, the --charlie option and the --delta option"); 3075 | Run ("--alpha 1 --bravo 2"); 3076 | Run ("--alpha 1 --bravo 2 --charlie 3"); 3077 | Run ("--alpha 1 --bravo 2 --name Donald --nino quack-quack-quack"); 3078 | FailRun("--alpha 1 --bravo 2 --charlie 3 --delta 4", "Please specify between 2 and 3 of the --alpha option, the --bravo option, the --charlie option and the --delta option"); 3079 | } 3080 | 3081 | with (new CP07) { 3082 | ExactlyNOf(2, arg_alpha, arg_bravo, arg_charlie, arg_delta); 3083 | FailRun("--alpha 23", "Please specify exactly 2 of the --alpha option, the --bravo option, the --charlie option and the --delta option"); 3084 | Run ("--alpha 1 --bravo 2"); 3085 | FailRun("--alpha 1 --bravo 2 --charlie 3", "Please specify exactly 2 of the --alpha option, the --bravo option, the --charlie option and the --delta option"); 3086 | FailRun("--alpha 1 --bravo 2 --charlie 3 --delta 4", "Please specify exactly 2 of the --alpha option, the --bravo option, the --charlie option and the --delta option"); 3087 | } 3088 | 3089 | with (new CP07) { 3090 | ExactlyOneOf(arg_alpha, arg_bravo); 3091 | FailRun("", "Please specify exactly 1 of the --alpha option and the --bravo option"); 3092 | Run ("--alpha 1"); 3093 | Run ("--bravo 33"); 3094 | Run ("--bravo 33 --charlie 4 --delta 59"); 3095 | FailRun("--alpha 1 --bravo 2", "Please specify exactly 1 of the --alpha option and the --bravo option"); 3096 | } 3097 | 3098 | with (new CP07) { 3099 | AtMostNOf(2, arg_alpha, arg_bravo, arg_charlie); 3100 | Run (""); 3101 | Run ("--alpha 23"); 3102 | Run ("--alpha 1 --bravo 2"); 3103 | Run ("--alpha 1 --bravo 2 --delta 3"); 3104 | FailRun("--alpha 1 --bravo 2 --charlie 4 --name Clarence", "Please don't specify more than 2 of the --alpha option, the --bravo option and the --charlie option"); 3105 | } 3106 | 3107 | with (new CP07) { 3108 | AtMostOneOf(arg_alpha, arg_bravo, arg_charlie); 3109 | Run (""); 3110 | Run ("--alpha 23"); 3111 | Run ("--bravo 2"); 3112 | Run ("--charlie 194 --delta 3"); 3113 | Run ("--delta 3"); 3114 | Run ("--charlie 8"); 3115 | FailRun("--alpha 1 --bravo 2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3116 | FailRun("--alpha 1 --charlie 2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3117 | FailRun("--charlie 1 --bravo 2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3118 | FailRun("--alpha 22 --charlie 1 --bravo 2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3119 | } 3120 | 3121 | with (new CP07) { 3122 | // Seenness shouldn't depend on whether the user chose to type long or 3123 | // short names, but let's test it anyway: 3124 | AtMostOneOf(arg_alpha, arg_bravo, arg_charlie); 3125 | Run (""); 3126 | Run ("-a23"); 3127 | Run ("-b 2"); 3128 | Run ("-c=194 -d 3"); 3129 | Run ("-d=3"); 3130 | Run ("-c8"); 3131 | FailRun("-a1 -b=2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3132 | FailRun("-a=1 -c 2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3133 | FailRun("-c1 -b2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3134 | FailRun("-a 22 -c 1 -b 2", "Please don't specify more than 1 of the --alpha option, the --bravo option and the --charlie option"); 3135 | } 3136 | 3137 | with (new CP07) { 3138 | FirstOrNone(arg_alpha, arg_bravo, arg_charlie); 3139 | Run (""); 3140 | Run ("-a23"); 3141 | Run ("-b2 -a22"); 3142 | Run ("-c=194 -a3"); 3143 | Run ("-d=3"); 3144 | FailRun("-b=2", "Please don't specify the --bravo option without also specifying the --alpha option"); 3145 | FailRun("-c=2 -d=9", "Please don't specify the --charlie option without also specifying the --alpha option"); 3146 | FailRun("-c1 -b2", "Please don't specify the --bravo option without also specifying the --alpha option"); 3147 | } 3148 | 3149 | with (new CP07) { 3150 | AllOrNone(arg_alpha, arg_bravo, arg_charlie); 3151 | Run ("--alpha 6 --charl 4 --brav 5"); 3152 | Run (""); 3153 | FailRun("-a6 -b5", "Please specify either all or none of the --alpha option, the --bravo option and the --charlie option"); 3154 | FailRun("-a6 -c5", "Please specify either all or none of the --alpha option, the --bravo option and the --charlie option"); 3155 | FailRun("-c6 -b5", "Please specify either all or none of the --alpha option, the --bravo option and the --charlie option"); 3156 | FailRun("-c6 -b5 -d2", "Please specify either all or none of the --alpha option, the --bravo option and the --charlie option"); 3157 | } 3158 | 3159 | with (new CP07) { 3160 | AllOrNone(arg_alpha, arg_bravo); 3161 | Run ("--alpha 6 --charl 4 --brav 5"); 3162 | Run (""); 3163 | FailRun("-a6 -c5", "Please specify either both or neither of the --alpha option and the --bravo option"); 3164 | FailRun("-b6 -c5", "Please specify either both or neither of the --alpha option and the --bravo option"); 3165 | FailRun("-c6 -b5", "Please specify either both or neither of the --alpha option and the --bravo option"); 3166 | FailRun("-c6 -b5 -d2", "Please specify either both or neither of the --alpha option and the --bravo option"); 3167 | } 3168 | 3169 | with (new CP07) { 3170 | // A single argument can belong to more than one arg group. 3171 | // This models HNAS's `sd-write-block' command, which accepts either an 3172 | // SD location as three parameters or a --last-read switch. 3173 | AllOrNone (arg_alpha, arg_bravo, arg_charlie); 3174 | ExactlyOneOf(arg_alpha, arg_twist); 3175 | 3176 | Run("-a0 -b1 -c2"); 3177 | Run("--twist"); 3178 | FailRun("", "Please specify exactly 1 of the --alpha option and the --twist option"); 3179 | FailRun("-a0 -b1", "Please specify either all or none of the --alpha option, the --bravo option and the --charlie option"); 3180 | FailRun("-a0 -b2 -c2 -w", "Please specify exactly 1 of the --alpha option and the --twist option"); 3181 | } 3182 | 3183 | // Test EOL defaults: 3184 | 3185 | class CP08: Fields00 { 3186 | this() { 3187 | Named("alpha", alpha, 1) ('a').EqualsDefault(5); 3188 | Named("bravo", bravo, 2) ('b').EqualsDefault(6); 3189 | Named("charlie", charlie, 3) ('c').EqualsDefault(7); 3190 | Named("delta", delta, 4) ('d').EqualsDefault(8); 3191 | } 3192 | } 3193 | 3194 | with (new CP08) { 3195 | auto eq_aargs = ["fake-program-name", "--alpha", "--bravo=12", "-cd=13"]; 3196 | Parse(eq_aargs); 3197 | assert(alpha == 5); 3198 | assert(bravo == 12); 3199 | assert(charlie == 7); 3200 | assert(delta == 13); 3201 | } 3202 | 3203 | // Test File arguments: 3204 | 3205 | class Fields01: TestableHandler { 3206 | File alpha, bravo, charlie, delta, echo, foxtrot; 3207 | string alpha_error, bravo_error, charlie_error, delta_error, echo_error, foxtrot_error; 3208 | Indicator got_alpha, got_bravo, got_charlie, got_delta, got_echo, got_foxtrot; 3209 | 3210 | this() { 3211 | Preserve; 3212 | } 3213 | 3214 | enum Mode = "rb"; 3215 | 3216 | void RunWithHardOpenFailure(in string joined_args) { 3217 | try { 3218 | Run(joined_args); 3219 | assert(false, "The open-failure should have thrown an exception; it didn't"); 3220 | } 3221 | catch (Exception) { } 3222 | } 3223 | 3224 | void RunWithHardOpenFailure(string[] args) { 3225 | try { 3226 | Run(args); 3227 | assert(false, "The open-failure should have thrown an exception; it didn't"); 3228 | } 3229 | catch (Exception) { } 3230 | } 3231 | } 3232 | 3233 | class CP10: Fields01 { 3234 | this(in string echo_default_name, in string foxtrot_default_name) { 3235 | // Mandatory with hard failure: 3236 | Named("alpha", alpha, Mode, null) .EolDefault("-"); 3237 | 3238 | // Mandatory with soft failure: 3239 | Named("bravo", bravo, Mode, &bravo_error) .EolDefault("-"); 3240 | 3241 | // Optional with indicator and hard failure: 3242 | Named("charlie", charlie, Mode, got_charlie, null) .EolDefault("-"); 3243 | 3244 | // Optional with indicator and soft failure: 3245 | Named("delta", delta, Mode, got_delta, &delta_error) .EolDefault("-"); 3246 | 3247 | // Optional with default filename and hard failure: 3248 | Named("echo", echo, Mode, echo_default_name, null) .EolDefault("-"); 3249 | 3250 | // Optional with default filename and soft failure: 3251 | Named("foxtrot", foxtrot, Mode, foxtrot_default_name, &foxtrot_error).EolDefault("-"); 3252 | } 3253 | } 3254 | 3255 | @trusted auto test_named_file_arguments() { 3256 | // @trusted because it refers to __gshared stdin, but never from 3257 | // multi-threaded code. 3258 | 3259 | auto cp10a = new CP10(existent_file, existent_file); 3260 | cp10a.ExpectSummary("--alpha --bravo [--charlie ] [--delta ] [--echo ] [--foxtrot ]"); 3261 | cp10a.FailRun("", "This command needs the --alpha option to be specified"); 3262 | 3263 | with (cp10a) { 3264 | Run(["--alpha", existent_file, "--bravo", existent_file]); 3265 | assert( alpha.isOpen); 3266 | assert( bravo.isOpen); 3267 | assert(!charlie.isOpen); 3268 | assert(!delta.isOpen); 3269 | assert( echo.isOpen); 3270 | assert( foxtrot.isOpen); 3271 | assert( bravo_error.empty); 3272 | assert( delta_error.empty); 3273 | assert( foxtrot_error.empty); 3274 | assert( got_charlie == Indicator.NotSeen); 3275 | assert( got_delta == Indicator.NotSeen); 3276 | } 3277 | 3278 | cp10a.RunWithHardOpenFailure(["--alpha", nonexistent_file, "--bravo", existent_file]); 3279 | 3280 | with (cp10a) { 3281 | Run(["--alpha", existent_file, "--bravo", nonexistent_file]); 3282 | assert( alpha.isOpen); 3283 | assert(!bravo.isOpen); 3284 | assert(!charlie.isOpen); 3285 | assert(!delta.isOpen); 3286 | assert( echo.isOpen); 3287 | assert( foxtrot.isOpen); 3288 | assert(!bravo_error.empty); 3289 | assert( delta_error.empty); 3290 | assert( foxtrot_error.empty); 3291 | assert( got_charlie == Indicator.NotSeen); 3292 | assert( got_delta == Indicator.NotSeen); 3293 | } 3294 | 3295 | cp10a.RunWithHardOpenFailure(["--alpha", existent_file, "--bravo", nonexistent_file, "--charlie", nonexistent_file]); 3296 | 3297 | with (cp10a) { 3298 | Run(["--alpha", existent_file, "--bravo", nonexistent_file, "--charlie", existent_file]); 3299 | assert( alpha.isOpen); 3300 | assert(!bravo.isOpen); 3301 | assert( charlie.isOpen); 3302 | assert(!delta.isOpen); 3303 | assert( echo.isOpen); 3304 | assert( foxtrot.isOpen); 3305 | assert(!bravo_error.empty); 3306 | assert( delta_error.empty); 3307 | assert( foxtrot_error.empty); 3308 | assert( got_charlie == Indicator.Seen); 3309 | assert( got_delta == Indicator.NotSeen); 3310 | } 3311 | 3312 | cp10a.RunWithHardOpenFailure(["--alpha", existent_file, "--bravo", nonexistent_file, "--delta", nonexistent_file]); 3313 | 3314 | with (cp10a) { 3315 | Run(["--alpha", existent_file, "--bravo", existent_file, "--delta", existent_file]); 3316 | assert( alpha.isOpen); 3317 | assert( bravo.isOpen); 3318 | assert(!charlie.isOpen); 3319 | assert( delta.isOpen); 3320 | assert( echo.isOpen); 3321 | assert( foxtrot.isOpen); 3322 | assert( bravo_error.empty); 3323 | assert( delta_error.empty); 3324 | assert( foxtrot_error.empty); 3325 | assert( got_charlie == Indicator.NotSeen); 3326 | assert( got_delta == Indicator.Seen); 3327 | } 3328 | 3329 | cp10a.RunWithHardOpenFailure(["--alpha", existent_file, "--bravo", existent_file, "--echo", nonexistent_file]); 3330 | 3331 | with (cp10a) { 3332 | Run(["--alpha", existent_file, "--bravo", existent_file, "--foxtrot", existent_file]); 3333 | assert( alpha.isOpen); 3334 | assert( bravo.isOpen); 3335 | assert(!charlie.isOpen); 3336 | assert(!delta.isOpen); 3337 | assert( echo.isOpen); 3338 | assert( foxtrot.isOpen); 3339 | assert( bravo_error.empty); 3340 | assert( delta_error.empty); 3341 | assert( foxtrot_error.empty); 3342 | assert( got_charlie == Indicator.NotSeen); 3343 | assert( got_delta == Indicator.NotSeen); 3344 | } 3345 | 3346 | with (cp10a) { 3347 | Run(["--alpha", existent_file, "--bravo", existent_file, "--foxtrot", nonexistent_file]); 3348 | assert( alpha.isOpen); 3349 | assert( bravo.isOpen); 3350 | assert(!charlie.isOpen); 3351 | assert(!delta.isOpen); 3352 | assert( echo.isOpen); 3353 | assert(!foxtrot.isOpen); 3354 | assert( bravo_error.empty); 3355 | assert( delta_error.empty); 3356 | assert(!foxtrot_error.empty); 3357 | assert( got_charlie == Indicator.NotSeen); 3358 | assert( got_delta == Indicator.NotSeen); 3359 | } 3360 | 3361 | auto cp10b = new CP10(nonexistent_file, nonexistent_file); 3362 | cp10b.RunWithHardOpenFailure(["--alpha", existent_file, "--bravo", existent_file]); 3363 | 3364 | with (cp10b) { 3365 | Run(["--alpha", existent_file, "--bravo", existent_file, "--echo", existent_file]); 3366 | assert( alpha.isOpen); 3367 | assert( bravo.isOpen); 3368 | assert(!charlie.isOpen); 3369 | assert(!delta.isOpen); 3370 | assert( echo.isOpen); 3371 | assert(!foxtrot.isOpen); 3372 | assert( bravo_error.empty); 3373 | assert( delta_error.empty); 3374 | assert(!foxtrot_error.empty); 3375 | assert( got_charlie == Indicator.NotSeen); 3376 | assert( got_delta == Indicator.NotSeen); 3377 | } 3378 | 3379 | with (cp10b) { 3380 | Run(["--alpha", existent_file, "--bravo", existent_file, "--echo", existent_file, "--foxtrot", existent_file]); 3381 | assert( alpha.isOpen); 3382 | assert( bravo.isOpen); 3383 | assert(!charlie.isOpen); 3384 | assert(!delta.isOpen); 3385 | assert( echo.isOpen); 3386 | assert( foxtrot.isOpen); 3387 | assert( bravo_error.empty); 3388 | assert( delta_error.empty); 3389 | assert( foxtrot_error.empty); 3390 | assert( got_charlie == Indicator.NotSeen); 3391 | assert( got_delta == Indicator.NotSeen); 3392 | } 3393 | 3394 | with (cp10a) { 3395 | Run(["--alpha", existent_file, "--bravo"]); 3396 | assert( alpha.isOpen); 3397 | assert( bravo.isOpen); 3398 | assert(!charlie.isOpen); 3399 | assert(!delta.isOpen); 3400 | assert( echo.isOpen); 3401 | assert( foxtrot.isOpen); 3402 | assert( bravo_error.empty); 3403 | assert( delta_error.empty); 3404 | assert( foxtrot_error.empty); 3405 | assert( got_charlie == Indicator.NotSeen); 3406 | assert( got_delta == Indicator.NotSeen); 3407 | assert( alpha != stdin); 3408 | assert( bravo == stdin); 3409 | } 3410 | 3411 | with (cp10a) { 3412 | Run(["--bravo", existent_file, "--alpha"]); 3413 | assert( alpha.isOpen); 3414 | assert( bravo.isOpen); 3415 | assert(!charlie.isOpen); 3416 | assert(!delta.isOpen); 3417 | assert( echo.isOpen); 3418 | assert( foxtrot.isOpen); 3419 | assert( bravo_error.empty); 3420 | assert( delta_error.empty); 3421 | assert( foxtrot_error.empty); 3422 | assert( got_charlie == Indicator.NotSeen); 3423 | assert( got_delta == Indicator.NotSeen); 3424 | assert( alpha == stdin); 3425 | assert( bravo != stdin); 3426 | } 3427 | 3428 | with (cp10a) { 3429 | Run(["--bravo", existent_file, "--alpha", existent_file, "--charlie"]); 3430 | assert( alpha.isOpen); 3431 | assert( bravo.isOpen); 3432 | assert( charlie.isOpen); 3433 | assert(!delta.isOpen); 3434 | assert( echo.isOpen); 3435 | assert( foxtrot.isOpen); 3436 | assert( bravo_error.empty); 3437 | assert( delta_error.empty); 3438 | assert( foxtrot_error.empty); 3439 | assert( got_charlie == Indicator.UsedEolDefault); 3440 | assert( got_delta == Indicator.NotSeen); 3441 | assert( charlie == stdin); 3442 | } 3443 | 3444 | with (cp10a) { 3445 | Run(["--bravo", existent_file, "--alpha", existent_file, "--delta"]); 3446 | assert( alpha.isOpen); 3447 | assert( bravo.isOpen); 3448 | assert(!charlie.isOpen); 3449 | assert( delta.isOpen); 3450 | assert( echo.isOpen); 3451 | assert( foxtrot.isOpen); 3452 | assert( bravo_error.empty); 3453 | assert( delta_error.empty); 3454 | assert( foxtrot_error.empty); 3455 | assert( got_charlie == Indicator.NotSeen); 3456 | assert( got_delta == Indicator.UsedEolDefault); 3457 | assert( delta == stdin); 3458 | } 3459 | } 3460 | 3461 | class CP11: Fields01 { 3462 | this(in string echo_default_name, in string foxtrot_default_name) { 3463 | // Mandatory with hard failure: 3464 | Pos("alpha", alpha, Mode, null); 3465 | 3466 | // Mandatory with soft failure: 3467 | Pos("bravo", bravo, Mode, &bravo_error); 3468 | 3469 | // Optional with indicator and hard failure: 3470 | Pos("charlie", charlie, Mode, got_charlie, null); 3471 | 3472 | // Optional with indicator and soft failure: 3473 | Pos("delta", delta, Mode, got_delta, &delta_error); 3474 | 3475 | // Optional with default filename and hard failure: 3476 | Pos("echo", echo, Mode, echo_default_name, null); 3477 | 3478 | // Optional with default filename and soft failure: 3479 | Pos("foxtrot", foxtrot, Mode, foxtrot_default_name, &foxtrot_error); 3480 | } 3481 | } 3482 | 3483 | @trusted auto test_positional_file_arguments() { 3484 | // @trusted because it refers to __gshared stdin, but never from 3485 | // multi-threaded code. 3486 | 3487 | auto cp11a = new CP11(existent_file, existent_file); 3488 | cp11a.ExpectSummary(" [] [] [] []"); 3489 | cp11a.FailRun("", "This command needs the alpha to be specified"); 3490 | 3491 | with (cp11a) { 3492 | Run([existent_file, existent_file]); 3493 | assert( alpha.isOpen); 3494 | assert( bravo.isOpen); 3495 | assert(!charlie.isOpen); 3496 | assert(!delta.isOpen); 3497 | assert( echo.isOpen); 3498 | assert( foxtrot.isOpen); 3499 | assert( bravo_error.empty); 3500 | assert( delta_error.empty); 3501 | assert( foxtrot_error.empty); 3502 | assert( got_charlie == Indicator.NotSeen); 3503 | assert( got_delta == Indicator.NotSeen); 3504 | } 3505 | 3506 | cp11a.RunWithHardOpenFailure([nonexistent_file, existent_file]); 3507 | 3508 | with (cp11a) { 3509 | Run([existent_file, nonexistent_file]); 3510 | assert( alpha.isOpen); 3511 | assert(!bravo.isOpen); 3512 | assert(!charlie.isOpen); 3513 | assert(!delta.isOpen); 3514 | assert( echo.isOpen); 3515 | assert( foxtrot.isOpen); 3516 | assert(!bravo_error.empty); 3517 | assert( delta_error.empty); 3518 | assert( foxtrot_error.empty); 3519 | assert( got_charlie == Indicator.NotSeen); 3520 | assert( got_delta == Indicator.NotSeen); 3521 | } 3522 | 3523 | cp11a.RunWithHardOpenFailure([existent_file, nonexistent_file, nonexistent_file]); 3524 | 3525 | with (cp11a) { 3526 | Run([existent_file, nonexistent_file, existent_file]); 3527 | assert( alpha.isOpen); 3528 | assert(!bravo.isOpen); 3529 | assert( charlie.isOpen); 3530 | assert(!delta.isOpen); 3531 | assert( echo.isOpen); 3532 | assert( foxtrot.isOpen); 3533 | assert(!bravo_error.empty); 3534 | assert( delta_error.empty); 3535 | assert( foxtrot_error.empty); 3536 | assert( got_charlie == Indicator.Seen); 3537 | assert( got_delta == Indicator.NotSeen); 3538 | } 3539 | 3540 | cp11a.RunWithHardOpenFailure([existent_file, nonexistent_file, "-", nonexistent_file]); 3541 | 3542 | with (cp11a) { 3543 | // Also tests that a bare dash is interpreted as a literal string, 3544 | // rather than the start of an option: 3545 | Run([existent_file, existent_file, "-", existent_file]); 3546 | assert( alpha.isOpen); 3547 | assert( bravo.isOpen); 3548 | assert( charlie.isOpen); 3549 | assert( delta.isOpen); 3550 | assert( echo.isOpen); 3551 | assert( foxtrot.isOpen); 3552 | assert( bravo_error.empty); 3553 | assert( delta_error.empty); 3554 | assert( foxtrot_error.empty); 3555 | assert( charlie == stdin); 3556 | assert( got_charlie == Indicator.Seen); 3557 | assert( got_delta == Indicator.Seen); 3558 | } 3559 | 3560 | cp11a.RunWithHardOpenFailure([existent_file, existent_file, "-", "-", nonexistent_file]); 3561 | 3562 | with (cp11a) { 3563 | Run([existent_file, existent_file, existent_file, existent_file, existent_file, existent_file]); 3564 | assert( alpha.isOpen); 3565 | assert( bravo.isOpen); 3566 | assert( charlie.isOpen); 3567 | assert( delta.isOpen); 3568 | assert( echo.isOpen); 3569 | assert( foxtrot.isOpen); 3570 | assert( bravo_error.empty); 3571 | assert( delta_error.empty); 3572 | assert( foxtrot_error.empty); 3573 | assert( got_charlie == Indicator.Seen); 3574 | assert( got_delta == Indicator.Seen); 3575 | } 3576 | 3577 | with (cp11a) { 3578 | Run([existent_file, existent_file, existent_file, existent_file, existent_file, nonexistent_file]); 3579 | assert( alpha.isOpen); 3580 | assert( bravo.isOpen); 3581 | assert( charlie.isOpen); 3582 | assert( delta.isOpen); 3583 | assert( echo.isOpen); 3584 | assert(!foxtrot.isOpen); 3585 | assert( bravo_error.empty); 3586 | assert( delta_error.empty); 3587 | assert(!foxtrot_error.empty); 3588 | assert( got_charlie == Indicator.Seen); 3589 | assert( got_delta == Indicator.Seen); 3590 | } 3591 | 3592 | auto cp11b = new CP11(nonexistent_file, nonexistent_file); 3593 | cp11b.RunWithHardOpenFailure([existent_file, existent_file]); 3594 | 3595 | with (cp11b) { 3596 | Run([existent_file, existent_file, existent_file, existent_file, existent_file]); 3597 | assert( alpha.isOpen); 3598 | assert( bravo.isOpen); 3599 | assert( charlie.isOpen); 3600 | assert( delta.isOpen); 3601 | assert( echo.isOpen); 3602 | assert(!foxtrot.isOpen); 3603 | assert( bravo_error.empty); 3604 | assert( delta_error.empty); 3605 | assert(!foxtrot_error.empty); 3606 | assert( got_charlie == Indicator.Seen); 3607 | assert( got_delta == Indicator.Seen); 3608 | } 3609 | 3610 | with (cp11b) { 3611 | Run([existent_file, existent_file, existent_file, existent_file, existent_file, existent_file]); 3612 | assert( alpha.isOpen); 3613 | assert( bravo.isOpen); 3614 | assert( charlie.isOpen); 3615 | assert( delta.isOpen); 3616 | assert( echo.isOpen); 3617 | assert( foxtrot.isOpen); 3618 | assert( bravo_error.empty); 3619 | assert( delta_error.empty); 3620 | assert( foxtrot_error.empty); 3621 | assert( got_charlie == Indicator.Seen); 3622 | assert( got_delta == Indicator.Seen); 3623 | } 3624 | } 3625 | 3626 | if (!existent_file.empty && !nonexistent_file.empty) { 3627 | test_named_file_arguments; 3628 | test_positional_file_arguments; 3629 | } 3630 | 3631 | // Regex-based string verification 3632 | 3633 | class Fields02(Char, Str): TestableHandler { 3634 | Str alpha, bravo; 3635 | Captures!(Char[])[] alpha_caps, bravo_caps; 3636 | 3637 | this() { 3638 | Preserve; 3639 | } 3640 | } 3641 | 3642 | class CP20(Char, Str): Fields02!(Char, Str) { 3643 | this() { 3644 | // With stored captures: 3645 | Named("alpha", alpha, "".to!Str) // Ethernet or aggregate port name: / ^ (?: eth | agg ) \d{1,3} $ /x 3646 | .AddRegex(` ^ (?P eth | agg ) (?! \p{alphabetic} ) `.to!Str, "x", "The port name must begin with 'eth' or 'agg'").Snip 3647 | .AddRegex(` ^ (?P \d{1,3} ) (?! \d ) `.to!Str, "x", "The port type ('{0:TYPE}') must be followed by one, two or three digits").Snip 3648 | .AddRegex(` ^ $ `.to!Str, "x", "The port name ('{0:TYPE}{1:NUMBER}') mustn't be followed by any other characters") 3649 | .StoreCaptures(alpha_caps); 3650 | 3651 | // Without stored captures: 3652 | Named("bravo", bravo, "".to!Str) // A person's name: a capital letter, followed by some small letters 3653 | .AddRegex(` ^ (?P \p{uppercase} ) `.to!Str, "x", "The name must start with a capital letter").Snip 3654 | .AddRegex(` ^ \p{lowercase}* $ `.to!Str, "x", "The initial, {0:INITIAL}, must be followed by nothing but small letters"); 3655 | } 3656 | } 3657 | 3658 | auto test_regexen(Char, Str) () { 3659 | with (new CP20!(Char, Str)) { 3660 | Run("--alpha eth2"); 3661 | assert(alpha_caps[0]["TYPE"] == "eth"); 3662 | assert(alpha_caps[1]["NUMBER"].to!uint == 2); 3663 | assert(alpha == "eth2"); 3664 | 3665 | Run("--alpha agg39"); 3666 | assert(alpha_caps[0]["TYPE"] == "agg"); 3667 | assert(alpha_caps[1]["NUMBER"].to!uint == 39); 3668 | assert(alpha == "agg39"); 3669 | 3670 | // Because we stored captures, we expect interpolation in error messages: 3671 | FailRun("--alpha ether", "The port name must begin with 'eth' or 'agg'"); 3672 | FailRun("--alpha arc22", "The port name must begin with 'eth' or 'agg'"); 3673 | FailRun("--alpha agg", "The port type ('agg') must be followed by one, two or three digits"); 3674 | FailRun("--alpha eth221b", "The port name ('eth221') mustn't be followed by any other characters"); 3675 | 3676 | Run("--bravo José"); 3677 | assert(bravo == "José"); 3678 | 3679 | // We didn't store captures, and so we don't expect interpolation: 3680 | FailRun("--bravo elliott", "The name must start with a capital letter"); 3681 | FailRun("--bravo O'Hanrahanrahan", "The initial, {0:INITIAL}, must be followed by nothing but small letters"); 3682 | } 3683 | } 3684 | 3685 | test_regexen!(char, string) (); 3686 | test_regexen!(wchar, wstring) (); 3687 | test_regexen!(dchar, dstring) (); 3688 | 3689 | // Prove that class Handler destroys its state if we don't call Preserve(): 3690 | @safe class Fields03: TestableHandler { 3691 | int alpha; 3692 | 3693 | this() { 3694 | Named("alpha", alpha, 0); 3695 | } 3696 | 3697 | auto Test() { 3698 | immutable command = "--alpha 43"; 3699 | Run(command); 3700 | assert(alpha == 43); 3701 | 3702 | FailRun(command, "This command has no --alpha option"); 3703 | } 3704 | } 3705 | 3706 | with (new Fields03) 3707 | Test; 3708 | } 3709 | 3710 | } // @safe 3711 | --------------------------------------------------------------------------------