view dang/OptParse.d @ 211:9e9f3e7e342b default tip

Added dang folder and Module in ast.
author Anders Johnsen <skabet@gmail.com>
date Tue, 12 Aug 2008 20:07:35 +0200
parents 2168f4cb73f1
children
line wrap: on
line source

/*
Copyright (c) 2007 Kirk McDonald

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
 * Command-line option parsing, in the style of Python's optparse.
 *
 * Refer to the complete docs for more information.
 */
module dang.OptParse;

import tango.io.Stdout;
import tango.text.Util : locate, locatePrior;
import tango.text.Ascii : toUpper;
import tango.stdc.stdlib : exit, EXIT_FAILURE, EXIT_SUCCESS;
import tango.text.convert.Integer : parse, toInt, toString = toString;
import tango.text.convert.Utf : toString, toString32;

/*
Options may be in two forms: long and short. Short options start with a single
dash and are one letter long. Long options start with two dashes and may
consist of any number of characters (so long as they don't start with a dash,
though they may contain dashes). Options are case-sensitive.

Short options may be combined. The following are equivalent:

$ myapp -a -b -c
$ myapp -abc
$ myapp -ab -c

If -f and --file are aliases of the same option, which accepts an argument,
the following are equivalent:

$ myapp -f somefile.txt
$ myapp -fsomefile.txt
$ myapp --file somefile.txt
$ myapp --file=somefile.txt

The following are also valid:

$ myapp -abcf somefile.txt
$ myapp -abcfsomefile.txt
$ myapp -abc --file somefile.txt

If an option occurs multiple times, the last one is the one recorded:

$ myapp -f somefile.txt --file otherfile.txt

Matches 'otherfile.txt'.
*/

bool startswith(char[] s, char[] start) {
    if (s.length < start.length) return false;
    return s[0 .. start.length] == start;
}
bool endswith(char[] s, char[] end) {
    if (s.length < end.length) return false;
    return s[$ - end.length .. $] == end;
}

/// Thrown if client code tries to set up an improper option.
class OptionError : Exception {
    this(char[] msg) { super(msg); }
}
// Thrown if client code tries to extract the wrong type from an option.
class OptionTypeError : Exception {
    this(char[] msg) { super(msg); }
}

/++
This class represents the results after parsing the command-line.
+/
class Options {
    char[][][char[]] opts;
    int[char[]] counted_opts;
    /// By default, leftover arguments are placed in this array.
    char[][] args;

    /// Retrieves the results of the Store and StoreConst actions.
    char[] opIndex(char[] opt) {
        char[][]* o = opt in opts;
        if (o) {
            return (*o)[0];
        } else {
            return "";
        }
    }
    /// Retrieves the results of the Store action, when the type is Integer.
    int value(char[] opt) {
        char[][]* o = opt in opts;
        if (o) {
            return toInt((*o)[0]);
        } else {
            return 0;
        }
    }
    /// Retrieves the results of the Append and AppendConst actions.
    char[][] list(char[] opt) {
        char[][]* o = opt in opts;
        if (o) {
            return *o;
        } else {
            return null;
        }
    }
    /// Retrieves the results of the Append action, when the type is Integer.
    int[] valueList(char[] opt) {
        char[][]* o = opt in opts;
        int[] l;
        if (o) {
            l.length = (*o).length;
            foreach (i, s; *o) {
                l[i] = toInt(s);
            }
        }
        return l;
    }
    /// Retrieves the results of the Count action.
    int count(char[] opt) {
        int* c = opt in counted_opts;
        if (c) {
            return *c;
        } else {
            return 0;
        }
    }
    /// Retrieves the results of the SetTrue and SetFalse actions.
    bool flag(char[] opt) {
        char[][]* o = opt in opts;
        if (o) {
            return (*o)[0] == "1";
        } else {
            return false;
        }
    }
}

