DEV Community

Jesse Phillips
Jesse Phillips

Posted on • Updated on

Argument Parsing Into Structure in D

Update: Made this into a project -

GitHub logo JesseKPhillips / structopt

Generate GetOpt method call from Structure

This package comes from a little tool I wrote for myself, then blogged about here.

https://dev.to/jessekphillips/argument-parsing-into-structure-4p4n

It allows for using a structure to define the desired command line arguments. Inspired by darg.

import structopt;

// Specify The Parameter Structure
struct Options
{
    @Option("threads", "t")
    @Help("Number of threads to use.")
    size_t threads;

    @Option("file")
    @Help("Input files")
    string[] files;
}

void main(string[] args) {
    Options props;

    // Pass in the struct to generate UDA for
    auto helpInfo = mixin(GenerateGetopt!props);

        defaultGetoptPrinter("Options: ",
          helpInfo.options);
        // Output to console:

        //Options:
        //-t --threads Number of threads to use.
        //      --file Input files
        //-h    --help This help information./
}



Here is an interesting one, starting with some background. Dlang has a standard argument parser, getopt, which provides a single function to specify the arguments to parser out. The function can be called multiple times which can help out with building sub commands with their own arguments. I've really enjoyed the design for this API shown in the documentation example.

However the are situations where a structure makes sense for driving what arguments are accepted and because acronyms are important I want to be DRY. Now it's not like there aren't libraries to parser into a structure like darg, I couldn't figure out my search and still think the standard approach kept things more consistent.

D has some excellent meta programming and generative capabilities. Unlike traditional attributes seen in C# or Java annotations, D has no runtime reflection. I wanted to have the structure build my options and help messages while using the standard getopt function, this is what happened.

import std.traits;
import std.getopt;

// Specify The Parameter Structure
struct Options
{
    @Option("threads", "t")
    @Help("Number of threads to use.")
    size_t threads;

    @Option("file")
    @Help("Input files")
    string[] files;
}

void main(string[] args) {
    Options prop;

    // D's mixin requires a complete
    // statement to be built
    string GenerateGetopt(alias Options)() pure {
        import std.meta;
        import std.typecons;
        import std.format;
        auto ans = `auto helpInfo = getopt(args, `;
        static foreach(opt; FieldNameTuple!Options) {
            // getUDAs will obtain the User Defined Attribute
            // of the specified type
            ans ~= format("getUDAs!(prop.%s, Option)[0].cononical(),"
              ~" getUDAs!(prop.%s, Help)[0].msg, &prop.%s,", opt, opt, opt);
        }

        return ans ~ ");";
    }

    // Pass in the struct to generate UDA for
    mixin(GenerateGetopt!Options);

    if (helpInfo.helpWanted)
    {
        defaultGetoptPrinter("Options: ",
          helpInfo.options);
        // Output to console:

        //Options:
        //-t --threads Number of threads to use.
        //      --file Input files
        //-h    --help This help information./
    }
}

// -- These define the data type for the UDA

struct Help {
    string msg;
}

struct Option {
    string[] names;

    this(string[] names...) {
        this.names = names;
    }

    string cononical() {
        import std.algorithm;
        import std.conv;
        return names.joiner("|").to!string;
    }
}
Enter fullscreen mode Exit fullscreen mode

I also do a similar thing to fill in the structure based on environment variables.

Discussion (1)

Collapse
bbasile profile image
Basile B.

Nice, I did one too a while back, if you need inspiration to enhance yours. But I think that we don't use the same metaprog ways. In iz it's only templates, not mixin.