Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact

Leave Comments, Critiques, and Suggestions Here

Argument handling using ArgParser

The ArgParser class in tango.text is very nice to use for command line based applications and the following tutorial shows some of what is possible. And even though the parsing of command line arguments is the most common usage, ArgParser can also be used for arguments stored in response files. The D feature most notable here is anonymous delegates, although in a more complex example using classes they could have been delegates referenced from methods.

The ArgParser class can handle all that is needed for the parsing and handling of the arguments themselves, but a few additional Tango classes are needed to do what we want for the more "advanced" features of the tutorial, which are mainly input and output.

Command Line Arguments

When a command line application (like the typical *nix tool or the commands available when using cmd.exe under Windows) is executed in a shell, command line arguments can be passed along to the application by writing them on the command line after the name of the executable itself. The application may or may not try to make sense of these. Which ones it accepts as legal arguments, is usually available from the documentation, whereas unknown arguments can be either ignored or cause an error.

Most programming languages, including D, pass these command line arguments to the program entry point, in D the main function. Each space separated argument is put into an element in an array of char![]s, and this array is then passed through the first parameter to the main function. The first element in this array is commonly the name of the executable itself, and thus extra arguments were passed if the array's length is larger than 1 (one).

As this short tutorial shows, the ArgParser class isn't restricted to handling this array alone, but any array of strings.

Argument Handling

To compile this example, you need to have Tango set up as your standard library. The full source for the example is in the example folder of the Tango distribution, and, using that as your root folder, the example can be built using

build example/argparser.d
import tango.io.Stdout,
       tango.io.FileConduit;

import tango.text.ArgParser,
       tango.text.LineIterator;

void main(char[][] args)
{
    char[][] fileList;
    char[] responseFile = null;
    char[] varx = null;
    bool coolAction = false;
    bool displayHelp = false;
    char[] helpText = "Available options:\n\t\t-h\tthis 
         help\n\t\t-cool-option\tdo cool things to your 
         files\n\t\t@filename\tuse filename as a response 
         file with extra arguments\n\t\tall other arguments 
         are handled as files to do cool things with.";

The first part of the example imports the needed modules, ArgParser, Stdout, FileConduit and LineIterator, all from Tango.

At the start of the main function, the variables needed for this small program are declared. A list of strings in fileList, a string called responseFile, a string called varx, the bools coolAction and displayHelp, and helpText. helpText is set to a long string with some information on how to run this application. It is somewhat messy, but shows how newlines and tabs can be used in strings for some basic formatting in the console.

    ArgParser parser = new ArgParser((char[] value,uint ordinal){
        Stdout.format("Added file number {0} to list of files", ordinal).newline;
        fileList ~= value;
    });

    parser.bind("-", "h",{
        displayHelp=true;
    });

    parser.bind("-", "cool-action",{
        coolAction=true;
    });

    parser.bind("-", "X=",(char[] value){
        varx=value;
    });
	
    parser.bindDefault("@",(char[] value, uint ordinal){
        if (ordinal > 0) {
            throw new Exception("Only one response file can be given.");
        }
        responseFile = value;
    });

First in this block, we created a parser instance from the ArgParser class. The default constructor does not take a parameter, but rather a delegate. The delegate should return a void and take both a string and an uint as parameters. The delegate passed here is anonymous, which means that it is really only the parser instance that has a reference to it, but if it is executed, it will be executed in the context of this small application. The delegate itself is called for all arguments that do not fit into the other bound delegates, for instance file names. tango.io.Stdout is used to print a message to screen using the ordinal, while the string value is added to the fileList variable.

After the constructor, three standard arguments are registered using the bind method. It is given a prefix and the argument itself. Since I don't want to attach a value to the argument for the first two, I only attach a simple delegate to it so the program is simply notified when that argument is used. Both of these delegates set a flag to true if they are called. The third delegate accepts a value (which on the commandline would immediately follow -X=), which is placed in the variable varx.

The last delegate bound is a default callback for a given prefix. It will be called if the argument has the prefix, but the argument self is not registered. Here an exception is thrown if this delegate is called more than once, since the passed string value is used in a way that conflicts would arise on multiple repetitions. The string value is stored in the responseFile variable.

    if (args.length < 2) {
        Stdout(helpText).newline;
        return;
    }
    parser.parse(args[1..$]);

    if (displayHelp) {
        Stdout(helpText).newline;
    }
    else {
        if (responseFile !is null) {
            auto file = new FileConduit(responseFile);
            // create an iterator and bind it to the file
            auto lines = new LineIterator(file);
            // process file one line at a time
            char[][] arguments;
            foreach (line; lines) {
                arguments ~= line;
            }
            parser.parse(arguments);
        }
        if (coolAction) {
            Stdout("Listing the files to be actioned in a cool way.").newline;
            foreach (id, file; fileList) {
                Stdout.format("{0}. {1}", id + 1, file).newline;
            }
            Stdout("Cool and secret action performed.").newline;
        }
        if (varx !is null) {
            Stdout.format("User set the X factor to \"{0}\".", varx).newline;
        }
    }

If no arguments are received, the help text is printed. If there are some arguments, they are parsed by sending them to the parse method in the ArgParser. Inside this parse call, the various delegates are called if the right matches are found. After the call to parse, the flags are checked to see if they are set. The first check is for displayHelp and, if it is true (that is, the application user passed the '-h' argument), the help text is displayed.

If the help text is not shown, the default callback is checked for the '@' prefix. The value passed in that call is used as a response file (similar to build). FileConduit is used on the string in the responseFile variable and attached to a LineIterator, which makes a new array of arguments. foreach is used on the LineIterator, thus making each line in the file an argument, and each line is parsed.

If the coolAction flag is set, a cool and secret action is performed on the fileList variable collected through the overall default callback.

Also, if the -X= was passed to the application, a text about the X factor will be printed.

When compiled and executed, it can look like this if passed only the '-h' switch:

larsivi@kubuntu:~/d/tango/trunk$ example/argparser -h
Available options:
                -h      this help
                -cool-action    do cool things to your files
                @filename       use filename as a response file with extra arguments
                all other arguments are handled as files to do cool things with.

A more complete example would need a response file. An example file with the following contents could be used:

testingFile1
testingFile2
readingFromFile3

and using it would look like this:

larsivi@kubuntu:~/d/tango/trunk$ example/argparser -cool-action foo bar @example/argparser.brf -X=WOW
Added file number 0 to list of files
Added file number 1 to list of files
Added file number 2 to list of files
Added file number 3 to list of files
Added file number 4 to list of files
Listing the files to be actioned in a cool way.
1. foo
2. bar
3. testingFile1
4. testingFile2
5. readingFromFile3
Cool and secret action performed.
User set the X factor to "WOW".

Summary

The ArgParser class can be used to handle arguments passed to an application, most commonly through the command line, but also by parsing any constructed array of strings. The class can be used by binding delegates to a given argument prefix. The delegates can optionally have a value parameters, which when the delegate is called will receive the value following the bound argument value. When the necessary delegates are bound, the parse method is called with the string array representing the arguments, and upon finding matches in the bindings, the relevant delegates are called.

Additional Notes

  • This class is a low level argument parser, and don't provide the more high level features of other command line libraries. However, for small applications, this should be more than enough, and it should also be a very good base for a higher level library.
  • Some applications use information from the environment for some of it's options. Reading these is beyond the scope of this tutorial, but individual environment variables can be retrieved by using the C function getenv, and thereafter passed to the parse method (the data will probably most likely need som preprocessing to fit it into a char![]![]).

Source