// Options, args, this opt's index in args, name[, arg]
///
alias void delegate(Options, inout char[][], inout int, char[], char[]) OptionCallbackFancyArg;
///
alias void delegate(Options, inout char[][], inout int, char[], int)    OptionCallbackFancyInt;
///
alias void delegate(Options, inout char[][], inout int, char[])         OptionCallbackFancy;

///
alias void delegate(char[]) OptionCallbackArg;
///
alias void delegate(int)    OptionCallbackInt;
///
alias void delegate()       OptionCallback;

/*
Actions:
 * Store:        name
 * StoreConst:   name, const_value
 * Append:       name
 * AppendConst:  name, const_value
 * Count:        name
 * CallbackArg:  dga
 * CallbackVoid: dg
*/
///
enum Action { /+++/Store, /+++/StoreConst, /+++/Append, /+++/AppendConst, /+++/Count, /+++/SetTrue, /+++/SetFalse, /+++/Callback, /+++/CallbackFancy, /+++/Help /+++/}
///
enum ArgType { /+++/None, /+++/String, /+++/Integer /+++/}

ArgType defaultType(Action action) {
    switch (action) {
        case Action.Store, Action.Append, Action.Callback, Action.CallbackFancy:
            return ArgType.String;
            break;
        default:
            return ArgType.None;
            break;
    }
}

/++
This class represents a single command-line option.
+/
class Option {
    char[][] shortopts, longopts;
    Action action;
    ArgType type;
    char[] name, argname;
    char[] const_value;

    char[] default_string;
    int default_value;
    bool default_flag, has_default;

    OptionCallbackArg callback;
    OptionCallbackInt int_callback;
    OptionCallback void_callback;

