1 /**
2     Handles program options which are arguments passed with a leading `-` or `--` and followed by a value
3 
4     Features:
5 
6     <li> Input validation
7     <li> Customize seperators for associative array args
8     <li> Supports environment variables
9     <li> Supports default values
10     <li> Supports custom types that have a constructor that is called with a string
11     <li> You can supply custom types and they will be called with a string that you can parse
12 
13     Enhancements_over_std.getopt:
14 
15     <li> `getopt(args)` is destructive on args.
16     <li> You cannot create your getopts and the parse later, which in combination with try/catch leads to awkward code
17     <li> `getopt` doesn't accept `$ ./program -p 3`. For short opts, you have to do `$ ./program -p3`.
18     <li> `getopt` doesn't allow case-sensitive short name and a case-insensitive long name
19     <li> `getopt` will initialize an array type with the default values AND what the program arg was.
20     <li> You cannot assign values to bundled short args, they are only incrementable
21     <li> There is no way to handle what happens with duplicate arguments
22 */
23 module dcli.program_options;
24 
25 ///
26 unittest {
27     import std.file: thisExePath;
28     import std.process: environment;
29 
30     environment["OPT_4"] = "22";
31 
32     auto args = [
33         thisExePath,            // Program name should be ignored
34         "program_name",         // Unknown argument, there's a handler for stray arguments
35         "--opt1", "value1",     // "--arg value" format
36         "-b", "1",              // "-a 1" format, case sensitive short name by default
37         "--Opt3=2",             // "--arg=value" format, case insesitive long name by default
38         // "--opt4",            // Set by an envaronment variable
39         "--OPT5", "4",          // first value of an int array
40         "--opt5", "5",          // second value of an int array
41         // "--opt6",            // Not set, will be give default value of int array
42         "--opt7", "9",          // Also an array
43         "--unknown", "ha",      // Unknown option and value
44         "--opt8=two",           // An enum vaue
45         "-i", "-j", "--incremental", "--opt9", // Option aliasing
46         "--opt10", "11",        // A validated, must be greater than 10
47         "--opt11", "3,4,5",     // Array format "--arg=v0,v1,v2"
48         "--opt12", "1=2::3=4::5=6", // Associative array with custom seperator (Default is ",")
49         "--opt13", "verbose",   // A custom parsed value - opt13 is an int
50         "-xyz=-7",              // Setting multiple, bundleable options at once
51         "--opt1", "value2",     // Uses duplication policy to be ignored
52         "--opt14", "1,2",       // Parses to a Custom type
53         "--opt15",              // Boolean, no value
54         "--",                   // Args after this are ignored
55         "extra",
56     ];
57 
58     enum Enum { one, two, }
59     static struct Custom {
60         int x;
61         int y;
62         this(int a, int b) {
63             x = a;
64             y = b;
65         }
66         this(string str) {
67             import std..string: split;
68             import std.conv: to;
69             auto parts = str.split(",");
70             x = parts[0].to!int;
71             y = parts[1].to!int;
72         }
73     }
74 
75     auto options = ProgramOptions!(
76         Option!("opt1", string)
77             .shortName!"a"
78             .description!"This is the description for option 1"
79             .duplicatePolicy!(OptionDuplicatePolicy.firstOneWins),
80         Option!("opt2", int)
81             .shortName!"b"
82             .description!"This is the description for option 2",
83         Option!("opt3", int)
84             .shortName!"B"
85             .description!(
86 `There are three kinds of comments:
87     1. Something rather sinister
88     2. And something else that's not so sinister`
89         ),
90         Option!("opt4", int)
91             .defaultValue!3
92             .environmentVar!"OPT_4"
93             .description!"THis is one that takes an env var",
94         Option!("opt5", int[])
95             .environmentVar!"OPT_5"
96             .description!"THis is one that takes an env var as well",
97         Option!("opt6", int[])
98             .defaultValue!([6, 7, 8]),
99         Option!("opt7", float[])
100             .defaultValue!([1, 2]),
101         Option!("opt8", Enum),
102         Option!("opt9", int)
103             .shortName!"i|j"
104             .longName!"incremental|opt9"
105             .incremental!true
106             .description!"sets some level incremental thingy",
107         Option!("opt10", int)
108             .validator!(a => a > 10),
109         Option!("opt11", int[]),
110         Option!("opt12", int[int])
111             .separator!"::",
112         Option!("opt13", int)
113             .parser!((value) {
114                 if (value == "verbose") return 7;
115                 return -1;
116             }),
117         Option!("b0", int)
118             .shortName!"x",
119         Option!("b1", int)
120             .shortName!"y",
121         Option!("b2", int)
122             .shortName!"z",
123         Option!("opt14", Custom),
124         Option!("opt15", bool),
125         Option!("opt16", bool)
126             .longName!""
127             .environmentVar!"OPT_16"
128             .description!"THis one only takes and envornment variable and cant be set with any flags",
129     )();
130 
131     string[] unknownArgs;
132     options.unknownArgHandler = (string name) {
133         unknownArgs ~= name;
134         return false;
135     };
136 
137     assert(options.parse(args) == ["extra"]);
138     assert(unknownArgs == ["program_name", "--unknown", "ha"]);
139 
140     assert(options.opt1 == "value1");
141     assert(options.opt2 == 1);
142     assert(options.opt3 == 2);
143     assert(options.opt4 == 22);
144     assert(options.opt5 == [4, 5]);
145     assert(options.opt6 == [6, 7, 8]);
146     assert(options.opt7 == [9]);
147     assert(options.opt8 == Enum.two);
148     assert(options.opt9 == 4);
149     assert(options.opt10 > 10);
150     assert(options.opt11 == [3, 4, 5]);
151     assert(options.opt12 == [1: 2, 3: 4, 5: 6]);
152     assert(options.opt13 == 7);
153     assert(options.b0 == -7);
154     assert(options.b1 == -7);
155     assert(options.b2 == -7);
156     assert(options.opt14 == Custom(1, 2));
157     assert(options.opt15 == true);
158 
159     assert(options.helpText ==
160 `Options:
161   -a  --opt1          This is the description for option 1
162   -b  --opt2          This is the description for option 2
163   -B  --opt3          There are three kinds of comments:
164                           1. Something rather sinister
165                           2. And something else that's not so sinister
166       --opt4          THis is one that takes an env var
167       --opt5          THis is one that takes an env var as well
168       --opt6
169       --opt7
170       --opt8
171   -i  --incremental   sets some level incremental thingy
172       --opt10
173       --opt11
174       --opt12
175       --opt13
176   -x  --b0
177   -y  --b1
178   -z  --b2
179       --opt14
180       --opt15
181 
182 Environment Vars:
183   OPT_4    See: --opt4
184   OPT_5    See: --opt5
185   OPT_16   THis one only takes and envornment variable and cant be set with any flags`
186   );
187 }
188 
189 // version = dcli_programoptions_debug;
190 
191 private void debug_print(Args...)(Args args, int line = __LINE__, string file = __FILE__) @trusted @nogc {
192     version(dcli_programoptions_debug) {
193         debug {
194             import std.stdio: writeln;
195             import std.path: baseName, stripExtension, dirName, pathSplitter;
196             import std.range: array;
197             auto stripped = baseName(stripExtension(file));
198             if (stripped == "package") {
199                 stripped = pathSplitter(dirName(file)).array[$ - 1] ~ "/" ~ stripped;
200             }
201             writeln("[", stripped, ":", line, "] : ", args);
202         }
203     }
204 }
205 
206 /**
207     The duplication policy can be passed to a `ProgramOptions` `Option.duplicatePolicy`
208 */
209 enum OptionDuplicatePolicy {
210     /// Throws a `DuplicateProgramArgument`
211     reject,
212     /// The last argument determines the value of the option
213     lastOneWins,
214     /// The first argument determines the value of the option
215     firstOneWins
216 }
217 
218 /**
219     Represents one program options. One of more of these can be given to
220     a `ProgramOptions` object as template arguments.
221 
222     Params:
223         varName = The name of the variable that will hold the value of the option.
224             This value represents the default long name of the program option.
225         T = The type of this variable
226 
227     Named_optional_arguments:
228 
229     A number of named optional arguments can be given to an `Option` for e.g.:
230     ---
231     ProgramOptions!(
232         Option!("optionName", bool).shortName!"o";
233     );
234     ---
235 
236     This will create an options object that can parse `--optionName` and `-o` on the command line.
237 
238     The following named optional arguments are available:
239 
240     <li>`shortName`: `string` - the short name for the option
241     <li>`longName`: `string` - the long name of the option if different from the variable name
242     <li>`defaultValue`: `T` - the default value if not supplied
243     <li>`description`: `string` - description for help message
244     <li>`environmentVar`: `string` - the name of the envrionemtn var that can set this option if present
245     <li>`caseSensitiveLongName`: `bool` - true if long name is case sensitive
246     <li>`caseSensitiveShortName`: `bool` - true if short name is case sensitive
247     <li>`incremental`: `bool` - true if this option represents an incremental value
248     <li>`validator`: `bool function(T)` - a function that will be given the value and must return true if the value is valid
249     <li>`seperator`: `string` - the seperator used to parse associative array values
250     <li>`parser`: `T function(string)` - a function that must return a T after parsing the string
251     <li>`bundleable`: `bool` - true if this short option can be bundled with other short options
252     <li>`duplicatePolicy`: `OptionDuplicatePolicy` - what to do when the option is encoutered for a second time
253 */
254 template Option(string varName, T) {
255     alias Option = OptionImpl!(varName, T);
256 }
257 
258 private template OptionImpl(
259     string _varName,
260     T,
261     string _longName = _varName,
262     string _shortName = null,
263     T _defaultValue = T.init,
264     string _description = null,
265     string _environmentVar = null,
266     bool _caseSensitiveLongName = false,
267     bool _caseSensitiveShortName = true,
268     bool _incremental = false,
269     alias _validator = null,
270     string _separator = ",",
271     alias _parser = null,
272     bool _bundleable = true,
273     OptionDuplicatePolicy _duplicatePolicy = OptionDuplicatePolicy.reject,
274 ) if (_varName.length > 0) {
275 
276     import std.typecons: Flag;
277     import bolts: isUnaryOver;
278     import std.traits: ReturnType;
279     import ddash.range: frontOr;
280 
281     alias VarName = _varName;
282     alias Type = T;
283     alias LongName = _longName;
284     alias ShortName = _shortName;
285     alias DefaultValue = _defaultValue;
286     alias Description = _description;
287     alias EnvironmentVar = _environmentVar;
288     alias CaseSensitiveLongName = _caseSensitiveLongName;
289     alias CaseSensitiveShortName = _caseSensitiveShortName;
290     alias Incremental = _incremental;
291     alias Validator = _validator;
292     alias Separator = _separator;
293     alias Parser = _parser;
294     alias Bundleable = _bundleable;
295     alias DuplicatePolicy = _duplicatePolicy;
296 
297     static if (_incremental) {
298         import std.traits: isIntegral;
299         static assert(
300             isIntegral!T,
301             "Cannot create incremental option '" ~ VarName ~ "' of type " ~ T.stringof ~ ". Incrementals must by integral type."
302         );
303     }
304 
305     import std.array: split;
306     private immutable NormalizedName = (LongName ~ "|" ~ ShortName ~ "|" ~ VarName).split("|").frontOr("");
307     private immutable PrimaryLongName = LongName.split("|").frontOr("");
308     private immutable PrimaryShortName = ShortName.split("|").frontOr("");
309 
310     public alias longName(string value) = OptionImpl!(
311         VarName, Type, value, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
312         CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
313     );
314 
315     public alias shortName(string value) = OptionImpl!(
316         VarName, Type, LongName, value, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
317         CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
318     );
319 
320     public alias defaultValue(T value) = OptionImpl!(
321         VarName, Type, LongName, ShortName, value, Description, EnvironmentVar, CaseSensitiveLongName,
322         CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
323     );
324 
325     public template description(string value) if (value.length > 0) {
326         alias description = OptionImpl!(
327             VarName, Type, LongName, ShortName, DefaultValue, value, EnvironmentVar, CaseSensitiveLongName,
328             CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
329         );
330     }
331 
332     public template environmentVar(string value) if (value.length > 0)  {
333         alias environmentVar = OptionImpl!(
334             VarName, Type, LongName, ShortName, DefaultValue, Description, value, CaseSensitiveLongName,
335             CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
336         );
337     }
338 
339     public alias caseSensitiveLongName(bool value) = OptionImpl!(
340         VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, value,
341         CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
342     );
343 
344     public alias caseSensitiveShortName(bool value) = OptionImpl!(
345         VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
346         value, Incremental, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
347     );
348 
349     public template incremental(bool value) {
350         alias incremental = OptionImpl!(
351             VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
352             CaseSensitiveShortName, value, Validator, Separator, Parser, Bundleable, DuplicatePolicy,
353         );
354     }
355 
356     public static template validator(alias value) if (isUnaryOver!(value, T)) {
357         import std.functional: unaryFun;
358         alias validator = OptionImpl!(
359             VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
360             CaseSensitiveShortName, Incremental, unaryFun!value, Separator, Parser, Bundleable, DuplicatePolicy,
361         );
362     }
363 
364     public template separator(string value) if (value.length > 0) {
365         alias separator = OptionImpl!(
366             VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
367             CaseSensitiveShortName, Incremental, Validator, value, Parser, Bundleable, DuplicatePolicy,
368         );
369     }
370 
371     public static template parser(alias value) {
372         alias parser = OptionImpl!(
373             VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
374             CaseSensitiveShortName, Incremental, Validator, Separator, value, Bundleable, DuplicatePolicy,
375         );
376     }
377 
378     public template bundleable(bool value) {
379         alias bundleable = OptionImpl!(
380             VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
381             CaseSensitiveShortName, Incremental, Validator, Separator, Parser, value, DuplicatePolicy,
382         );
383     }
384 
385     public template duplicatePolicy(OptionDuplicatePolicy value) {
386         alias duplicatePolicy = OptionImpl!(
387             VarName, Type, LongName, ShortName, DefaultValue, Description, EnvironmentVar, CaseSensitiveLongName,
388             CaseSensitiveShortName, Incremental, Validator, Separator, Parser, Bundleable, value,
389         );
390     }
391 
392     private bool isMatch(string name, Flag!"shortName" isShortName) {
393         import std.uni: toLower;
394         import std.array: split;
395         import std.algorithm: any;
396         if (isShortName) {
397             static if (CaseSensitiveShortName) {
398                 auto lhs = ShortName;
399                 auto rhs = name;
400             } else {
401                 auto lhs = ShortName.toLower;
402                 auto rhs = name.toLower;
403             }
404             return lhs.split("|").any!(a => a == rhs);
405         } else {
406             static if (CaseSensitiveLongName) {
407                 auto lhs = LongName;
408                 auto rhs = name;
409             } else {
410                 auto lhs = LongName.toLower;
411                 auto rhs = name.toLower;
412             }
413             return lhs.split("|").any!(a => a == rhs);
414         }
415     }
416 }
417 
418 /// Thrown when an error occurs in parsing
419 class ProgramOptionException: Exception {
420     this(string msg, string file = __FILE__, size_t line = __LINE__) {
421         super(msg, file, line);
422     }
423 }
424 
425 /// Occurs when an argument is incorrectly formatted
426 class MalformedProgramArgument: ProgramOptionException {
427     this(string msg, string file = __FILE__, size_t line = __LINE__) {
428         super(msg, file, line);
429     }
430 }
431 
432 /// Occurs when an argument does not validate
433 class InvalidProgramArgument: ProgramOptionException {
434     this(string msg, string file = __FILE__, size_t line = __LINE__) {
435         super(msg, file, line);
436     }
437 }
438 
439 /// Occurs if there's a duplicate argument
440 class DuplicateProgramArgument: ProgramOptionException {
441     this(string msg, string file = __FILE__, size_t line = __LINE__) {
442         super(msg, file, line);
443     }
444 }
445 
446 /// Occurs when a program argument is missing
447 class MissingProgramArgument: ProgramOptionException {
448     this(string msg, string file = __FILE__, size_t line = __LINE__) {
449         super(msg, file, line);
450     }
451 }
452 
453 package(dcli) template isProgramOptions(T) {
454     import std.traits: isInstanceOf;
455     enum isProgramOptions = isInstanceOf!(ProgramOptions, T);
456 }
457 
458 /**
459     You can configure a `ProgramOptions` object with a number of `Option`s and then use it to parse
460     and array of command line arguments.
461 
462     The object will generate its member variables from the `Option`s you pass in, for e.g.
463 
464     ---
465     auto opts = ProgramOptions!(Option!("one", int), Option!("two", string[]));
466     static assert(is(typeof(opts.one) == int));
467     static assert(is(typeof(opts.two) == string[]));
468     ---
469 
470     All the member varibles will have their `init` values unless you specify a `defaultValue` for
471     an `Option`.
472 
473     Params:
474         Options = 1 or more `Option` objects, each representing one command line argument
475 */
476 struct ProgramOptions(Options...) if (Options.length > 0) {
477 
478     import std.typecons: Flag, Tuple, tuple, Yes, No;
479     import optional;
480 
481     static foreach (I, opt; Options) {
482         mixin("opt.Type " ~ opt.VarName ~ "= opt.DefaultValue;");
483     }
484 
485     private bool[string] encounteredOptions;
486 
487     Optional!(bool delegate(string)) unknownArgHandler;
488 
489     private enum AssignResult {
490         setValueUsed,
491         setValueUnused,
492         noMatch
493     }
494 
495     /*
496         Why is this static you may ask... because of D's 'delegates can only have one context pointer' thing:
497 
498         https://www.bountysource.com/issues/1375082-cannot-use-delegates-as-parameters-to-non-global-template
499 
500         The error would be:
501             Error: template instance `tryAssign!()` cannot use local Option!(..., __lambda2)
502             as parameter to non-global template tryAssign(alias option)(string assumedValue)
503 
504         So making this a static removes the need for an extra context pointer
505     */
506     private static AssignResult tryAssign(alias option, T)(
507         ref T self,
508         string assumedValue,
509         Flag!"ignoreDuplicate" ignoreDuplicate = No.ignoreDuplicate,
510     ) {
511         import std.traits: isNarrowString, isArray, isAssociativeArray;
512         import std.conv: to;
513         import std.array: split;
514         import std.algorithm: filter;
515 
516         // If this is the first encounter, we also set the value to T.init
517         bool isDuplicate = false;
518         if (!ignoreDuplicate) {
519             auto p = option.VarName in self.encounteredOptions;
520             if (p !is null && !*p) {
521                 *p = true;
522                 mixin("self."~option.VarName ~ " = option.Type.init;");
523             } else {
524                 isDuplicate = true;
525             }
526         }
527 
528         static if (!(isArray!(option.Type) && !isNarrowString!(option.Type)) && !isAssociativeArray!(option.Type) && !option.Incremental) {
529             static if (option.DuplicatePolicy == OptionDuplicatePolicy.firstOneWins) {
530                 if (isDuplicate) {
531                     // Flags are special. They may or may not consume the value depending on if consuming
532                     // the value succeeds or not
533                     static if (is(option.Type == bool)) {
534                         try {
535                             assumedValue.to!bool;
536                         } catch (Exception) {
537                             return AssignResult.setValueUnused;
538                         }
539                     }
540                     return AssignResult.setValueUsed;
541                 }
542             }
543 
544             static if (option.DuplicatePolicy == OptionDuplicatePolicy.reject) {
545                 if (isDuplicate) {
546                     throw new DuplicateProgramArgument(
547                         "Duplicate program option argument '" ~ assumedValue ~ "' not allowed for option '" ~ option.NormalizedName ~ "'"
548                     );
549                 }
550             }
551         }
552 
553         auto tryParseValidate(T = option.Type)(string assumedValue) {
554             import bolts: isNullType;
555             T value;
556             static if (!isNullType!(option.Parser)) {
557                 value = option.Parser(assumedValue);
558             } else {
559                 static assert(
560                     __traits(compiles, { assumedValue.to!(T); }),
561                     "Cannot convert program arg to type '" ~ T.stringof ~ "'. Maybe type is missing a this(string) contructor?"
562                 );
563                 value = assumedValue.to!(T);
564             }
565             static if (!isNullType!(option.Validator)) {
566                 if (!option.Validator(value)) {
567                     throw new InvalidProgramArgument("Option '" ~ option.NormalizedName ~ "' got invalid value: " ~ assumedValue);
568                 }
569             }
570             return value;
571         }
572 
573         AssignResult result = AssignResult.setValueUsed;
574         static if (option.Incremental) {
575             mixin("self."~option.VarName ~ " += 1;");
576             result = AssignResult.setValueUnused;
577         } else static if (is(option.Type == bool)) {
578             mixin("self."~option.VarName ~ " = true;");
579             try {
580                 mixin("self."~option.VarName ~ " = " ~ "assumedValue.to!bool;");
581             } catch (Exception) {
582                 result = AssignResult.setValueUnused;
583             }
584         } else static if (isArray!(option.Type) && !isNarrowString!(option.Type)) {
585             import std.range: ElementType;
586             auto values = assumedValue.split(option.Separator).filter!"a.length";
587             foreach (str; values) {
588                 auto value = tryParseValidate!(ElementType!(option.Type))(str);
589                 mixin("self."~option.VarName ~ " ~= value;");
590             }
591         } else static if (isAssociativeArray!(option.Type)) {
592             auto values = assumedValue.split(option.Separator).filter!"a.length";
593             foreach (str; values) {
594                 auto parts = str.split("=");
595                 if (!parts.length == 2) {
596                     return AssignResult.noMatch;
597                 }
598                 import std.traits: KeyType, ValueType;
599                 auto key = parts[0].to!(KeyType!(option.Type));
600                 auto value = parts[1].to!(ValueType!(option.Type));
601                 mixin("self."~option.VarName ~ "[key] = value;");
602             }
603         } else {
604             import bolts: isNullType;
605             auto value = tryParseValidate(assumedValue);
606             mixin("self."~option.VarName ~ " = value;");
607         }
608         return result;
609     }
610 
611     private AssignResult trySet(string name, string value, Flag!"shortName" shortName) {
612         import std.conv: ConvException;
613         try {
614             static foreach (opt; Options) {
615                 if (opt.isMatch(name, shortName)) {
616                     return tryAssign!(opt)(this, value);
617                 }
618             }
619         } catch (ConvException ex) {
620             throw new MalformedProgramArgument(
621                 "Could not set '" ~ name ~ "' to '" ~ value ~ "' - " ~ ex.msg
622             );
623         }
624         return AssignResult.noMatch;
625     }
626 
627     private bool isBundleableShortName(string str) {
628         import std.typecons: Yes;
629         static foreach (opt; Options) {
630             if (opt.isMatch(str, Yes.shortName)) {
631                 return opt.Bundleable;
632             }
633         }
634         return false;
635     }
636 
637     private bool isShortName(string str) {
638         import std.typecons: Yes;
639         static foreach (opt; Options) {
640             if (opt.isMatch(str, Yes.shortName)) {
641                 return true;
642             }
643         }
644         return false;
645     }
646 
647     /**
648         Parses the command line arguments according to the set of `Option`s that are passed in.
649         Environment variables are parsed first and then program args second
650     */
651     public string[] parse(const string[] args) {
652         this.parseEnv;
653         return this.parseArgs(args);
654     }
655 
656     private void parseEnv() {
657         import std.process: environment;
658         import std.conv: ConvException;
659 
660         // Parse possible environment vars
661         static foreach (opt; Options) {
662             static if (opt.EnvironmentVar.length) {{
663                 string value;
664                 try {
665                     value = environment[opt.EnvironmentVar];
666                 } catch (Exception ex) {
667                     // Env var doesn't exist
668                 }
669                 if (value.length) {
670                     try {
671                         tryAssign!(opt)(this, value, Yes.ignoreDuplicate);
672                     } catch (ConvException ex) {
673                         throw new MalformedProgramArgument(
674                             "Could not set '" ~ opt.EnvironmentVar  ~ "' to '" ~ value ~ "' - " ~ ex.msg
675                         );
676                     }
677                 }
678             }}
679         }
680     }
681 
682     private string[] parseArgs(const string[] args) {
683         import std.algorithm: startsWith, findSplit, filter, map;
684         import std.range: drop, array, tee, take;
685         import std.conv: to;
686         import std.typecons: Yes, No;
687         import std..string: split;
688 
689         // Initialize the encounters array
690         static foreach (opt; Options) {
691             encounteredOptions[opt.VarName] = false;
692         }
693 
694         // Parse the first arg and executable path and see if we should skip first arg
695         import ddash.range: first, last, frontOr, withFront;
696         import ddash.algorithm: flatMap;
697         import std.file: thisExePath;
698 
699         int index = 0;
700 
701         args.first.map!(a => a.split("/").last).withFront!((a) {
702             import std.file: thisExePath;
703             thisExePath.split("/").last.withFront!((b) {
704                 if (a == b) {
705                     index = 1;
706                 }
707             });
708         });
709 
710         while (index < args.length) {
711             auto arg = args[index];
712             debug_print("parsing arg ", arg);
713             bool shortOption = false;
714             string rest = "";
715             if (arg.startsWith("--")) {
716                 rest = arg.drop(2);
717                 shortOption = false;
718             } else if (arg.startsWith("-")) {
719                 rest = arg.drop(1);
720                 shortOption = true;
721             } else {
722                 unknownArgHandler(arg).filter!"a".tee!((a) {
723                     throw new MalformedProgramArgument(
724                         "Unknown argument '" ~ arg ~ "'"
725                     );
726                 }).array;
727                 index++;
728                 continue;
729             }
730 
731             // Make sure we have something in rest before we continue
732             if (rest.length == 0) {
733                 debug_print("no option found") ;
734                 if (shortOption) {
735                     index += 1;
736                     continue;
737                 }
738                 // Else it was a -- so we slice those out and we're done
739                 debug_print("done. storing rest arguments: ", args[index + 1 .. $]) ;
740                 return args[index + 1 .. $].dup;
741             }
742 
743             auto nextValue() {
744                 import ddash.range: nth;
745                 auto next = args.nth(index + 1);
746                 if (next.empty) {
747                     throw new MissingProgramArgument("Did not find value for arg " ~ args[index]);
748                 }
749                 return next.front;
750             }
751 
752             assert(rest.length > 0);
753 
754             string[] options;
755             string value;
756             int steps = () {
757                 if (shortOption) {
758                     debug_print("parsing short name") ;
759                     if (rest.length == 1) {
760                         debug_print("one letter option only") ;
761                         options = [rest.to!string];
762                         value = nextValue();
763                         return 2;
764                     }
765 
766                     // If we have more than one char then it can be either of the forms
767                     //
768                     // a) ooo - bundleable options no value
769                     // b) o=V - one option and assigned value
770                     // c) oV - one option and stuck value
771                     // d) ooo=V - bundleable options and assigned value
772                     // e) oooV - bundleable options and stuck value
773 
774                     assert(rest.length > 1);
775 
776                     import std.algorithm: until;
777                     auto bundleableArgs = rest.until!(a => !isBundleableShortName(a.to!string)).array;
778 
779                     debug_print("bundleableArgs=", bundleableArgs) ;
780 
781                     // Greater than one, and all of them are bundleable, and no value - case a
782                     if (bundleableArgs.length == rest.length) {
783                         debug_print("case a") ;
784                         options = bundleableArgs.map!(to!string).array;
785                         value = nextValue();
786                         return 2;
787                     }
788 
789                     auto shortNames = rest.until!(a => !isShortName(a.to!string)).array;
790 
791                     debug_print("shortNames=", shortNames) ;
792 
793                     // Greater than one, but only one valid short name - case b and c
794                     if (shortNames.length == 1) {
795                         options = shortNames.map!(to!string).array;
796                         auto parts = rest.findSplit("=");
797                         debug_print("got parts=", parts) ;
798                         if (parts[0].length == 1 && parts[1] == "=") { // case b
799                             debug_print("case b") ;
800                             value = parts[2];
801                         } else {
802                             debug_print("case c") ;
803                             value = parts[0].drop(1);
804                         }
805                         return 1;
806                     }
807 
808                     // We have more than one short name, so now we have bundleables
809                     assert(shortNames.length > 1);
810 
811                     if (shortNames.length != bundleableArgs.length) {
812                         throw new MalformedProgramArgument(
813                             "Bundled args '" ~ shortNames.to!string ~ "' are not all bundleable"
814                         );
815                     }
816 
817                     assert(shortNames.length == bundleableArgs.length);
818 
819                     // Hanle case d and e
820                     options = shortNames.map!(to!string).array;
821                     auto parts = rest.findSplit("=");
822                     debug_print("got parts=", parts) ;
823                     if (parts[0].length == shortNames.length && parts[1] == "=") { // case d
824                         debug_print("case d") ;
825                         value = parts[2];
826                     } else { // case e
827                         debug_print("case e") ;
828                         value = parts[0].drop(shortNames.length);
829                     }
830                     return 1;
831                 } else {
832                     debug_print("parsing long name") ;
833 
834                     // We have a long name, and two cases:
835                     //
836                     // a) name
837                     // b) name=V
838 
839                     auto parts = rest.findSplit("=");
840                     debug_print("got parts=", parts) ;
841                     if (parts[1] != "=") { // case a
842                         debug_print("case a") ;
843                         options = [parts[0]];
844                         value = nextValue();
845                         return 2;
846                     }
847 
848                     // case b
849                     options = [parts[0]];
850                     value = parts[2];
851                     debug_print("case b") ;
852                     return 1;
853                 }
854             }();
855 
856             debug_print("parsed => ", options, ", value: ", value, ", steps: ", steps) ;
857 
858             loop: foreach (option; options) {
859 
860                 auto result = trySet(option, value, shortOption ? Yes.shortName : No.shortName);
861 
862                 with (AssignResult) final switch (result) {
863                 case setValueUsed:
864                     break;
865                 case setValueUnused:
866                     steps = 1;
867                     break;
868                 case noMatch:
869                     unknownArgHandler(arg).filter!"a".tee!((a) {
870                         throw new MalformedProgramArgument(
871                             "Unknown argument '" ~ arg ~ "'"
872                         );
873                     }).array;
874                     steps = 1;
875                     break loop;
876                 }
877             }
878 
879             index += steps;
880         }
881 
882         return [];
883     }
884 
885     /**
886         Returns an string that is a map the variable names and their values
887     */
888     public string toString() const {
889         import std.conv: to;
890         string ret = "{ ";
891         static foreach (I, Opt; Options) {
892             ret ~= Opt.VarName ~ ": " ~ mixin(Opt.VarName ~ ".to!string");
893             static if (I < Options.length - 1) {
894                 ret ~= ", ";
895             }
896         }
897         ret ~= " }";
898         return ret;
899     }
900 
901     /**
902         Returns a string that represents a block of text that can be output to stdout
903         to display a help message
904     */
905     public string helpText() const {
906         import std..string: leftJustify, stripRight;
907         import std.typecons: Tuple;
908 
909         string ret;
910 
911         // The max lengths will be used to indent the description text
912         size_t maxLongNameLength = 0;
913         size_t maxEnvVarLength = 0;
914         static foreach (Opt; Options) {
915             static if (Opt.Description) {
916                 if (Opt.PrimaryLongName.length > maxLongNameLength) {
917                     maxLongNameLength = Opt.PrimaryLongName.length;
918                 }
919                 static if (Opt.EnvironmentVar.length) {
920                     if (Opt.EnvironmentVar.length > maxEnvVarLength) {
921                         maxEnvVarLength = Opt.EnvironmentVar.length;
922                     }
923                 }
924             }
925         }
926 
927         maxLongNameLength += 2; // for the --
928         immutable maxShortNameLength = 2;
929 
930         immutable startIndent = 2;
931         immutable shortLongIndent = 2;
932         immutable longDescIndent = 3;
933 
934         alias EnvVarData = Tuple!(string, "name", string, "linkedOption", string, "description");
935         EnvVarData[] envVarData;
936 
937         bool hasOptionsSection = false;
938         static foreach (I, Opt; Options) {{
939 
940             // This will be used later to write out the Environment Var section
941             EnvVarData envVarDatum;
942             envVarDatum.description = Opt.Description;
943 
944             // If we have a long name or short name we can display an "Options" section
945             static if (Opt.PrimaryShortName.length || Opt.PrimaryLongName.length) {
946 
947                 // Set this to null because we will output the description as part of the opttion names
948                 envVarDatum.description = null;
949 
950                 // Write the Option section header once.
951                 if (!hasOptionsSection) {
952                     ret ~= "Options:";
953                     hasOptionsSection = true;
954                 }
955 
956                 ret ~= "\n";
957 
958                 string desc = startIndent.spaces;
959                 static if (Opt.PrimaryShortName.length) {{
960                     auto str = "-" ~ Opt.PrimaryShortName;
961                     envVarDatum.linkedOption = str;
962                     desc ~= str;
963                 }} else {
964                     desc ~= maxShortNameLength.spaces;
965                 }
966                 desc ~= shortLongIndent.spaces;
967                 static if (Opt.PrimaryLongName.length) {{
968                     auto str = "--" ~ Opt.PrimaryLongName;
969                     envVarDatum.linkedOption = str;
970                     desc ~= str.leftJustify(maxLongNameLength, ' ');
971                 }} else {
972                     desc ~= maxLongNameLength.spaces;
973                 }
974                 desc ~= longDescIndent.spaces;
975                 auto indent = startIndent + maxShortNameLength + shortLongIndent + maxLongNameLength + longDescIndent;
976                 if (!Opt.Description.length) {
977                     desc = desc.stripRight;
978                 }
979                 ret ~= desc ~ printWithIndent(indent, Opt.Description);
980             }
981 
982             static if (Opt.EnvironmentVar.length) {
983                 envVarDatum.name = Opt.EnvironmentVar;
984                 envVarData ~= envVarDatum;
985             }
986         }}
987 
988         foreach (i, data; envVarData) {
989             // If first iteration, write out section header and add new lines if we had some text before (means we had an Options section)
990             if (i == 0) {
991                 if (ret.length) {
992                     ret ~= "\n\n";
993                 }
994                 ret ~= "Environment Vars:";
995             }
996 
997             ret ~= "\n";
998             ret ~= startIndent.spaces ~ data.name.leftJustify(maxEnvVarLength) ~ longDescIndent.spaces;
999             if (data.description.length) {
1000                 ret ~= printWithIndent(startIndent + maxEnvVarLength + longDescIndent, data.description);
1001             } else {
1002                 ret ~= "See: " ~ data.linkedOption;
1003             }
1004         }
1005 
1006         return ret;
1007     }
1008 }
1009 
1010 private string spaces(size_t i) pure nothrow {
1011     import std.range: repeat, take, array;
1012     return ' '.repeat.take(i).array;
1013 }
1014 
1015 private string printWithIndent(size_t indent, string description, int maxColumnCount = 80) {
1016     import std..string: splitLines;
1017     import std.algorithm: splitter;
1018     import std.uni: isWhite;
1019     import std.range: array;
1020     string ret;
1021     auto currentWidth = indent;
1022     auto lines = description.splitLines;
1023     foreach(li, line; lines) {
1024         auto words = line.splitter!isWhite.array;
1025         foreach (wi, word; words) {
1026             ret ~= word ~ (wi < words.length - 1 ? 1 : 0).spaces;
1027             currentWidth += word.length;
1028             if (currentWidth > maxColumnCount) {
1029                 ret ~= "\n" ~ indent.spaces;
1030                 currentWidth = indent;
1031             }
1032         }
1033         if (li < lines.length - 1) {
1034             ret ~= "\n";
1035             ret ~= indent.spaces;
1036             currentWidth = indent;
1037         }
1038     }
1039     return ret;
1040 }
1041 
1042 unittest {
1043     import std.exception;
1044     auto opts = ProgramOptions!(
1045         Option!("opt", string).shortName!"o"
1046     )();
1047     assertThrown!MissingProgramArgument(opts.parse(["-o"]));
1048 }