Quarrel User Manual 1.6.1

Book Metadata

DATE | 2023-05-12T10:12:20+00:00
DESCRIPTION | User manual for the quarrel package.
IDENTIFIER | ce8c7e2f-3469-45fa-8ffa-4fc1ab2412d2
LANGUAGE | en
SOURCE | https://www.io7m.com/software/quarrel/
TITLE | Quarrel User Manual 1.6.1

License

Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com

This book is placed into the public domain for free use by anyone for any purpose. It may be freely used, modified, and distributed.

In jurisdictions that do not recognise the public domain this book may be freely used, modified, and distributed without restriction.

This book comes with absolutely no warranty.

Table Of Contents

Cover

Colophon

Table Of Contents

1. Introduction

2. Usage

2.1. Overview

2.2. Design

2.3. Implementing QCommandType

2.3.1. Overview

2.3.2. Command Metadata

2.3.3. Named Parameters

2.3.4. Positional Parameters

2.3.5. Executable

2.4. QStrings

2.5. Creating A QApplicationType

2.6. Calling The QApplicationType

2.7. Hidden Commands

2.8. The help command

2.9. The version command

2.10. @ Syntax

2.11. Value Converters

3. Example Application

4. JavaDoc

1. Introduction

The quarrel package provides a strict, opinionated command-line argument parser intended for producing consistent command-line applications.

1.2. Features

Strongly-typed access to command-line arguments, for program correctness.

Simple and regular command-line parsing for easily understood command-line interfaces.

Automatic generation of "help" and "version" commands for command-line interfaces.

Detailed, structured, and localized user-facing error messages for clear explanations as to how to use the command-line interfaces correctly.

A small, easily auditable codebase with no use of reflection or annotations.

An extensive automated test suite with high coverage.

Supplies a restricted form of @ syntax, for storing command-line arguments in files.

Fully documented, with usage tutorial.

OSGi-ready

JPMS-ready

ISC license.

2. Usage

2.1. Overview

2.2. Design

2.3. Implementing QCommandType

2.3.1. Overview

2.3.2. Command Metadata

2.3.3. Named Parameters

2.3.4. Positional Parameters

2.3.5. Executable

2.4. QStrings

2.5. Creating A QApplicationType

2.6. Calling The QApplicationType

2.7. Hidden Commands

2.8. The help command

2.9. The version command

2.10. @ Syntax

2.11. Value Converters

2.1. Overview

The quarrel package provides a simple API for producing command-line applications. Using the package consists of the following steps:

2.1.2. Usage Steps

Provide one implementation of QCommandType for each of the commands exposed by your application.

Create an instance of QApplicationType, registering each of the commands (or, optionally, grouping them into command groups). The application instance must also be provided metadata such as the name and version of the application.

Pass arguments to the application instance and either execute the returned command, or display the returned error information.

2.2. Design

The quarrel package does not intend to provide a fully generic command-line parser that can emulate the argument parsing style of every application ever conceived. It is patterned on a strict subset of the kind of command-line parsing used by programs such as podman.

Briefly, the command line interface is divided into discrete commands. Commands may be grouped into command groups. A command accepts a possibly empty list of named parameters, followed by a possibly empty list of positional parameters. Named parameters have a configurable cardinality; a named parameter can be declared as "must be specified exactly one", "may be specified any number of times", "must be specified at least once", and so on. A named parameter must be supplied with a value at run-time, but can also be specified to have a given default value if one is not provided. Both positional and named parameters are typed, with values being parsed by looking up a parser in a directory of value converters. The package comes with a useful set of value converters for many of the standard Java types, but new converters can be registered by the application developer.

The quarrel package provides an application abstraction that, given metadata such as the application name and version, automatically provides useful commands such as help and version to relieve the application developer of the burden of having to implement such standard commands. The application abstraction also handles the dispatching of arguments to a tree of nested commands/command groups.

The quarrel package also provides detailed, localized, and structured error reporting to assist users of command-line applications built using quarrel. For example, if the user fails to provide a required named argument, or provides a value that cannot be parsed as a value of the argument's declared type, they will receive an error message such as the following:

2.2.5. Example Error Messages

quarrel: ERROR: The wrong number of values were provided for a parameter.
  Command          : example
  Error Code       : parameter-cardinality
  Maximum Values   : 1
  Minimum Values   : 1
  Parameter        : --number
  Provided Count   : 0
  Suggested Action : Provide the right number of arguments for the parameter.