    OptionCallbackFancyArg fancy_callback;
    OptionCallbackFancyInt fancy_int_callback;
    OptionCallbackFancy fancy_void_callback;
    char[] helptext;
    this(
        char[][] shorts, char[][] longs, ArgType type,
        Action act, char[] name, char[] const_value,
        OptionCallbackArg dga, OptionCallback dg,
        OptionCallbackInt dgi,
        OptionCallbackFancyArg fdga,
        OptionCallbackFancyInt fdgi,
        OptionCallbackFancy fdg
    ) {
        this.shortopts = shorts;
        this.longopts = longs;
        this.action = act;
        this.type = type;
        this.name = name;
        this.argname = toUpper(name.dup);
        this.default_string = "";
        this.default_value = 0;
        this.default_flag = false;

        // Perform sanity checks.
        assert (name !is null);
        switch (act) {
            case Action.Store, Action.Append:
                assert(type != ArgType.None);
                break;
            case Action.StoreConst, Action.AppendConst:
                assert(type == ArgType.None);
                assert(const_value !is null);
                break;
            case Action.Callback:
                //assert(type != ArgType.None);
                switch (type) {
                    case ArgType.String:
                        assert(dga !is null);
                        break;
                    case ArgType.Integer:
                        assert(dgi !is null);
                        break;
                    case ArgType.None:
                        assert(dg !is null);
                        break;
                }
                break;
            case Action.CallbackFancy:
                switch (type) {
                    case ArgType.String:
                        assert(fdga !is null);
                        break;
                    case ArgType.Integer:
                        assert(fdgi !is null);
                        break;
                    case ArgType.None:
                        assert(fdg !is null);
                        break;
                }
            default:
                break;
        }
        this.const_value = const_value;
        this.callback = dga;
        this.int_callback = dgi;
        this.void_callback = dg;
        this.fancy_callback = fdga;
        this.fancy_int_callback = fdgi;
        this.fancy_void_callback = fdg;
    }
    char[] toString() {
        int optCount = this.shortopts.length + this.longopts.length;
        char[] result;
        bool printed_arg = false;
        foreach(i, opt; this.shortopts ~ this.longopts) {
            result ~= opt;
            if (i < optCount-1) {
                result ~= ", ";
            } else if (this.hasArg()) {
                result ~= "=" ~ toUpper(this.argname.dup);
            }
        }
        return result;
    }
    //enum Action { Store, StoreConst, Append, AppendConst, Count, SetTrue, SetFalse, Callback, CallbackFancy, Help }
    void issue_default(Options results) {
        // Only set the default if the option doesn't already have a value.
        char[][]* val = this.name in results.opts;
        switch (this.action) {
            case Action.Store, Action.Append:
                if (val !is null) return;
                if (this.type == ArgType.String) {
                    results.opts[name] = [default_string];
                } else {
                    results.opts[name] = [.toString(default_value)];
                }
                break;
            case Action.StoreConst, Action.AppendConst:
                if (val !is null) return;
                results.opts[name] = [default_string];
                break;
            case Action.SetTrue, Action.SetFalse:
                if (val !is null) return;
                if (default_flag) {
                    results.opts[name] = ["1"];
                } else {
                    results.opts[name] = ["0"];
                }
                break;
            default:
                return;
        }
    }
    // Does whatever this option is supposed to do.
    void performAction(OptionParser parser, Options results, inout char[][] args, inout int idx, char[] arg) {
        int i;
        if (this.type == ArgType.Integer) {
            // Verify that it's an int.
            i = parser.toOptInt(arg);
        }
        switch (this.action) {
            case Action.Store:
                results.opts[name] = [arg];
                break;
            case Action.Append:
                results.opts[name] ~= arg;
                break;
            case Action.StoreConst:
                assert(arg is null, "Got unexpected argument for '"~name~"' option.");
                results.opts[name] = [const_value];
                break;
            case Action.AppendConst:
                assert(arg is null, "Got unexpected argument for '"~name~"' option.");
                results.opts[name] ~= const_value;
                break;
            case Action.Count:
                assert(arg is null, "Got unexpected argument for '"~name~"' option.");
                ++results.counted_opts[name];
                break;
            case Action.SetTrue:
                results.opts[name] = ["1"];
                break;
            case Action.SetFalse:
                results.opts[name] = ["0"];
                break;
            case Action.Callback:
                switch (type) {
                    case ArgType.String:
                        callback(arg);
                        break;
                    case ArgType.Integer:
                        int_callback(i);
                        break;
                    case ArgType.None:
                        void_callback();
                        break;
                }
                break;
            case Action.CallbackFancy:
                switch (type) {
                    case ArgType.String:
                        fancy_callback(results, args, idx, name, arg);
                        break;
                    case ArgType.Integer:
                        fancy_int_callback(results, args, idx, name, i);
                        break;
                    case ArgType.None:
                        fancy_void_callback(results, args, idx, name);
                        break;
                }
                break;
            case Action.Help:
                parser.helpText();
                exit(EXIT_SUCCESS);
                break;
        }
    }
    /// Returns whether this option accepts an argument.
    bool hasArg() {
        return this.type != ArgType.None;
    }
    /// Sets the help text for this option.
    Option help(char[] help) {
        this.helptext = help;
        return this;
    }
    /// Sets the name of this option's argument, if it has one.
    Option argName(char[] argname) {
        this.argname = argname;
        return this;
    }
    Option def(char[] val) {
        if (
            (this.type != ArgType.String || (this.action != Action.Store && this.action != Action.Append)) &&
            this.action != Action.StoreConst && this.action != Action.AppendConst
        )
            throw new OptionError("Cannot specify string default for non-string option '"~this.name~"'");
        this.has_default = true;
        this.default_string = val;
        return this;
    }
    Option def(int val) {
        if (this.type != ArgType.Integer || (this.action != Action.Store && this.action != Action.Append))
            throw new OptionError("Cannot specify integer default for non-integer option '"~this.name~"'");
        this.has_default = true;
        this.default_value = val;
        return this;
    }
    Option def(bool val) {
        if (this.action != Action.SetTrue && this.action != Action.SetFalse)
            throw new OptionError("Cannot specify boolean default for non-flag option '"~this.name~"'");
        this.has_default = true;
        this.default_flag = val;
        return this;
    }
    // Returns true if the passed option string matches this option.
    bool matches(char[] _arg) {
        dchar[] arg = toString32(_arg);
        if (
            arg.length < 2 ||
            arg.length == 2 && (arg[0] != '-' || arg[1] == '-') ||
            arg.length > 2 && (arg[0 .. 2] != "--" || arg[2] == '-')
        ) {
            return false;
        }
        if (arg.length == 2) {
            foreach (opt; shortopts) {
                if (_arg == opt) {
                    return true;
                }
            }
        } else {
            foreach (opt; longopts) {
                if (_arg == opt) {
                    return true;
                }
            }
        }
        return false;
    }
}

