Introduction to Colipa

This is an introduction on how to use Colipa. The full documentation is in the comments in the interface section of the colipa.m file. You can also find an example program in the test.m file.

This file is released under the terms of the GNU Free Documentation License, Version 1.3 or any later version. See COPYING.FDL for details.

GNU style command line arguments

Colipa supports GNU style command line arguments. This means you have short arguments, which begin with a single dash (-a) and long arguments, which begin with a double dash (--arg). Both can have a value, which can be placed inside the parameter or in the next parameter (-aval, --arg=val, -a val or --arg val). Parameters can be arguments (beginning with a dash) or non-arguments (everything else). Arguments can occur anywhere in the command line, not just at the beginning. Everything after the special parameter -- is treated as a non-argument. Long arguments can be abbreviated as long as they are unique.

Introduction

Defining the arguments

Command line arguments are identified by unique strings. These strings are semantically wrapped inside the cla type. This makes it possible to reuse and share command line argument definitions. (Previously, in the 1 versions of Colipa, the arguments were identified by an enumeration type. That made it impossible to define command line arguments in one module and use them in another.)

This is the type of the command line argument identifiers:

:- type cla ---> cla(cla :: string).

The argument identifiers have the form cla(Str), where Str is a unique string that names the argument. It is meant not to change, although it’s relatively easy to change it later - you just need to replace all occurences of cla(XXX) with cla(YYY), where XXX and YYY are the strings which identify the argument, before and after the change.

You describe each command line argument with a list of properties. You have one big list of all the arguments and their properties. The required type for that list is assoc_list(cla, list(property)). For instance:

:- func argdescl = assoc_list(cla, list(property)).

argdescl = [
        cla("dir") -
        [ prop_long("dir"),
          prop_switch_value("Dir"),
          prop_description("Name of the directory.")
        ],

        cla("archive") -
        [ prop_short('a'),
          prop_long("archive"),
          prop_switch,
          prop_description("If the program is to archive the data.")
        ],

        ...
].

All arguments need to have a name, by which they appear on the command line. It can be either short (prop_short) or long (prop_long) or both. If you want to use the automatic usage info generation, you need to give your arguments descriptions, with the prop_description property. See colipa.m for a list of all supported properties.

The prop_default property, for setting a default value, is different. It must be specified like this:

'new prop_default'(Default)

The new prefix is needed because the data constructor uses an existential type. See the chapter Existential types in the Mercury Language Reference Manual. But don’t worry, you don’t need to understand existential types for using Colipa.

Parsing the command line

Next, you need to call the command line parser. The most simple way should be to use the colipa/5 predicate:

:- pred colipa(
    assoc_list(cla, list(property))::in,
    arguments::out,
    list(string)::out,
    io::di, io::uo)
is det.

You pass it, as the first parameter, your argument description list. The recognized command line arguments are returned in the second parameter, in a value of type arguments. Later you use it when querying the command line arguments. The non-argument parameters are returned in the third parameter of colipa/5. So the call looks like this:

colipa(argdescl, Arguments, NonArguments, !IO)

Extracting information from the command line

Now you’re ready to get the information you want out of the parsed command line arguments. This is done by query functions. The function to use is determined by what you want to get out of the parameters. For instance, for an argument that can be specified zero or one times, and has a value, you would use the arg_maybe_value function. If your argument is of type T, then arg_maybe_value will return a value of type maybe(T). When the argument hasn’t been given on the command line, it would return no. If it has, it would return yes(Value).

The used query function must match the argument definition (the argument’s property list). For instance, you can’t query an argument that doesn’t take a value, with arg_value or arg_maybe_value. It is probably a bug, when this should happen. If such a mismatch is detected, an arg_desc_error is thrown. You can catch and output that error and get an elaborate message about what went wrong.

The query functions all take the Arguments value and the argument identifier as parameters, and return the queried value. Using a query function could, for instance, look like this:

Dir = arg_maybe_value(Arguments, cla("dir"))

When the type of the argument value isn’t clear from the context, you will get a compile time error. In this case, you need to explicitly specify the type. Like this:

Dir = arg_maybe_value(Arguments, cla("dir")) : maybe(string)

Errors and error messages

The user can make errors. For instance, supplying a value to an argument that doesn’t take one, or supplying a value that isn’t a well-formed, parseable representation of the argument’s type. The programmer can make errors as well - bugs. For instance, by defining an argument with contradictory properties, or by querying an argument with a query function for the wrong type. Not all programmer errors can be detected at compile time.

This means that there are two types, which can be thrown by Colipa: command_line_error for errors that the user makes, and arg_desc_error for errors by the programmer. Both can be triggered by the colipa/5 predicate and by a query function.

You catch them like this:

try [io(!IO)] ...
then ...
catch (ADE : arg_desc_error) ->
    print_on_terminal("Bug found: " ++ ade_message(ADE) ++ "\n", !IO)
catch (CLE : command_line_error) ->
    print_on_terminal(cle_message(CLE) ++ "\n", !IO).