quarrel: ERROR: The value supplied for a parameter is unparseable.
  Command          : example
  Error Code       : parameter-unparseable-value
  Parameter        : --number
  Provided         : x
  Suggested Action : Provide a parseable value on the command-line.
  Syntax           : 0 | [1-9][0-9]+
  Type             : java.lang.Integer

2.3. Implementing QCommandType

2.3.1. Overview

Application programmers must provide an implementation of the QCommandType interface for each command in the application.

In order to implement the QCommandType interface, the following pieces of information must be provided:

2.3.1.3. QCommandType Requirements

The command metadata such as the name of the command, the short one-line description of the command for help messages, the optional long description of the command, and etc.

The set of named parameters the command accepts, if any.

The set of positional parameters the command accepts, if any.

The actual code executed when the command is invoked.

2.3.2. Command Metadata

The metadata for the command consists of the name of the command, a short one-line description of the command used in generated help messages, and an optional long description also used in help messages. The metadata must be returned by the implementation of the metadata method:

2.3.2.2. Metadata method

@Override
public QCommandMetadata metadata()
{
  return new QCommandMetadata(
    "cat",
    new QConstant("Hear a cat speak."),
    Optional.empty()
  );
}

Localizable strings such as the descriptions are specified as values of the QStringsType type so that the quarrel package knows whether to try to look up their value in resources, or use their string constant values directly. Command names are not localizable, because having a different command-line interface depending on locale would create severe problems when using applications from scripts.

Command names cannot begin with the @ character, because this would conflict with the provided @ syntax.

Command names cannot contain any characters in the Unicode \p{Z} set. This includes any kind of whitespace or invisible separator.

Command names must be non-empty.

2.3.3. Named Parameters

A command may accept zero or more named parameters. A command implementation must return zero or more parameter declarations from the onListNamedParameters method. The values returned are used later by the implemented onExecute method to retrieve the parsed values supplied to those parameters.

A named parameter is an instance of the sealed QParameterNamedType. Every named parameter has a name, a type, and descriptive metadata used for help messages. Named parameters may also declare alternative names (for example, to provide short versions of parameters such as -f in addition to --file). Parameter names must be unique within a given command (including alternative names). Each of the subclasses of QParameterNamedType encodes a different parameter cardinality:

2.3.3.3. Parameter Cardinality

Type | Cardinality
QParameterNamed0N | The parameter may be specified zero or more times, accumulating values into a list.
QParameterNamed1 | The parameter must be specified exactly once.
QParameterNamed1N | The parameter must be specified at least once, accumulating values into a list.
QParameterNamed01 | The parameter may be specified at most once, yielding an optional value.

2.3.3.4. onListNamedParameters method

private static final QParameterNamed1<Path> FILE =
  new QParameterNamed1<>(
    "--file",
    List.of("-f"),
    new QConstant("The input file."),
    Optional.empty(),
    Path.class
  );

private static final QParameterNamed01<Integer> COUNT =
  new QParameterNamed01<>(
    "--count",
    List.of(),
    new QConstant("The number of items to process."),
    Optional.of(100),
    Integer.class
  );

@Override
public List<QParameterNamedType<?>> onListNamedParameters()
{
  return List.of(FILE, COUNT);
}

A named parameter may have a value of any type for which there is an appropriate value converter registered.

Parameter names cannot begin with the @ character, because this would conflict with the provided @ syntax.

Parameter names cannot contain any characters in the Unicode \p{Z} set. This includes any kind of whitespace or invisible separator.

Parameter names must be non-empty.

2.3.4. Positional Parameters

A command may accept zero or more positional parameters. A command implementation must return a value of the QParametersPositionalType type from the implemented onListPositionalParameters method that indicates how the command accepts positional parameters:

2.3.4.2. Parameter Positionals

Type | Meaning
QParametersPositionalAny | Any sequence of positional parameters are acceptable. Essentially, no parsing occurs.
QParametersPositionalNone | No positional parameters are allowed. Passing anything results in an error.
QParametersPositionalTyped | The command requires the exact sequence of given parameters.

As with named parameters, positional parameters must have names and types, although the names are only used for documentation and are otherwise ignored. If a sequence of positional parameters are provided, all parameters must be supplied on the command-line; positional parameters cannot be optional - use named parameters for optional parameters.

2.3.4.4. onListNamedParameters method (None)

@Override
public QParametersPositionalType onListPositionalParameters()
{
  return new QParametersPositionalNone();
}