/++
This class is used to define a set of options, and parse the command-line
arguments.
+/
class OptionParser {
    OptionCallbackArg leftover_cb;
    /// An array of all of the options known by this parser.
    Option[] options;
    char[] name, desc;
    /// The description of the programs arguments, as used in the Help action.
    char[] argdesc;
    private void delegate(char[]) error_callback;

    this(char[] desc="") {
        this.name = "";
        this.desc = desc;
        this.argdesc = "[options] args...";
    }

    /// Sets a callback, to override the default error behavior.
    void setErrorCallback(void delegate(char[]) dg) {
        error_callback = dg;
    }
    void unknownOptError(char[] opt) {
        error("Unknown argument '"~opt~"'");
    }
    void expectedArgError(char[] opt) {
        error("'"~opt~"' option expects an argument.");
    }
    /// Displays an error message and terminates the program.
    void error(char[] err) {
        if (error_callback !is null) {
            error_callback(err);
        } else {
            this.helpText();
            Stdout.formatln(err);
        }
        exit(EXIT_FAILURE);
    }
    int toOptInt(char[] s) {
        int i;
        uint ate;
        i = .parse(s, 10u, &ate);
        if (ate != s.length)
            error("Could not convert '"~s~"' to an integer.");
        return i;
    }

    /// Displays useful "help" information about the program's options.
    void helpText() {
        int optWidth;
        char[][] optStrs;
        typedef char spacechar = ' ';
        spacechar[] padding;
        // Calculate the maximum width of the option lists.
        foreach(i, opt; options) {
            optStrs ~= opt.toString();
            if (optStrs[i].length > optWidth) {
                optWidth = optStrs[i].length;
            }
        }
        Stdout.formatln("Usage: {0} {1}", this.name, this.argdesc);
        if (this.desc !is null && this.desc != "") Stdout.formatln(this.desc);
        Stdout.formatln("\nOptions:");
        foreach(i, opt; options) {
            padding.length = optWidth - optStrs[i].length;
            Stdout.formatln("  {0}{1} {2}", optStrs[i], cast(char[])padding, opt.helptext);
        }
    }
    