You can also catch individual errors like this:

catch (cle_missing_value(ArgDesc, No, Last) : command_line_error) ->
    ...

The print_on_terminal predicate determines the width of the terminal (80 is used if not connected to a terminal) and wraps lines such that no words are split up. cle_message/1 and ade_message/1 make an error message, from a command_line_error or arg_desc_error, respectively.

The predicate which catches the exceptions must have the cc_multi determinism category. See Exception Handling in the Mercury Language Reference Manual.

Automatically generated usage information

Usage information about the command line arguments can be generated automatically from the argument descriptions. Use the prop_description property to add a descriptive text. This text should not be broken down into lines by newline characters. Colipa does that by itself. But newlines are allowed and will be respected.

To print a usage summary on stdout, use this predicate:

print_usage_info(argdescl, !IO)

There are more and more flexible predicates to do this. You can also sort the argument list by the name of the first long argument:

sort_argdescl(argdescl, ArgDescL2),
print_usage_info(ArgDescL2, !IO)

Here’s an example of what it looks like, in a terminal of width 58. Color isn’t shown here:

-v                 This argument can be given up to three
                   times.
-a  --archive      This is a switch. It can be specified
                   zero or one times. It's value is a
                   boolean.
-c  --count Count  This argument takes an integer. It
                   also has a default and a checker.
    --dir Dir      Name of the directory.
    --float F      This is an example of an argument that
                   takes a floating point number as its
                   value.
...

New argument types

By default, Colipa supports the argument types string, int and float. You can add new argument types easily by instantiating the argument typeclass. You need to define a parser for that type, which goes into the from_str method. It will return fs_ok(Val) for a successful parse, and fs_error(Msg) for a parse error, where Msg is an error message. When the parse failed, Colipa will throw a cle_malformed_value exception, which includes the error message.

This is an example:

:- instance argument(bool) where [
        from_str(Str) = Res :-
            ( if        Str = "yes" 
              then      Res = fs_ok(yes)

              else if   Str = "no" 
              then      Res = fs_ok(no)

              else      Res = fs_error("Expected ""yes"" or ""no"".")
            )
    ].

Group Constraints

While argument descriptions can place constraints and checks on individual arguments, they can’t place restrictions on groups of arguments. For this purpose, group constraints predicates have been added. They begin with args_. They each check something about several arguments and throw a command_line_error if the constraint isn’t observed.

Checkers

Checkers check an argument value from the command line, after it has been successfully parsed. This means, they place additional constraints on a command line argument. For instance, there are two predefined checkers in Colipa, called nonneg_integer and positive_integer, respectively. They check an integer for being non-negative or positive (one or bigger). When a checker fails, a cle_invalid_value error is thrown.

You can define a checker with the make_checker function. For instance, nonneg_integer is defined like this:

nonneg_integer = make_checker(nni).

:- pred nni(int::in, maybe(string)::out) is det.

nni(I, Err) :-
    (  if I < 0 then Err = yes("Non-negative integer expected.")
                else Err = no ).

make_checker takes a predicate, which checks the value and returns an error message in case the check failed. This message is included in the cle_invalid_value exception. The type of make_checker is:

:- func make_checker(
      pred(T, maybe(string))::pred(in, out) is det
      ) = (checker::out) is det
      <=  argument(T).

Checkers are added to a command line argument with the prop_check property, such as prop_check(nonneg_integer).

Command line evaluation framework for programs with subcommands

Colipa provides a simple framework for dealing with command line arguments and subcommands.

The program/6 predicate strives to unburden you when your program uses a subcommand. It is passed a stripped down version of your main predicate. Part of the subcommand and command line handling is done by program/6, so you don’t have to do it there. It passes the following pieces to your new main predicate:

It handles the special subcommand “help” and prints usage information (including subcommands and command line arguments). It also handles invalid subcommands. If no subcommand is specified, a short message is printed, which tells the user to call the program with “help”.

See colipa.m for the invocation details of program/6.

The framework also provides a predicate catch_cle_ade/5:

:- pred catch_cle_ade(
    int::in,                                    % Exit code for command line error
    int::in,                                    % Exit code for argument description error
    pred(io, io)::in(pred(di, uo) is cc_multi), % Predicate to wrap
    io::di, io::uo
) is cc_multi.

This wraps some IO predicate and deals with command_line_errors and arg_desc_errors by printing an error message to stderr and signaling the program to exit. This signaling is done by throwing a value of this type:

:- type shutdown ---> shutdown(int).

The enclosed integer is the intended exit code of the program. The top-level main program is meant to catch such shutdown exceptions, set the exit code and leave. Like this:

main(!IO) :-
    try [io(!IO)]
        main1(!IO)
    then true
    catch (colipa.shutdown(ExitCode)) ->
        io.set_exit_status(ExitCode, !IO).

The predicate main1/2 here is the rest of the main program. It typically begins with a call to catch_cle_ade/5.