ARAVINDA VK

Implementing CLI Subcommands with D programming language

Jul 28, 2019
3 minutes read.
dlang d

Never heard of D Programming?

D is a general-purpose programming language with static typing, systems-level access, and C-like syntax. With the D Programming Language, write fast, read fast, and run fast.

Fast code, fast. https://dlang.org

Parsing command-line arguments is very easy using the "getopt" module available in the standard library. In this blog, I am sharing my experience about how I added subcommand support to my command-line applications.

Below example shows a basic program which accepts --name or -n argument.

import std.stdio;
import std.getopt;

int main(string[] args)
{
    string name;
    auto opts = getopt(
        args,
        "n|name", "Your Name", &name
    );

    if (opts.helpWanted) {
        defaultGetoptPrinter("./mycli", opts.options);
        return 1;
    }

    writefln("Hello %s", name);
    return 0;
}

Once "getopt" parses the flags, args will contain only remaining arguments(positional arguments). For example,

import std.stdio;
import std.getopt;

int main(string[] args)
{
    string lastName;
    auto opts = getopt(
        args,
        "last-name", "Your Last Name", &lastName
    );

    if (opts.helpWanted || args.length != 2) {
        defaultGetoptPrinter("./mycli <name> [OPTIONS]", opts.options);
        return 1;
    }

    string name = args[1];

    writefln("Hello %s %s", name, lastName);
    return 0;
}

Now to handle sub-commands, we need to define multiple getopt. For Example,

import std.stdio;
import std.getopt;

const progHelp = "./mycli get|set [OPTIONS]";

int subcmdSet(string[] args)
{
    string name;
    bool isAdmin;
    auto opts = getopt(
        args,
        std.getopt.config.required,  // To make --name or -n as required field
        "n|name", "Name", &name,
        "admin", "Set Admin privileges", &isAdmin
    );

    if (opts.helpWanted)
    {
        defaultGetoptPrinter("./mycli set -n <Name>", opts.options);
        return 1;
    }
    // Subcommand implementation
    writef("Set name as %s", name);
    if (isAdmin)
        write("(admin)");
    writeln();
    return 0;
}

int subcmdGet(string[] args)
{
    string name;
    auto opts = getopt(
        args,
        std.getopt.config.required,
        "n|name", "Name", &name
    );

    if (opts.helpWanted)
    {
        defaultGetoptPrinter("./mycli get --n <Name>", opts.options);
        return 1;
    }
    // Subcommand implementation
    writefln("Name is %s", name);
    return 0;
}

int main(string[] args)
{
    auto subcmds = [
        "set": &subcmdSet,
        "get": &subcmdGet,
    ];

    if (args.length < 2)
    {
        writeln("subcommand not specified");
        defaultGetoptPrinter(progHelp, globalOpts.options);
        return 1;
    }

    auto func = (args[1] in subcmds);

    if (func is null)
    {
        writeln("Unknown sub-command");
        defaultGetoptPrinter(progHelp, globalOpts.options);
        return 1;
    }
    return (*func)(args);
}

If the program needs global flags which are applicable for all sub-commands, then make sure to add global flags getopt before parsing sub-commands. Also, add std.getopt.config.passThrough to avoid failing to parse flags related to sub-commands.

struct globalFlags
{
    string logLevel = "INFO";
    bool debugEnabled;
}

globalFlags gflags;

int main(string[] args)
{
    auto globalOpts = getopt(
        args,
        std.getopt.config.passThrough,
        "l|log-level", "Log Level", &gflags.logLevel,
        "debug", "Debug mode", &gflags.debugEnabled
    );

    if (args.length < 2)
    {
        if (!globalOpts.helpWanted)
            writeln("subcommand not specified");

        defaultGetoptPrinter(progHelp, globalOpts.options);
        return 1;
    }

    // -h is already parsed during Global options parsing. Reinsert to args
    // So that subcommands will work as usual
    if (globalOpts.helpWanted)
        args ~= "-h";

    auto subcmds = [
        "set": &subcmdSet,
        "get": &subcmdGet,
    ];

    auto func = (args[1] in subcmds);

    if (func is null)
    {
        writeln("Unknown sub-command");
        defaultGetoptPrinter(progHelp, globalOpts.options);
        return 1;
    }
    return (*func)(args);
}

Notes:

  • getopt errors can be handled by catching GetoptException and,

  • Handle any datatype conversion errors by catching ConvException`.

  • Positional arguments start with index 2(Example: args[2])

That’s it! Let me know if this blog post is useful.

About Aravinda VK

Partner at Kadalu Investments, Creator of Sanka, Creator of Chitra, GlusterFS core team member, Maintainer of Kadalu Storage
Contact: Linkedin | Twitter | Facebook | Github | mail@aravindavk.in