    // Checks the passed arg against all the options in the parser.
    // Returns null if no match is found.
    Option matches(char[] arg) {
        foreach(o; options) {
            if (o.matches(arg)) {
                return o;
            }
        }
        return null;
    }
    char[] getBaseName(char[] path) {
        version(Windows) {
            char delimiter = '\\';
        } else {
            char delimiter = '/';
        }
        uint idx = locatePrior(path, delimiter);
        if (idx == path.length) return path;
        return path[idx+1 .. $];
    }
    char[] getProgramName(char[] path) {
        version(Windows) {
            // (Unicode note: ".exe" only contains 4 code units, so this slice
            // should Just Work.) (Although it remains to be seen how robust
            // this code actually is.)
            //Stdout.formatln(path);
            //assert(path[$-4 .. $] == ".exe");
            //path = path[0 .. $-4];
        }
        return getBaseName(path);
    }
    /// Parses the passed command-line arguments and returns the results.
    Options parse(char[][] args) {
        this.name = getProgramName(args[0]);
        args = args[1 .. $];
        Options options = new Options;
        /*
        The issue is this:

        $ myapp -abc

        This might be three short opts, or one or two opts, the last of which
        accepts an argument. In the three-opt case, we want to get:

        $ myapp -a -b -c

        In the one-opt case, we want:

        $ myapp -a bc

        In the two-opt case, we want:

        $ myapp -a -b c

        We also want to parse apart "--file=somefile" into "--file somefile"
        */
        char[] opt, newopt, arg;
        dchar[] opt32;
        int idx;
        Option match;

        for (int i=0; i<args.length; ++i) {
            opt = args[i];
            // -- ends the option list, the remainder is dumped into args
            if (opt == "--") {
                if (this.leftover_cb !is null) {
                    foreach(a; args[i+1 .. $]) {
                        this.leftover_cb(a);
                    }
                } else {
                    options.args ~= args[i+1 .. $];
                }
                i = args.length;
            } else if (opt.startswith("--")) {
                idx = locate(opt, '=');
                if (idx != opt.length) {
                    newopt = opt[0 .. idx];
                    // Stitch out the old arg, stitch in the newopt, arg pair.
                    // (Unicode note: idx+1 works, since we know '=' is a
                    // single code unit.)
                    args = args[0 .. i] ~ [newopt, opt[idx+1 .. $]] ~ args[i+1 .. $];
                } else {
                    newopt = opt;
                }
                match = matches(newopt);
                if (match is null) {
                    unknownOptError(newopt);
                }
                if (match.hasArg) {
                    if (i == args.length-1) expectedArgError(match.name);
                    arg = args[i+1];
                    ++i;
                } else {
                    arg = null;
                }
                match.performAction(this, options, args, i, arg);
            } else if (opt.startswith("-")) {
                if (opt.length >= 2) {
                    opt32 = toString32(opt[1 .. $]);
                    foreach (j, c; opt32) {
                        newopt = .toString("-" ~ [c]);
                        match = matches(newopt);
                        if (match is null) {
                            unknownOptError(newopt);
                        }
                        if (match.hasArg) {
                            // This is the last char in the group, look to the
                            // next element of args for the arg.
                            if (j == opt32.length-1) {
                                if (i == args.length-1) expectedArgError(match.name);
                                arg = args[i+1];
                                ++i;
                            // Otherwise, consume the rest of this group for
                            // the arg.
                            } else {
                                arg = .toString(opt32[j+1 .. $]);
                                match.performAction(this, options, args, i, arg);
                                break;
                            }
                        } else {
                            arg = null;
                        }
                        match.performAction(this, options, args, i, arg);
                    }
                } else {
                    unknownOptError(opt);
                }
            } else {
                if (this.leftover_cb is null) {
                    options.args ~= opt;
                } else {
                    this.leftover_cb(opt);
                }
            }
        }
        foreach (o; this.options) {
            o.issue_default(options);
        }
        return options;
    }

