From d602a5347a9f554a442a9ab6bb9d5efe3d7ec40b Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Fri, 28 Mar 2014 02:11:41 +0100 Subject: [PATCH] getoptx some tinkering alot more comments some more more doc getoptX cleanup tuple -> struct getoptX -> getoptEx Required getoptEx is now inline GetoptRslt -> GetoptResult more info some comment fixes --- std/getopt.d | 341 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 327 insertions(+), 14 deletions(-) diff --git a/std/getopt.d b/std/getopt.d index 7e1d2d909..bba810e90 100644 --- a/std/getopt.d +++ b/std/getopt.d @@ -33,7 +33,7 @@ Distributed under the Boost Software License, Version 1.0. module std.getopt; private import std.array, std.string, std.conv, std.traits, std.bitmanip, - std.algorithm, std.ascii, std.exception; + std.algorithm, std.ascii, std.exception, std.typetuple, std.typecons; version (unittest) { @@ -70,13 +70,19 @@ Color color; void main(string[] args) { - getopt( + auto helpInformation = getopt( args, "length", &length, // numeric "file", &data, // string "verbose", &verbose, // flag - "color", &color); // enum + "color", "Information about this color", &color); // enum ... + + if (helpInformation.helpWanted) + { + defaultGetoptPrinter("Some information about the program.", + helpInformation.options); + } } --------- @@ -360,6 +366,22 @@ getopt(args, In case you want to only enable bundling for some of the parameters, bundling can be turned off with $(D std.getopt.config.noBundling). +$(B Required) + +An option can be marked as required. If that option is not present in the +arguments an exceptin will be thrown. + +--------- +bool foo, bar; +getopt(args, + std.getopt.config.required, + "foo|f", &foo, + "bar|b", &bar); +--------- + +Only the option direclty following $(D std.getopt.config.required) is +required. + $(B Passing unrecognized options through) If an application needs to do its own processing of whichever arguments @@ -377,6 +399,14 @@ getopt(args, An unrecognized option such as "--baz" will be found untouched in $(D args) after $(D getopt) returns. +$(D Help Information Generation) + +If an option string is followed by another string, this string serves as an +description for this option. The function $(D getopt) returns a struct of type +$(D GetoptResult). This return value contains information about all passed options +as well a bool indicating if information about these options where required by +the passed arguments. + $(B Options Terminator) A lonesome double-dash terminates $(D getopt) gathering. It is used to @@ -385,11 +415,32 @@ to another program). Invoking the example above with $(D "--foo -- --bar") parses foo but leaves "--bar" in $(D args). The double-dash itself is removed from the argument array. */ -void getopt(T...)(ref string[] args, T opts) { +GetoptResult getopt(T...)(ref string[] args, T opts) { enforce(args.length, "Invalid arguments string passed: program name missing"); configuration cfg; - return getoptImpl(args, cfg, opts); + GetoptResult rslt; + + getoptImpl(args, cfg, rslt, opts); + + return rslt; +} + +/// +unittest +{ + auto args = ["prog", "--foo", "-b"]; + + bool foo; + bool bar; + auto rslt = getopt(args, "foo|f" "Some information about foo.", &foo, "bar|b", + "Some help message about bar.", &bar); + + if (rslt.helpWanted) + { + defaultGetoptPrinter("Some information about the program.", + rslt.options); + } } /** @@ -413,10 +464,52 @@ enum config { noPassThrough, /// Stop at first argument that does not look like an option stopOnFirstNonOption, + /// Makes the next option a required option + required } -private void getoptImpl(T...)(ref string[] args, - ref configuration cfg, T opts) +/** The result of the $(D getoptX) function. + +The $(D GetOptDRslt) contains two members. The first member is a boolean with +the name $(D helpWanted). The second member is an array of $(D Option). The +array is accessable by the name $(D options). +*/ +struct GetoptResult { + bool helpWanted; /// Flag indicating if help was requested + Option[] options; /// All possible options +} + +/** The result of the $(D getoptHelp) function. +*/ +struct Option { + string optShort; /// The short symbol for this option + string optLong; /// The long symbol for this option + string help; /// The description of this option + bool required; /// If a option is required, not passing it will result in + /// an error. +} + +pure Option splitAndGet(string opt) @trusted nothrow +{ + auto sp = split(opt, "|"); + Option ret; + if (sp.length > 1) + { + ret.optShort = "-" ~ (sp[0].length < sp[1].length ? + sp[0] : sp[1]); + ret.optLong = "--" ~ (sp[0].length > sp[1].length ? + sp[0] : sp[1]); + } + else + { + ret.optLong = "--" ~ sp[0]; + } + + return ret; +} + +private void getoptImpl(T...)(ref string[] args, ref configuration cfg, + ref GetoptResult rslt, T opts) { static if (opts.length) { @@ -424,13 +517,29 @@ private void getoptImpl(T...)(ref string[] args, { // it's a configuration flag, act on it setConfig(cfg, opts[0]); - return getoptImpl(args, cfg, opts[1 .. $]); + return getoptImpl(args, cfg, rslt, opts[1 .. $]); } else { // it's an option string auto option = to!string(opts[0]); - auto receiver = opts[1]; + Option optionHelp = splitAndGet(option); + optionHelp.required = cfg.required; + + static if (is(typeof(opts[1]) : string)) + { + auto receiver = opts[2]; + optionHelp.help = opts[1]; + immutable lowSliceIdx = 3; + } + else + { + auto receiver = opts[1]; + immutable lowSliceIdx = 2; + } + + rslt.options ~= optionHelp; + bool incremental; // Handle options of the form --blah+ if (option.length && option[$ - 1] == autoIncrementChar) @@ -438,8 +547,17 @@ private void getoptImpl(T...)(ref string[] args, option = option[0 .. $ - 1]; incremental = true; } - handleOption(option, receiver, args, cfg, incremental); - return getoptImpl(args, cfg, opts[2 .. $]); + + bool optWasHandled = handleOption(option, receiver, args, cfg, incremental); + + if (cfg.required && !optWasHandled) + { + throw new GetOptException("Required option " ~ option ~ + "was not supplied"); + } + cfg.required = false; + + return getoptImpl(args, cfg, rslt, opts[lowSliceIdx .. $]); } } else @@ -462,14 +580,28 @@ private void getoptImpl(T...)(ref string[] args, { throw new GetOptException("Unrecognized option "~a); } + + if (a == "--help" || a == "-h") + { + rslt.helpWanted = true; + + args = args.remove(i + 1); + } } + + Option helpOpt; + helpOpt.optShort = "-h"; + helpOpt.optLong = "--help"; + helpOpt.help = "This help information."; + rslt.options ~= helpOpt; } } -void handleOption(R)(string option, R receiver, ref string[] args, +bool handleOption(R)(string option, R receiver, ref string[] args, ref configuration cfg, bool incremental) { // Scan arguments looking for a match for this option + bool ret = false; for (size_t i = 1; i < args.length; ) { auto a = args[i]; if (endOfOptions.length && a == endOfOptions) break; @@ -504,6 +636,9 @@ void handleOption(R)(string option, R receiver, ref string[] args, ++i; continue; } + + ret = true; + // found it // from here on, commit to eat args[i] // (and potentially args[i + 1] too, but that comes later) @@ -624,6 +759,8 @@ void handleOption(R)(string option, R receiver, ref string[] args, } } } + + return ret; } // 5316 - arrays with arraySep @@ -720,7 +857,8 @@ private struct configuration bool, "bundling", 1, bool, "passThrough", 1, bool, "stopOnFirstNonOption", 1, - ubyte, "", 4)); + bool, "required", 1, + ubyte, "", 3)); } private bool optMatch(string arg, string optPattern, ref string value, @@ -784,6 +922,7 @@ private void setConfig(ref configuration cfg, config option) case config.noBundling: cfg.bundling = false; break; case config.passThrough: cfg.passThrough = true; break; case config.noPassThrough: cfg.passThrough = false; break; + case config.required: cfg.required = true; break; case config.stopOnFirstNonOption: cfg.stopOnFirstNonOption = true; break; default: assert(false); @@ -1058,5 +1197,179 @@ unittest // same bug as 7693 only for bool assertThrown(getopt(args, "foo", &foo)); args = ["prog", "--foo"]; getopt(args, "foo", &foo); - assert(foo); + assert(foo); +} + +unittest +{ + bool foo; + auto args = ["prog", "--foo"]; + getopt(args, "foo", &foo); + assert(foo); +} + +unittest +{ + bool foo; + bool bar; + auto args = ["prog", "--foo", "-b"]; + getopt(args, config.caseInsensitive,"foo|f" "Some foo", &foo, + config.caseSensitive, "bar|b", "Some bar", &bar); + assert(foo); + assert(bar); +} + +unittest +{ + bool foo; + bool bar; + auto args = ["prog", "-b", "--foo", "-z"]; + getopt(args, config.caseInsensitive, config.required, "foo|f" "Some foo", + &foo, config.caseSensitive, "bar|b", "Some bar", &bar, + config.passThrough); + assert(foo); + assert(bar); +} + +unittest +{ + bool foo; + bool bar; + auto args = ["prog", "-b", "-z"]; + assertThrown(getopt(args, config.caseInsensitive, config.required, "foo|f", + "Some foo", &foo, config.caseSensitive, "bar|b", "Some bar", &bar, + config.passThrough)); +} + +unittest +{ + bool foo; + bool bar; + auto args = ["prog", "--foo", "-z"]; + assertNotThrown(getopt(args, config.caseInsensitive, config.required, + "foo|f", "Some foo", &foo, config.caseSensitive, "bar|b", "Some bar", + &bar, config.passThrough)); + assert(foo); + assert(!bar); +} + +unittest +{ + bool foo; + auto args = ["prog", "-f"]; + auto r = getopt(args, config.caseInsensitive, "help|f", "Some foo", &foo); + assert(foo); + assert(!r.helpWanted); +} + +/** This function prints the passed $(D Option) and text in an aligned manner. + +The passed text will be printed first, followed by a newline. Than the short +and long version of every option will be printed. The short and long version +will be aligned to the longest option of every $(D Option) passed. If a help +message is present it will be printed after the long version of the +$(D Option). + +------------ +foreach(it; opt) { + writefln("%*s %*s %s", lengthOfLongestShortOption, it.optShort, + lengthOfLongestLongOption, it.optLong, it.help); +} +------------ + +Params: + text = The text to printed at the beginning of the help output. + opt = The $(D Option) extracted from the $(D getoptX) parameter. +*/ +void defaultGetoptPrinter(string text, Option[] opt) +{ + import std.stdio : stdout; + + defaultGetoptFormatter(stdout.lockingTextWriter(), text, opt); +} + +/** This function writes the passed text and $(D Option) into an output range +in the manner, described in the documentation of function +$(D defaultGetoptXPrinter). + +Params: + output = The output range used to write the help information. + text = The text to printed at the beginning of the help output. + opt = The $(D Option) extracted from the $(D getoptX) parameter. +*/ +void defaultGetoptFormatter(Output)(Output output, string text, Option[] opt) { + import std.format : formattedWrite; + + output.formattedWrite("%s\n", text); + + size_t ls, ll; + bool hasRequired = false; + foreach (it; opt) + { + ls = max(ls, it.optShort.length); + ll = max(ll, it.optLong.length); + + hasRequired = hasRequired || it.required; + } + + size_t argLength = ls + ll + 2; + + string re = " Required: "; + + foreach (it; opt) + { + output.formattedWrite("%*s %*s%*s%s\n", ls, it.optShort, ll, it.optLong, + hasRequired ? re.length : 1, it.required ? re : " ", it.help); + } +} + +unittest +{ + bool a; + auto args = ["prog", "--foo"]; + auto t = getopt(args, "foo|f", "Help", &a); + string s; + auto app = appender!string(); + defaultGetoptFormatter(app, "Some Text", t.options); + + string helpMsg = app.data(); + //writeln(helpMsg); + assert(helpMsg.length); + assert(helpMsg.count("\n") == 3, to!string(helpMsg.count("\n")) ~ " " + ~ helpMsg); + assert(helpMsg.indexOf("--foo") != -1); + assert(helpMsg.indexOf("-f") != -1); + assert(helpMsg.indexOf("-h") != -1); + assert(helpMsg.indexOf("--help") != -1); + assert(helpMsg.indexOf("Help") != -1); + + string wanted = "Some Text\n-f --foo Help\n-h --help This help " + ~ "information.\n"; + assert(wanted == helpMsg); +} + +unittest +{ + bool a; + auto args = ["prog", "--foo"]; + auto t = getopt(args, config.required, "foo|f", "Help", &a); + string s; + auto app = appender!string(); + defaultGetoptFormatter(app, "Some Text", t.options); + + string helpMsg = app.data(); + //writeln(helpMsg); + assert(helpMsg.length); + assert(helpMsg.count("\n") == 3, to!string(helpMsg.count("\n")) ~ " " + ~ helpMsg); + assert(helpMsg.indexOf("Required:") != -1); + assert(helpMsg.indexOf("--foo") != -1); + assert(helpMsg.indexOf("-f") != -1); + assert(helpMsg.indexOf("-h") != -1); + assert(helpMsg.indexOf("--help") != -1); + assert(helpMsg.indexOf("Help") != -1); + + string wanted = "Some Text\n-f --foo Required: Help\n-h --help " + " This help information.\n"; + assert(wanted == helpMsg, helpMsg ~ wanted); }