2.3.4.5. onListNamedParameters method (Any)

@Override
public QParametersPositionalType onListPositionalParameters()
{
  return new QParametersPositionalAny();
}

2.3.4.6. onListNamedParameters method (Typed)

private static final QParameterPositional<Path> INPUT =
  new QParameterPositional<>(
    "input",
    new QConstant("The input file."),
    Path.class
  );

private static final QParameterPositional<Path> OUTPUT =
  new QParameterPositional<>(
    "output",
    new QConstant("The output file."),
    Path.class
  );

@Override
public QParametersPositionalType onListPositionalParameters()
{
  return new QParametersPositionalTyped(List.of(INPUT, OUTPUT));
}

A positional parameter may have a value of any type for which there is an appropriate value converter registered.

Parameter names cannot begin with the @ character, because this would conflict with the provided @ syntax.

Parameter names cannot contain any characters in the Unicode \p{Z} set. This includes any kind of whitespace or invisible separator.

Parameter names must be non-empty.

2.3.5. Executable

The command implementation must implement the onExecute method. This method contains the actual code executed by the application after the command is invoked and all argument parsing has succeeded. The method is passed a value of type QCommandContextType that provides access to:

2.3.5.2. Command Context

The PrintWriter output to which the command can write output. In typical application, this is attached to the standard output, but commands should use this instead of writing to standard output in order to facilitate unit testing of the commands.

The values of the parsed named parameters (if any).

The values of the parsed positional parameters.

To obtain access to the values of named parameters, the application code can call any of the various context methods, passing in the original parameter declarations. For example:

2.3.5.4. Named Parameter Values

@Override
public QCommandStatus onExecute(
  final QCommandContextType context)
{
  final var w = context.output();

  final Path file = context.parameterValue(FILE);
  w.println(file);

  final Integer count = context.parameterValue(COUNT);
  w.println(count);

  final Path input = context.parameterValue(INPUT);
  w.println(input);

  final Path output = context.parameterValue(OUTPUT);
  w.println(output);

  w.flush();
  return SUCCESS;
}

The command context contains methods specialized to each cardinality subclass in order to return values of an appropriate collection type without requiring the programmer to do any unsafe casting. For example a parameter declared with a value of type T with a cardinality of "exactly one" will return a value of T directly. However, a parameter declared with a value of type T with a cardinality of "zero or more times" will return a value of List<T>.

The onExecute method is defined to allow the raising of any subclass of Exception, but is also defined to require the returning of a value of type QCommandStatus. The value QCommandStatus.SUCCESS indicates that the command succeeded, and the value QCommandStatus.FAILURE indicates that the command failed. This may seem slightly counterintuitive, after all raising an exception clearly indicates failure. However, consider the standard POSIX utilities such as grep; the grep command returns a non-zero exit code on the command line if the input did not match a given regular expression. The command did not "fail" in a way that would justify raising an exception (such as the user asking grep to open a file that turned out not to exist), but it did return some feedback that justified adjusting the exit code such that scripts can easily observe the results without having to parse the command's output.

2.4. QStrings

The quarrel package has to accept and display arbitrary strings returned from code implemented by users. In order to properly support localization, the package needs to know whether it needs to localize a given returned string. The QStringType can refer to either of the following types:

2.4.2. QString Types

Type | Behaviour
QConstant | The value of the constant is used directly, without any kind of localization.
QLocalize | The value will be localized from the application's resources. The value will ultimately be used as a key to index into a ResourceBundle.

2.5. Creating A QApplicationType

The quarrel package provides a top-level application type that acts as a container for a tree of commands and command groups. Application developers should instantiate a value of type QApplicationType using the provided mutable builder type.

First, place the application metadata into a value of type QApplicationMetadata:

2.5.3. Metadata

final var metadata =
  new QApplicationMetadata(
    "quarrel",
    "com.io7m.quarrel.example",
    "1.2.0",
    "eacd59a2",
    "The Quarrel example application.",
    Optional.of(URI.create("https://www.io7m.com/software/quarrel/"))
  );

Create an application builder and register some commands, and set the application's string resources (used to localize the strings returned by the application's commands):

2.5.5. Builder

final ResourceBundle resources = ...;

final var builder = QApplication.builder(metadata);
builder.setApplicationResources(resources);
builder.addCommand(new Command0());
builder.addCommand(new Command1());
builder.addCommand(new Command2());