    /++
    Overrides the default behavior of leftover arguments, calling this callback
    with them instead of adding them an array.
    +/
    void leftoverCallback(OptionCallbackArg dg) {
        this.leftover_cb = dg;
    }
    ///
    Option addOption(Option option) {
        this.options ~= option;
        return option;
    }
    //                    options  action             name  type                       const_value  dga   dgv   dgi   fdga  fdgi  fdg
    ///
    Option addOption(char[][] options ...) {
        return addOption(options, Action.Store,      null, defaultType(Action.Store), null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, char[] name) {
        return addOption(options, Action.Store,      name, defaultType(Action.Store), null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, Action action) {
        return addOption(options, action,            null, defaultType(action),       null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, ArgType type) {
        return addOption(options, Action.Store,      null, type,                      null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, Action action, ArgType type) {
        return addOption(options, action,            null, type,                      null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, char[] name, Action action) {
        return addOption(options, action,            name, defaultType(action),       null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, char[] name, Action action, ArgType type) {
        return addOption(options, action,            name, type,                      null,        null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, Action action, char[] const_value) {
        return addOption(options, action,            null, defaultType(action),       const_value, null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, char[] name, char[] const_value) {
        return addOption(options, Action.StoreConst, name, defaultType(Action.Store), const_value, null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, char[] name, Action action, char[] const_value) {
        return addOption(options, action,            name, defaultType(action),       const_value, null, null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, OptionCallbackArg dg) {
        return addOption(options, Action.Callback,   null, ArgType.String,            null,        dg,   null, null, null, null, null);
    }
    ///
    Option addOption(char[][] options, OptionCallback dg) {
        return addOption(options, Action.Callback,   null, ArgType.None,              null,        null, dg,   null, null, null, null);
    }
    ///
    Option addOption(char[][] options, OptionCallbackInt dg) {
        return addOption(options, Action.Callback,   null, ArgType.Integer,           null,        null, null, dg,   null, null, null);
    }
    ///
    Option addOption(char[][] options, OptionCallbackFancyArg dg) {
        return addOption(options, Action.CallbackFancy, null, ArgType.String,         null,        null, null, null, dg,   null, null);
    }
    ///
    Option addOption(char[][] options, OptionCallbackFancy dg) {
        return addOption(options, Action.CallbackFancy, null, ArgType.None,           null,        null, null, null, null, null, dg);
    }
    ///
    Option addOption(char[][] options, OptionCallbackFancyInt dg) {
        return addOption(options, Action.CallbackFancy, null, ArgType.Integer,        null,        null, null, null, null, dg,   null);
    }
    ///
    Option addOption(char[][] options, char[] name, OptionCallbackFancyArg dg) {
        return addOption(options, Action.CallbackFancy, name, ArgType.String,         null,        null, null, null, dg,   null, null);
    }
    ///
    Option addOption(char[][] options, char[] name, OptionCallbackFancy dg) {
        return addOption(options, Action.CallbackFancy, name, ArgType.None,           null,        null, null, null, null, null, dg);
    }
    ///
    Option addOption(char[][] options, char[] name, OptionCallbackFancyInt dg) {
        return addOption(options, Action.CallbackFancy, name, ArgType.Integer,        null,        null, null, null, null, dg,   null);
    }
    // Although users certainly /can/ call this, all those overloads are there
    // for a reason.
    Option addOption(
        char[][] options, 
        Action action, char[] name, ArgType type, char[] const_value,
        OptionCallbackArg callback, OptionCallback vcall,
        OptionCallbackInt icall,
        OptionCallbackFancyArg fdga,
        OptionCallbackFancyInt fdgi,
        OptionCallbackFancy fdg
    ) {
        char[][] shortopts;
        char[][] longopts;
        dchar[] opt;
        Option option;
        foreach (_opt; options) {
            // (Unicode note: We convert to dchar[] so the length checks work
            // out in the event of a short opt with a >127 character.)
            opt = toString32(_opt);
            if (opt.length < 2) {
                throw new OptionError(
                    "invalid option string '" ~ _opt ~ "': must be at least two characters long"
                );
            } else if (opt.length > 2) {
                if (opt[0 .. 2] != "--" || opt[2] == '-')
                    throw new OptionError(
                        "invalid long option string '" ~ _opt ~ "': must start with --, followed by non-dash"
                    );
                longopts ~= _opt;
            } else {
                if (opt[0] != '-' || opt[1] == '-')
                    throw new OptionError(
                        "invalid short option string '" ~ _opt ~ "': must be of the form -x, where x is non-dash"
                    );
                shortopts ~= _opt;
            }
        }
        if (name is null) {
            // (Unicode note: We know '-' is a single code unit, so these
            // slices are okay.)
            if (longopts.length > 0)
                name = longopts[0][2 .. $];
            else if (shortopts.length > 0)
                name = shortopts[0][1 .. 2];
            else
                throw new OptionError(
                    "No options provided to addOption!"
                );
        }
        option = new Option(shortopts, longopts, type, action, name, const_value, callback, vcall, icall, fdga, fdgi, fdg);
        this.options ~= option;
        return option;
    }
}