For each QString the quarrel package encounters, it first tries to localize the string using the application's provided resource bundle. If the string does not exist there, it tries to localize the string using its own internal resource bundle.

Optionally, commands can be placed into groups:

2.5.8. Groups

final var group =
  builder.createCommandGroup(
    new QCommandMetadata(
      "animal",
      new QConstant("Hear an animal speak."),
      Optional.of(new QConstant("A long description."))
    ));

group.addCommand(new CommandCat());
group.addCommand(new CommandDog());
group.addCommand(new CommandCow());

Groups and commands form a tree structure, with the root of the tree effectively being an anonymous command group. The command-line arguments passed to an application instance are consumed left to right in order to walk the tree and locate a specific command. For example, if the given command line arguments are podman system connection list -q, assuming that the first argument "podman" is stripped away [1], then the application receives the arguments "system" "connection" "list" "-q". The application will look for a top-level command or group named "system". If "system" names a command, the command will be passed the arguments "connection" "list" "-q" and the search terminates. If "system" names a command group, then the command group will be inspected for a command or group named "connection". This procedure continues recursively until either a command is located, a group doesn't contain the given name (resulting in an error), or the argument list is exhausted (resulting in an error).

Finally, build an application. Application instances are immutable.

2.5.11. Groups

final var application = builder.build();

2.6. Calling The QApplicationType

Given the application instance constructed previously, the instance can be passed a set of command line arguments. The application will either raise an exception indicating that arguments could not be parsed or were incorrect in some manner, or it will return a command context. The command context can be used to execute the parsed command.

2.6.2. Execute

List<String> arguments = ...;

try {
  var cmd = application.parse(arguments);
  return result = cmd.execute();
} catch (final Exception e) {
  // Log errors
  return QCommandStatus.FAILURE;
}

The quarrel package uses a QException type containing structured error information to produce useful error messages. The QApplicationType type contains a convenience method named run that effectively implements the above code with some provided error formatting. Given an SLF4J logger, the run method can reduce the above code to:

2.6.4. Run

Logger LOG = ... ;

return application.run(LOG, arguments)

Executing the example program with no arguments yields a generated usage page:

2.6.6. Example Usage

$ quarrel
quarrel: usage: quarrel [command] [arguments ...]

  The Quarrel example application.

  Use the "help" command to examine specific commands:

    $ quarrel help help.

  Command-line arguments can be placed one per line into a file, and
  the file can be referenced using the @ symbol:

    $ echo help > file.txt
    $ echo help >> file.txt
    $ quarrel @file.txt

  Commands:
    animal      Hear an animal speak.
    cmd-0       A command that does nothing.
    cmd-1       A command that does nothing.
    cmd-1       A command that does nothing.
    help        Show usage information for a command.
    version     Show the application version.

  Documentation:
    https://www.io7m.com/software/quarrel/

2.7. Hidden Commands

The QCommandType interface contains a default method isHidden that, if implemented to return true, will cause the command implementing it to be hidden in the listings produced by the help command. This can be useful if an application contains internal commands that are not intended to be user-facing.

The default behaviour for the method is to return false, thus making commands visible.

2.8. The help command

The help command is registered at the top level of the resulting application and can produce detailed usage messages when given the name of an existing command. For example, the cmd-everything command from the example application yields the following output when inspected using help cmd-everything:

2.8.2. help cmd-everything

quarrel: usage: cmd-everything [named-arguments ...] <x> <y> <z>

  A command with everything.

  Named parameters:
    --0file
      Description       : A file.
      Type              : String
      Cardinality       : [0, 1]; Specify at most once.
      Syntax            : <any sequence of characters>
      Alternative names : -x, -y, -z
    --1number
      Description       : A number.
      Type              : Integer
      Cardinality       : [0, 1]; Specify at most once, or use the default.
      Default value     : 23
      Syntax            : 0 | [1-9][0-9]+
  * --2number-opt
      Description       : A number.
      Type              : Integer
      Cardinality       : [1]; Specify exactly once.
      Syntax            : 0 | [1-9][0-9]+
    --3date
      Description       : A date.
      Type              : OffsetDateTime
      Cardinality       : [1]; Specify exactly once, or use the default.
      Default value     : 2023-05-12T11:11:22.138315473Z
      Syntax            : yyyy-mm-ddThh:mm:ss+zz:zz (ISO 8601)
    --4net
      Description       : A network address.
      Type              : InetAddress
      Cardinality       : [0, N]; Specify zero or more times.
      Syntax            : Hostname, IPv4 or IPv6 address (RFC 2732)
    --5uuid
      Description       : A UUID.
      Type              : UUID
      Cardinality       : [0, N]; Specify at least once, or use the default.
      Default value     : [5552215f-253f-4391-bd07-7d87f57572ce]
      Syntax            : [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
    --6path
      Description       : A path.
      Type              : Path
      Cardinality       : [1, N]; Specify at least once.
      Syntax            : <platform-specific path syntax>
    --7uri
      Description       : A URI.
      Type              : URI
      Cardinality       : [1, N]; Specify at least once, or use the default.
      Default value     : urn:x
      Syntax            : RFC 3986 URI

  Positional parameters:
    x
      Type              : Integer
      Description       : An x.
      Syntax            : 0 | [1-9][0-9]+
    y
      Type              : Integer
      Description       : A y.
      Syntax            : 0 | [1-9][0-9]+
    z
      Type              : Integer
      Description       : A z.
      Syntax            : 0 | [1-9][0-9]+

  Farmer Bertram was in bed when the stranger entered, having had a fall
  from his horse while hunting.

  The horseman said his business was of such pressing importance that he
  must see the farmer at once.

  Bertram recognized the name, and directed his old servant to admit the
  stranger to his chamber at once.

2.9. The version command

The version command is registered at the top level of the resulting application and prints version information based on the metadata provided to the application instance. For example, the version command from the example application yields the following output:

2.9.2. version

com.io7m.quarrel.example 1.2.0 eacd59a2

The command outputs the application ID, followed by a space, followed by the version number, followed by a space, followed by the build identifier, followed by a newline.

2.10. @ Syntax

If the first character of the first argument passed to the application is @, then the argument (minus the @ character) is interpreted as a filename containing command-line arguments for the application.

The file named using @ is read line-by-line. Lines beginning with the character # are ignored. Leading and trailing whitespace is trimmed from each line, and empty/blank lines are ignored. After all lines have been read, each line is interpreted as being a single argument, and the resulting list of arguments is prepended onto the arguments following the @ argument.

The processing of @ syntax may be disabled by setting allowAtSyntax to false on the application builder.

2.11. Value Converters

A value converter is a class that can convert values to and from strings. Essentially, they combine a parsing function with a serialization function, and also allow for returning some descriptive information such as the expected syntax of string values. Value converters are collected into a value converter directory, represented by an instance of the QValueConverterDirectoryType type. The directory can be thought of as a map from Java Class values to values of type QValueConverterType.

The quarrel package includes a standard directory covering many of the scalar types within the JDK. The list includes but is not necessarily limited to:

2.11.3. Standard Types

java.lang.Boolean
java.lang.Double
java.lang.Float
java.lang.Integer
java.lang.Long
java.lang.String
java.math.BigDecimal
java.math.BigInteger
java.net.InetAddress
java.net.URI
java.nio.file.Path
java.time.Duration
java.time.OffsetDateTime
java.util.UUID

Additionally, the default implementation of the QValueConverterDirectoryType automatically supports the use of any Java enum type.

Footnotes

1

As it is the name of the program, and this is not present in the argument array passed to the mainfunction in Java.

References to this footnote: 1

3. Example Application

The full sources of the example application are as follows:

3.2. ExCat.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
import com.io7m.quarrel.core.QCommandType;
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QParametersPositionalNone;
import com.io7m.quarrel.core.QParametersPositionalType;
import com.io7m.quarrel.core.QStringType.QConstant;

import java.util.List;
import java.util.Optional;

/**
 * An example command.
 */

public final class ExCat implements QCommandType
{
  /**
   * An example command.
   */

  public ExCat()
  {

  }

  @Override
  public QCommandMetadata metadata()
  {
    return new QCommandMetadata(
      "cat",
      new QConstant("Hear a cat speak."),
      Optional.empty()
    );
  }

  @Override
  public List<QParameterNamedType<?>> onListNamedParameters()
  {
    return List.of();
  }

  @Override
  public QParametersPositionalType onListPositionalParameters()
  {
    return new QParametersPositionalNone();
  }

  @Override
  public QCommandStatus onExecute(
    final QCommandContextType context)
  {
    final var w = context.output();
    w.println("Meow.");
    w.flush();
    return QCommandStatus.SUCCESS;
  }
}

3.3. ExCmd0.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
import com.io7m.quarrel.core.QCommandType;
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QParametersPositionalNone;
import com.io7m.quarrel.core.QParametersPositionalType;
import com.io7m.quarrel.core.QStringType.QLocalize;

import java.util.List;
import java.util.Optional;

import static com.io7m.quarrel.core.QCommandStatus.SUCCESS;

/**
 * An example command.
 */

public final class ExCmd0 implements QCommandType
{
  /**
   * An example command.
   */

  public ExCmd0()
  {

  }

  @Override
  public QCommandMetadata metadata()
  {
    return new QCommandMetadata(
      "cmd-0",
      new QLocalize("cmd-0.short"),
      Optional.of(new QLocalize("cmd-0.long"))
    );
  }

  @Override
  public List<QParameterNamedType<?>> onListNamedParameters()
  {
    return List.of();
  }

  @Override
  public QParametersPositionalType onListPositionalParameters()
  {
    return new QParametersPositionalNone();
  }

  @Override
  public QCommandStatus onExecute(
    final QCommandContextType context)
  {
    return SUCCESS;
  }
}

3.4. ExCow.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
import com.io7m.quarrel.core.QCommandType;
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QParametersPositionalNone;
import com.io7m.quarrel.core.QParametersPositionalType;
import com.io7m.quarrel.core.QStringType.QConstant;

import java.util.List;
import java.util.Optional;

/**
 * An example command.
 */

public final class ExCow implements QCommandType
{
  /**
   * An example command.
   */

  public ExCow()
  {

  }

  @Override
  public QCommandMetadata metadata()
  {
    return new QCommandMetadata(
      "cow",
      new QConstant("Hear a cow speak."),
      Optional.empty()
    );
  }

  @Override
  public List<QParameterNamedType<?>> onListNamedParameters()
  {
    return List.of();
  }

  @Override
  public QParametersPositionalType onListPositionalParameters()
  {
    return new QParametersPositionalNone();
  }

  @Override
  public QCommandStatus onExecute(
    final QCommandContextType context)
    throws Exception
  {
    final var w = context.output();
    w.println("Moo.");
    w.flush();
    return QCommandStatus.SUCCESS;
  }
}

3.5. ExDog.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
import com.io7m.quarrel.core.QCommandType;
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QParametersPositionalNone;
import com.io7m.quarrel.core.QParametersPositionalType;
import com.io7m.quarrel.core.QStringType.QConstant;

import java.util.List;
import java.util.Optional;

/**
 * An example command.
 */

public final class ExDog implements QCommandType
{
  /**
   * An example command.
   */

  public ExDog()
  {

  }

  @Override
  public QCommandMetadata metadata()
  {
    return new QCommandMetadata(
      "dog",
      new QConstant("Hear a dog speak."),
      Optional.empty()
    );
  }

  @Override
  public List<QParameterNamedType<?>> onListNamedParameters()
  {
    return List.of();
  }

  @Override
  public QParametersPositionalType onListPositionalParameters()
  {
    return new QParametersPositionalNone();
  }

  @Override
  public QCommandStatus onExecute(
    final QCommandContextType context)
    throws Exception
  {
    final var w = context.output();
    w.println("Bark.");
    w.flush();
    return QCommandStatus.SUCCESS;
  }
}

3.6. ExEverything.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
import com.io7m.quarrel.core.QCommandType;
import com.io7m.quarrel.core.QParameterNamed01;
import com.io7m.quarrel.core.QParameterNamed0N;
import com.io7m.quarrel.core.QParameterNamed1;
import com.io7m.quarrel.core.QParameterNamed1N;
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QParameterPositional;
import com.io7m.quarrel.core.QParametersPositionalType;
import com.io7m.quarrel.core.QParametersPositionalTyped;
import com.io7m.quarrel.core.QStringType.QConstant;

import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static com.io7m.quarrel.core.QCommandStatus.SUCCESS;

/**
 * An example command.
 */

public final class ExEverything implements QCommandType
{
  private static final QParameterNamed01<String> PARAMETER_0 =
    new QParameterNamed01<>(
      "--0file",
      List.of("-x", "-y", "-z"),
      new QConstant("A file."),
      Optional.empty(),
      String.class
    );

  private static final QParameterNamed01<Integer> PARAMETER_1 =
    new QParameterNamed01<>(
      "--1number",
      List.of(),
      new QConstant("A number."),
      Optional.of(23),
      Integer.class
    );

  private static final QParameterNamed1<Integer> PARAMETER_2 =
    new QParameterNamed1<>(
      "--2number-opt",
      List.of(),
      new QConstant("A number."),
      Optional.empty(),
      Integer.class
    );

  private static final QParameterNamed1<OffsetDateTime> PARAMETER_3 =
    new QParameterNamed1<>(
      "--3date",
      List.of(),
      new QConstant("A date."),
      Optional.of(OffsetDateTime.now()),
      OffsetDateTime.class
    );

  private static final QParameterNamed0N<InetAddress> PARAMETER_4 =
    new QParameterNamed0N<>(
      "--4net",
      List.of(),
      new QConstant("A network address."),
      List.of(),
      InetAddress.class
    );

  private static final QParameterNamed0N<UUID> PARAMETER_5 =
    new QParameterNamed0N<>(
      "--5uuid",
      List.of(),
      new QConstant("A UUID."),
      List.of(UUID.randomUUID()),
      UUID.class
    );

  private static final QParameterNamed1N<Path> PARAMETER_6 =
    new QParameterNamed1N<>(
      "--6path",
      List.of(),
      new QConstant("A path."),
      Optional.empty(),
      Path.class
    );

  private static final QParameterNamed1N<URI> PARAMETER_7 =
    new QParameterNamed1N<>(
      "--7uri",
      List.of(),
      new QConstant("A URI."),
      Optional.of(URI.create("urn:x")),
      URI.class
    );

  private static final QParameterPositional<Integer> P_PARAMETER_0 =
    new QParameterPositional<>(
      "x",
      new QConstant("An x."),
      Integer.class
    );

  private static final QParameterPositional<Integer> P_PARAMETER_1 =
    new QParameterPositional<>(
      "y",
      new QConstant("A y."),
      Integer.class
    );

  private static final QParameterPositional<Integer> P_PARAMETER_2 =
    new QParameterPositional<>(
      "z",
      new QConstant("A z."),
      Integer.class
    );

  private static final String TEXT = """
Farmer Bertram was in bed when the stranger entered, having had a fall
from his horse while hunting.

The horseman said his business was of such pressing importance that he
must see the farmer at once.

Bertram recognized the name, and directed his old servant to admit the
stranger to his chamber at once.
    """;

  /**
   * An example command.
   */

  public ExEverything()
  {

  }

  @Override
  public List<QParameterNamedType<?>> onListNamedParameters()
  {
    return List.of(
      PARAMETER_0,
      PARAMETER_1,
      PARAMETER_2,
      PARAMETER_3,
      PARAMETER_4,
      PARAMETER_5,
      PARAMETER_6,
      PARAMETER_7
    );
  }

  @Override
  public QParametersPositionalType onListPositionalParameters()
  {
    return new QParametersPositionalTyped(
      List.of(P_PARAMETER_0, P_PARAMETER_1, P_PARAMETER_2)
    );
  }

  @Override
  public QCommandMetadata metadata()
  {
    return new QCommandMetadata(
      "cmd-everything",
      new QConstant("A command with everything."),
      Optional.of(new QConstant(TEXT))
    );
  }

  @Override
  public QCommandStatus onExecute(
    final QCommandContextType context)
  {
    final var w = context.output();
    w.println(context.parameterValue(PARAMETER_0));
    w.println(context.parameterValue(PARAMETER_1));
    w.println(context.parameterValue(PARAMETER_2));
    w.println(context.parameterValue(PARAMETER_3));
    w.println(context.parameterValues(PARAMETER_4));
    w.println(context.parameterValues(PARAMETER_5));
    w.println(context.parameterValues(PARAMETER_6));
    w.println(context.parameterValues(PARAMETER_7));
    w.println(context.parameterValue(P_PARAMETER_0));
    w.println(context.parameterValue(P_PARAMETER_1));
    w.println(context.parameterValue(P_PARAMETER_2));
    w.flush();
    return SUCCESS;
  }
}

3.7. ExMeta.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
import com.io7m.quarrel.core.QCommandType;
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QParametersPositionalAny;
import com.io7m.quarrel.core.QParametersPositionalType;
import com.io7m.quarrel.core.QStringType.QConstant;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

import static com.io7m.quarrel.core.QCommandStatus.SUCCESS;

/**
 * The meta command.
 */

public final class ExMeta implements QCommandType
{
  /**
   * The meta command.
   */

  public ExMeta()
  {

  }

  @Override
  public List<QParameterNamedType<?>> onListNamedParameters()
  {
    return List.of();
  }

  @Override
  public QParametersPositionalType onListPositionalParameters()
  {
    return new QParametersPositionalAny();
  }

  @Override
  public QCommandStatus onExecute(
    final QCommandContextType context)
    throws Exception
  {
    final var args = context.parametersPositionalRaw();
    if (!args.isEmpty()) {
      switch (args.get(0)) {
        case "converters" -> {
          showConverters(context);
        }
        default -> {

        }
      }
    }

    return SUCCESS;
  }

  private static void showConverters(
    final QCommandContextType context)
  {
    final var w = context.output();
    final var cs = new ArrayList<>(context.valueConverters().converters());
    cs.sort(Comparator.comparing(o -> o.convertedClass().getCanonicalName()));

    for (final var c : cs) {
      w.println(c.convertedClass().getCanonicalName());
    }

    w.flush();
  }

  @Override
  public QCommandMetadata metadata()
  {
    return new QCommandMetadata(
      "meta",
      new QConstant("Display hidden application metadata."),
      Optional.empty()
    );
  }
}

3.8. ExStrings.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.jxtrand.vanilla.JXTAbstractStrings;

import java.io.IOException;
import java.util.Locale;

/**
 * The string resources.
 */

public final class ExStrings extends JXTAbstractStrings
{
  /**
   * The string resources.
   *
   * @param locale The application locale
   *
   * @throws IOException On I/O errors
   */

  public ExStrings(
    final Locale locale)
    throws IOException
  {
    super(
      locale,
      ExStrings.class,
      "/com/io7m/quarrel/example",
      "Messages"
    );
  }
}

3.9. Main.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.quarrel.example;

import com.io7m.quarrel.core.QApplication;
import com.io7m.quarrel.core.QApplicationMetadata;
import com.io7m.quarrel.core.QApplicationType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QStringType.QConstant;
import com.io7m.quarrel.ext.xstructural.QCommandXS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

/**
 * The main program.
 */

public final class Main implements Runnable
{
  private static final Logger LOG =
    LoggerFactory.getLogger(Main.class);

  private final List<String> args;
  private final QApplicationType application;
  private int exitCode;

  /**
   * The main entry point.
   *
   * @param inArgs Command-line arguments
   */

  public Main(
    final String[] inArgs)
  {
    try {
      this.args =
        Objects.requireNonNull(List.of(inArgs), "Command line arguments");

      final var metadata =
        new QApplicationMetadata(
          "quarrel",
          "com.io7m.quarrel.example",
          "1.2.0",
          "eacd59a2",
          "The Quarrel example application.",
          Optional.of(URI.create("https://www.io7m.com/software/quarrel/"))
        );

      final var resources =
        new ExStrings(Locale.getDefault()).resources();

      final var builder = QApplication.builder(metadata);
      builder.setApplicationResources(resources);
      builder.addCommand(new ExCmd0());
      builder.addCommand(new ExCmd1());
      builder.addCommand(new ExEverything());
      builder.addCommand(new ExMeta());
      builder.addCommand(new QCommandXS("xstructural", true));

      {
        final var group =
          builder.createCommandGroup(new QCommandMetadata(
            "animal",
            new QConstant("Hear an animal speak."),
            Optional.of(new QConstant("A long description."))
          ));

        group.addCommand(new ExCat());
        group.addCommand(new ExDog());
        group.addCommand(new ExCow());
      }

      this.application = builder.build();
      this.exitCode = 0;
    } catch (final IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  /**
   * The main entry point.
   *
   * @param args Command line arguments
   */

  public static void main(
    final String[] args)
  {
    System.exit(mainExitless(args));
  }

  /**
   * The main (exitless) entry point.
   *
   * @param args Command line arguments
   *
   * @return The exit code
   */

  public static int mainExitless(
    final String[] args)
  {
    final var cm = new Main(args);
    cm.run();
    return cm.exitCode();
  }

  /**
   * @return The exit code
   */

  public int exitCode()
  {
    return this.exitCode;
  }

  @Override
  public void run()
  {
    this.exitCode = this.application.run(LOG, this.args).exitCode();
  }

  @Override
  public String toString()
  {
    return String.format(
      "[Main 0x%s]",
      Long.toUnsignedString(System.identityHashCode(this), 16)
    );
  }
}

4. JavaDoc

The API is documented in the included JavaDoc.