Quick Start Tutorial: Properties as command line arguments

Let's take a properties file that we'll use in our application and then add library code to load the properties in the file and then override those properties on the command line.

Building The Examples

The finished examples for this section are kept in the examples/ directory and are not built by default; there are three sets of examples:

  • 01-apache-properties: Apache commons configuration implementation;
  • 02-java-properties: Java utils properties implementation; and
  • 03-com-typesafe-properties: com.typesafe HOCON implementation

Assuming the main API has been built, the first two can be built by changing directory into the examples/ directory and running mvn clean install.

To build the com.typesafe implementation, first build the library for com.typesafe implementation by changing directory into implementations/com.typesafe/ and running mvn clean install. Then change directory into examples/03-com-typesafe-properties/ and run mvn clean install.

The binaries to run the examples are as follows and kept in the target/appassembler/bin/ directory with the application names as follows:

Directory Applciation Name
examples/01-apache-properties apache-props-demo
examples/02-java-properties java-props-demo
examples/03-com-typesafe-properties com-typesafe-props-demo

The listed names are for the non-Windows binaries; the Windows quivalents will have the file extension .bat.

Properties Walk-Through

First, let's start by defining some properties for an imagined application for processing directories of media files and uploading the results to a server on a specific port using a defined protocol:

host.ip = localhost
host.port = 1234
host.protocol = https
strip.exif = false
file.mimeTypes = text/html, media/jpg
file.extensions = mp3, jpeg
in.dir = media/processing-in
out.dir = media/processing-out

The properties will be packaged into the application directly (using Maven this is in src/main/resources/) - while this means that the file (and therefore the properties) - cannot be modified directly, this isn't an issue - we're going to override the properties using values overridden from the command line.

Next we need to write our actual code within the body of the constructor method - in this example, the main class we're calling from is named ApachePropertiesDemo, using Apache commons-configuration2:


public static void main(String[] args) {
    new ApachePropertiesDemo(args);
}

public ApachePropertiesDemo(String[] args) {
    try {
        PropertiesBuilder<Configuration> builder = new PropertiesConfigurationBuilder();
        builder.addProperties(ApachePropertiesDemo.class.getResourceAsStream("/cliapp.props"));
        Configuration props = builder.build(args);
    } catch (Exception ex) {
        System.err.println(ex.getMessage());
    }
}

The above code is relatively simple. Starting wtih the first line within the try declaration, which creates an Apache Configuration properties builder. The second line adds the properties from our application. Finallly, on the third line, we call the builder to build with command line arguments passed in the the main(String[] args) method and obtain the converted properties.

There's still a bit more code to add - accessing the properties - but we'll leave that for the moment and continue the walk-through.

We can easily do the same but using Java properties instead of commons-configuration2 properties by replacing the code within the try with the following code - the main class in this example is the JavaPropertiesDemo class:

PropertiesBuilder<Properties> builder = new JavaPropertiesBuilder();
builder.addProperties(JavaPropertiesDemo.class.getResourceAsStream("/cliapp.props"));
Properties props = builder.build(args);

Note that the com.typesafe implementation named ComTypeSafePropertiesDemo is omitted from this section.

The addProperties(InputStream) call can be called multiple times in order to load properties from several different sources; in addition there's also the addProperties(File) API call to load properties directly from a file rather than an input stream. Both calls can be intermixed and called in any order for any number of properties files or streams.

Before overriding properties using command line arguments, let's invoke the baked-in help provided by the API by passing in a single argument: --help. The following output is produced:

usage: Default generated property help.
Auto-generated content.
    --file-extensions <arg>  Overrides property 'file.extensions', default
                             value 'mp3, jpeg'
    --file-mimeTypes <arg>   Overrides property 'file.mimeTypes', default
                             value 'text/html, media/jpg'
    --help                   Print this help then exit.
    --host-ip <arg>          Overrides property 'host.ip', default value
                             'localhost'
    --host-port <arg>        Overrides property 'host.port', default value
                             '1234'
    --host-protocol <arg>    Overrides property 'host.protocol', default
                             value 'https'
    --strip-exif <arg>       Overrides property 'strip.exif', default
                             value 'false'
End of auto-generated content.

Notice that the switches to override any given property are the same as the property name (ignoring the leading double-hyphen) but with non-alphanumeric characters replaced with hyphens. For example to override the host.protocol property, we supply the command line switch --host-protocol.

Also note that only long options are displayed - properties-based command line arguments, out of the box, do not support short options since the maximmum number of short options is limited to the upper- and lowercase characters and numbers, and properties files could easily run out of characters. However, it is possible to override this behaviour to be able to add short options, as well as to be able to use short options only. It is also possible to override the long option names (especially useful when property names are very long). Overriding property switch values is defined below.

All we need to do now is add the code to print the properties out, following the call for the builder to build(args) - which is illustrative in this example in order to show the output:

if (!Clc.getInstance().isParseArgs()) {
    System.exit(0);
} else {
    System.out.println("IP address : " + props.getString("host.ip"));
    System.out.println("Port       : " + props.getString("host.port"));
    System.out.println("Protocol   : " + props.getString("host.protocol"));
    System.out.println("MIME type  : " + props.getString("file.mimeTypes"));
    System.out.println("extensions : " + props.getString("file.extensions"));
    System.out.println("Strip EXIF : " + props.getString("strip.exit"));
    System.out.println("In Dir     : " + props.getString("in.dir"));
    System.out.println("Out Dir    : " + props.getString("out.dir"));
}

The check for Clc.getInstance().isParseArgs() is a way of determining if the default help was invoked. When help is invoked, there's no point continuing and thus the call to System.exit(0). Otherwise, the main application code resides in the else statement, where for any other application would contain the main code to process the options and arguments as required.

Likewise to print the property values of the Java properties version, we'd use:

if (!Clc.getInstance().isParseArgs()) {
    System.exit(0);
} else {
    System.out.println("IP address : " + props.getProperty("host.ip"));
    System.out.println("Port       : " + props.getProperty("host.port"));
    System.out.println("Protocol   : " + props.getProperty("host.protocol"));
    System.out.println("MIME type  : " + props.getProperty("file.mimeTypes"));
    System.out.println("extensions : " + props.getProperty("file.extensions"));
    System.out.println("Strip EXIF : " + props.getProperty("strip.exif"));
    System.out.println("In Dir     : " + props.getProperty("in.dir"));
    System.out.println("Out Dir    : " + props.getProperty("out.dir"));
}

Now let's run the application with the following arguments:

--host-ip 192.168.1.15 --host-port 8080 --file-extensions mp3,avi,ogg --in-dir audio/mp3

Once we run the application, we get the following output - notice how the properties that have been overridden by our command line arguments have been replaced:

IP address : 192.168.1.15
Port       : 8080
Protocol   : https
MIME type  : text/html, media/jpg
extensions : mp3,avi,ogg
Strip EXIF : false
In Dir     : audio/mp3
Out Dir    : media/processing-out

The help output is intended to be terse and only outline what properties are available to override. However, we can include a CLC configuration to override the help output, as well as modify other elements of the configuration - dealt with in a different section, below.

Under the hood, although you don't see it for both the Apache properties and Java properties, the API is generating a CLC - command line configuration, then processing this against the arguments passed in and returning the overridden properties. The CLC format is highly flexible and enables callers of the API to insert custom CLC entries in order to do things such as:

  • override default help settings, such as setting the format of the generated help output and sorting command line switches (this is disabled by default);
  • override the value types of properties and coerce them into a non-string format. For example, properties can be converted to numbers, booleans, files and many other types;
  • infer underlying command line arguments as primitive types, where appropriate for the properties implementation being used, based on the values when processing a given properties file. This has the effect of ‘forcing’ callers that override properties to also use the type that was encountered in the original properties file, and acts as a form of simple error checking; and
  • override argument value names to be more descriptive, replacing the <arg> values in the above output. For example we can ensure that rather than display --host-port <arg> in the help output, --host-port <port> is rendered, instead. We can also override the default description.

So far, all arguments have been Java Strings. The following subsections outline different ways of manipulating the API to enable callers to change the value types of arguments and how to add custom CLC entries, with minimal effort, to change how the application can work with command line arguments.

Inferring Basic Types

The API offers the ability, when parsing properties, to attempt to determine what underlying types the properties are. Typically this is restricted to simple Java primitive types such as whole numbers, decimal-place numbers and booleans; utltimately, however, it is dependant on the underlying properties implementation. Further customisation of other types is available using the CLC format directly, discussed further down; we'll just focus on using the API directly for now to infer underlying types.

Both Apache configuration properties and Java properties offer the ability to infer types according to the following rules:

  • if the property can be parsed as a whole (natural) number, then encountered properties that match this will be treated as Java ints;
  • if the property can be parsed as a number with a decimal place (real) number, then encountered properties that match this will be treated as Java floats; and
  • if the property is matches the strings true or false, then properties that match this will be treated as Java booleans.

This can be achieved in-code by adding the following to our properties builder prior to the call to build(args):

builder.withTypeInferralConfig(new TypeInferralConfigBuilder()
        .withInferTypes().build());

This will ensure all whole numbers are treated as ints, real numbers are treated as floats, and values that are true or false be treated as booleans.

It's worth noting that prior to the call to inferring types, the generated CLC for the host.port property looks as follows:

option.host-port.opts=host-port
option.host-port.hasArg=true
option.host-port.description=Overrides property 'host.port', default value '1234'

After inferring types, the generated CLC looks like so:

option.host-port.opts=host-port
option.host-port.hasArg=true
option.host-port.description=Overrides property 'host.port', default value '1234'
option.host-port.type=int

If we wanted to look at the content generated in order to parse arguments into properties, the configuration data can be viewed by changing the line where the build(args) is called with the following line:

System.out.println(builder.buildConfigurationData());

The above code will print out the generated CLC data that callers do not see when invoking the build(String[]) method.

There's also another way to print out CLC data, using the cli-gen-clc command, from the tools/cli-gen-clc/ directory. Assuming that cli-gen-clc is in the path, from the root directory of the project, run:

cli-gen-clc  --infer-types examples/01-apache-properties/src/main/resources/cliapp.props

The above command once executed will print out the generated CLC content that is being created when we invoke the API.

Type inference allows setting a default type for whole and decimal numbers, too. To override what type whole numbers are assigned we can call:

builder.withTypeInferralConfig(new TypeInferralConfigBuilder()
        .withNaturalNumbersAs(<type>).build());

… Where <type> can be one of byte, short, int, long or biginteger.

Likewise to override what type real numbers are assigned we can call:

builder.withTypeInferralConfig(new TypeInferralConfigBuilder()
        .withRealNumbersAs(<type>).build());

… Where <type> can be one of float, double or bigdecimal.

This can be seen as entirely desirable with regard to Java properties which are, by default, always treated as Java Strings. By constrast, the Apache properties library offers several different calls to get different types from read properties; for example, when accessing the host.port property, we could call:

System.out.println("Port       : " + props.getInt("host.port"));

However, there's a hidden benefit to using type inferrence: It forces callers to also have to use the same inferred type when overriding property values, acting as a very simple for of error checking.

For example the host.port has the value 1234; by using type inference, if we pass in (for example) --host-port fifty-five, then the following error is produced:

Error: option host-port: Invalid integer value: 'fifty-five'.

For boolean properties the API offers the ability, using type inference, to change false property values to unary switches; rather than setting (for example) a false value to true via a binary switch, we can either keep the value as false or by supplying the unary switch set it to true.

Consider the property strip.exif from the defined properties, above. By adding the following to the API:

TypeInferralConfigBuilder typeInferral = new TypeInferralConfigBuilder();
builder.withTypeInferralConfig(typeInferral
        .withInferTypes().withFalseAsUnarySwitch().build());

… And running --help again, the following output is produced for the strip.exif property (trimmed for brevity):

 --strip-exif                Overrides property 'strip.exif', default value
                              'false'

Notice how the help output no longer supplies the <arg> value: That's because the property is now treated as a unary switch. If the switch isn't supplied on the command line, the property value will be a boolean with with the value false; if it is supplied, the property value will be set to the boolean value true.

Type inferrence is a simple but effective method to convert underlying string-based properties to more useful data types.

The next section details how more options can be added to a custom overrides file to take advantage of the CLC API.

Adding Custom CLC Elements

In this section we continue with the properties-to-command line example above and outline how custom CLC entries can be added to an application.

As well as using type inference as described above to determine value types to arguments, custom CLC entries can be added to a CLC file that will override and add to the properties already read in. The two methods - type inferrence and adding custom CLC entries - are not mutually exclusive and can be mixed and matched as required.

The API offers the ability to load additional CLC properties that will be injected into the generated content such that callers can add to the basic default definitions. In the above example this would enable callers to add extra option.<config-name>.<option> = <value> entries.

Furthermore not only can the command line options be added to, but also the global configuration that contains properties for updating help and help formatting (among other things).

Changing Help Settings

First though, let's add some CLC entries to do the following:

  • Change the default usage section displayed when invoking help; and
  • Change the header and footer for the help output.

To do so, we need to create a new file - let's call it overrides.clc - with the following contents:

global.help.command.usage = ${manifest:app-name} [options...] <files...>
global.help.command.header = Process files in the given input directory and \
    move them to the specified output directory.
global.help.command.footer = See some random URL for information

The global.help.command.usage will replace the top-section of the help output (that is, the usage section). global.help.command.header and global.help.command.footer will replace the header and footer text, respectively.

The ${manifest:app-name} declaration will substitute in the value within the manifest file for the manifest key app-name. An error will be created if the manifest entry is missing. When the Apache properties example is run, the following output will be produced for the usage line of the help output:

usage: apache-props-demo [options]  <files...>

Likewise, the Java properties example will output the following:

usage: java-props-demo [options]  <files...>

Notice that the global.help.command.header is split onto two lines for readability. Many of the configuration options can be treated this way. When defining multiline configurations, all trailing whitespace before the closing backslash will be preserved, and succeeding lines will have leading spaces stripped; the lines will then be joined together. Escaping lines in such a way is not mandatory and only used to aid readability, as stated previously.

We'll actually place the overrides.clc files at the same level as the properties file so it will be packaged into the JAR file at build time. All we need to do is add the following line after the call to create the builder but before the call to build(String[]), and rebuild the sources:

builder.withConfiguration(ApachePropertiesDemo.class.getResourceAsStream("/overrides.clc"));

Now, let's run the --help command again and observe the output for the Apache configuration properties demo:

usage: apache-props-demo [options] <in-dir> <out-dir>
Process files in the given input directory and move them to the specified
output directory.
    --file-extensions <arg>  Overrides property 'file.extensions', default
                             value 'mp3, jpeg'
    --file-mimeTypes <arg>   Overrides property 'file.mimeTypes', default
                             value 'text/html, media/jpg'
    --help                   Print this help then exit.
    --host-ip <arg>          Overrides property 'host.ip', default value
                             'localhost'
    --host-port <arg>        Overrides property 'host.port', default value
                             '1234'
    --host-protocol <arg>    Overrides property 'host.protocol', default
                             value 'https'
    --in-dir <arg>           Overrides property 'in.dir', default value
                             'media/processing-in'
    --out-dir <arg>          Overrides property 'out.dir', default value
                             'media/processing-out'
    --strip-exif             Overrides property 'strip.exif', default
                             value 'false'
See some random URL for information

Notice how, with just three custom CLC entries, we've now converted the help output to be a little (but not much) more useful.

We can also override the property descriptions and argument names (the latter of which default to <arg>); let's do that for one of the properties - file.mimeTypes. To override any property description for the individual help output, we need to add a CLC entry, however we must use the converted name (same as the command line switch without the -- prefix). Remember, the command switch names are the same as the property names with all non-alphanumeric characters converted to hyphens. So if we wish to add an entry for the file.mimeTypes property, we need to use the name file-mimeTypes.

Add the following entries to enable custom output for the file.mimeTypes property:

option.file-mimeTypes.argName=mimeTypes...
option.file-mimeTypes.description=MIME types to consider; files that do not \
    match any of the given MIME types will be left in place. Separate multiple \
    MIME types with commas; if there are spaces in the list, surround the \
    arguments with double quotes.

… And observe the output from running --help (trimmed for brevity):

    --file-mimeTypes <mimeTypes...>  MIME types to consider; files that do
                                     not match any of the given MIME types
                                     will be left in place. Separate
                                     multiple MIME types with commas; if
                                     there are spaces in the list,
                                     surround the arguments with double
                                     quotes.

There are other help-based options (detailed in the CLC format section below), not detailed here that enable formatting the resultant output so that it has wider columns etc. when invoking --help.

Adding Version

It's the exception rather than the rule for command line applications not to provide a version. The CLC API enables versioning out of the box for properties and is very simple to do.

To add versioning, simply call the builder with withVersion(). This adds the command line switch --version to the application, the version being determined according to the following rules:

  1. If there is a version property in the properties, that value will be used; or
  2. If the manifest file contains an Implementation-Version value, that value will be used.

When help is invoked, the version will be presented as follows (trimmed from the other options for brevity):

--version                        Print version then exit.

With only the Implementation-Version present the output will be rather terse, for example

apache-props-demo --version
1.0

We can make use of manifest entries and adding in a version property to the properties file, for example:

version = ${manifest:app-name}, version ${manifest:Implementation-Version}

The output when version is invoked will now be a little more verbose:

apache-props-demo --version
apache-props-demo, version 1.1

If the properties already has a version that is used for a different purpose, the withVersion(String propertyVersionKey) call can be used. This will use the specified property converted to a command line value. For example, calling withVersion("app.version") will enable the version to be called from the command line via the --app-version switch.

Overriding Properties and Types

Next, there are a few options that can be converted to non-string types. Using type inference we've already converted host.port to an integer. So let's add some extra error checking to make sure users enter a valid port number. Under the hood the library converts all inferred types to a value type - a non-string based value. Almost all value types have properties associated with them to enable out-of-the-box error checking, rather than having to do it ourselves.

Using type inferrence we already know that the host.port property has been inferred to be an int. We can add int-based properties to override any CLI argument by adding the following to our overrides.clc file:

option.<config-name>.properties = <properties...>

To override the properties for host.port we can add the following - we can supply either a min, max, or both together, separated by a comma (order doesn't matter):

option.host-port.properties = min = 1025, max = 65535

min and max are numeric value type properties that can be applied to all types that are considered numbers. They are both optional and only one of them needs to be supplied, if chosen. This enables callers to either add a minimum, maximum or both to ensure correct values when the application is run.

Defining a value that is less than the specified minimum, we'll get the following error when specifying --host-port 100:

Error: option host-port: 100 is less than specified minimum: 1025

Now let's continue by converting the in.dir and out.dir properties to be directories. To do so, add the following entries to the overrides.clc file:

option.in-dir.type = dir
option.in-dir.properties = dirType=exists

option.out-dir.type=dir
option.out-dir.properties = dirType=exists

The property defined on each directory type, dirType=exists, means the API will check that the specified directory exists, reducing the amount of code we need to generate - for example, specifying --in-dir as an invalid directory yields the following example output for a non-existant directory named audio-output-dir/:

Error: option in-dir: Specified directory audio-output-dir does not exist.

We can also take advantage of the list value type since two of the properties contain comma-separated values. In particular, file.extensions and file.mimeTypes both contain data separated by commas. To do so, adapt overrides.clc and add the following entries:

option.file-mimeTypes.type = list
option.file-extensions.type = list

Beware that once types are converted to the underlying property implementation, the calls to get the correct values will in fact change depending on the implementation.

For example with the new types in place, the calls for Apache configuration properties don't actually need to change; calling getString(String) will still return a Java string. However, if we want to get the actual value converted by the CLC API, the println calls need to be changed to:

System.out.println("IP address   : " + props.getString("host.ip"));
System.out.println("Port         : " + props.getInt("host.port"));
System.out.println("Protocol     : " + props.getString("host.protocol"));
System.out.println("MIME types   : " + props.getList("file.mimeTypes"));
List<String> list = props.getList(String.class, "file.mimeTypes");
for (int i = 0; i < list.size(); i++) {
    System.out.println("MIME type " + i + "  : " + list.get(i));
}
System.out.println("Extensions   : " + props.getList("file.extensions"));
List<String> list2 = props.getList(String.class, "file.extensions");
for (int i = 0; i < list2.size(); i++) {
    System.out.println("Extension " + i + "  : " + list2.get(i));
}
System.out.println("Strip EXIF   : " + props.getString("strip.exif"));
System.out.println("In-dir       : " + props.get(File.class, "iin.dir"));
System.out.println("Out-dir      : " + props.get(File.class, "out.dir"));

For the Java properties implementation, this will change to:

System.out.println("IP address   : " + props.getProperty("host.ip"));
System.out.println("Port         : " + (int) props.get("host.port"));
System.out.println("Protocol     : " + props.getProperty("host.protocol"));
System.out.println("MIME Types   : " + props.get("file.mimeTypes"));
List<String> list = (List<String>) props.get("file.mimeTypes");
for (int i = 0; i < list.size(); i++) {
    System.out.println("MIME type " + i + "  : " + list.get(i));
}
System.out.println("Extensions   : " + (List<String>) props.get("file.extensions"));
List<String> list2 = (List<String>) props.get("file.extensions");
for (int i = 0; i < list2.size(); i++) {
    System.out.println("Extension " + i + "  : " + list2.get(i));
}
System.out.println("Strip EXIF   : " + (boolean) props.get("strip.exif"));
System.out.println("In-dir       : " + (File) props.get("in-dir"));
System.out.println("Out-dir      : " + (File) props.get("out-dir"));

The changes for Java-based properties is because getProperty(String) returns String, and changing the value types to non-strings means we need to use the get(String) call which returns an object of the appropriate type.

Lists aren't just limited to string values: It is possible to convert list values to any other value type. Refer to the list type documentation in the Value Types section for more information.

Overriding Command Line Names

It's possible using CLC overrides to override the names of the command line arguments that have been generated from the underlying property names. This section focuses on standard options; overriding global option names (help, version) is discussed in the next section.

This can be desirable for large property files that use large namespaces to describe properties. For example, consider the property application.gui.panels.user-management.title - when displayed using help, the following output is producced:

    --application-gui-panels-user-management-title <arg>  Overrides
                                                          property
                                                          'application.gui
                                                          .panels.user-man
                                                          agement.title',
                                                          default value
                                                          '"User
                                                          Management
                                                          Panel"'

This would be unwieldly for the user to type out as well as making the help descriptions difficult to read.

Command line names can be overridden using the opts option; all that's required is to add an entry into the overrides CLC file of the form:

option.<config-name>.opts = <long-option-name>

Continuing the previous examples, to convert the host.port property to be able to be invoked using --port, we can add the following entry to the overrides CLC file:

option.host-port.opts = port

When running with --help, the following output is now produced (trimmed for brevity):

    --port <arg>                 Overrides property 'host.port', default
                                 value '1234'

Under the hood, the API maps the new name port to the property name host.port.

By default, all properties-based implementations use the global option declaration global.options.opts-type with value LONG, meaning all options available via the command line will be long options prefixed with two hyphens.

There are several other types available:

  1. BOTH: All declared options must be come in short and long form using the declaration <short-name>/<long-name>. In such a case, each property must have an opts declaration;
  2. SHORT: All declared options must be overridden and a short option (single character) defined for it - all properties must be overridden to contain a relevant short-option definition; and
  3. ANY: Options can be a mix of short-only, long-only or short-and-long values. In this case all properties that are not overridden will continue to be long options; other properties can be modified to be a short option (single character), or a short and long option of the form <short-name>/<long-name>

Some examples:

To use global option type BOTH, we need to add the global definition, plus all options overridden with an opts definition of the form <short-name>/<long-name>. Continuing the previous example (and only including one property definition for brevity), the CLC overrides file would contain the following:

global.options.opts-type = BOTH
option.host-port.opts = p / port
# ^ Other opts omitted; you'd have to do this for every defined option because
# options type expects short AND long options to be defined for each property

Likewise, to use only short-name types, the following definition would be used:

global.options.opts-type = SHORT
option.host-port.opts = p
# ^ Other opts omitted; you'd have to do this for every defined option because
# options type expects short options to be defined for each property

Remember: When overriding BOTH and SHORT, all properties will require an opts override.

Finally, for the ANY global options type, we could mix and match the above short and short-and-long definitions, the definitions that are not overridden staying as long based options. This means that for any opts declaration, we can use just a short option, a long option or a short and long option. For example, the following shows one option using a short option, one using a long option and one using both a short and long option:

global.options.opts-type = ANY
option.host-port.opts = p
option.host-ip.opts = ip
option.host-protocol.opts = P / protocol
option.file-mimeTypes.opts = mimeTypes

When we invoke help the following output is produced:

usage: apache-props-demo [options] <in-dir> <out-dir>
Process files in the given input directory and move them to the specified
output directory.
    --extensions <arg>          Overrides property 'file.extensions',
                                default value 'mp3, jpeg'
 -h,--help                      Print this help then exit.
    --ip <arg>                  Overrides property 'host.ip', default
                                value 'localhost'
    --mimeTypes <mimeTypes...>  MIME types to consider; files that do not
                                match any of the given MIME types will be
                                left in place. Separate multiple MIME
                                types with commas; if there are spaces in
                                the list, surround the arguments with
                                double quotes.
 -P,--protocol <arg>            Overrides property 'host.protocol',
                                default value 'https'
 -p,--port <arg>                Overrides property 'host.port', default
                                value '1234'
    --strip-exif                Overrides property 'strip.exif', default
                                value 'false'
 -v,--version                   Print version then exit.
See some random URL for information

Overriding Global Command Line Names: Help & Version

Overriding command line names for help and versioning is slightly different to standard options.

To change the options for help options, override global.help.switch.opts with the required options. For example, to change the default long version from the default help to show-help, the following would be added to the overrides CLC file:

global.help.switch.opts = show-help

Versioning acts the same but version options are modified using the global.version.switch.opts global option. For example, to specify the version command line switch as get-version-info, add the following to the CLC overrides file:

global.help.switch.opts = get-version-info

Customising Help

Although help is provided out of the box, it is possible to define your own help if the provided help is not quite what is required.

To override the default help, ensure help(false) is added to the properties builder when building the properties - this will prevent the default help from being generated.

The next step is to add your application as an OptionListener and override the option method, as well as to add custom CLC entries to the CLC overrides file.

For the CLC overrides, add necessary CLC values, for example:

option.help.opts = help
option.help.description = Show some help.
option.help.ignoreCliArgs = true

ignoreCliArgs is set to ensure arguments are not parsed when the API is invoked; it's used to determine if to continue processing arguments.

The next step is to add the application as a listener for the help command.

The application must implement org.statefive.clic.OptionListener with the following implementation, adding in the custom help invocation where the comment is defined:

@Override
public void option(final String option, final Object value) {
    if ("help".equals(option)) {
        // Custom help code here
    }
}

It's also possible to pass an argument to the help command; for example a topic-based help command where a topic is passed to the help command line switch. To do so, modify the CLC definition as follows:

option.help.opts = help
option.help.description = Show some help for the given topic. Topics are 'foo' and 'bar'.
option.help.ignoreCliArgs = true
option.help.hasArg = true
option.help.argName = topic

We can now override the listener to filter on topic and display help accordingly:

@Override
public void option(final String option, final Object value) {
    if ("help".equals(option)) {
        String topic = value.toString();
        switch (topic) {
            case "foo":
                // display 'foo' based topic
                break;
            case "foo":
                // display 'bar' based topic
                break;
            default:
                System.err.println("Unknown topic: " + topic);
        }
    }
}

Adding CLC arguments

In the above example application, there were enough options such that we didn't need to consider arguments - those values that are not command line switches. For example with the GNU/Linux command ls, the arguments are the files that we wish to run the list command on. Arguments are the string values left over on the command line after all command line switches have been processed.

The CLC API enables obtaining the arguments once all options have been processed via the PropertiesListenerBindings.getInstance().getArgs() API call.

To view arguments passed to an application, add the following code underneath the println statements to print the property values:

List<String> cliArgs = PropertiesListenerBindings.getInstance().getArgs();
for (int i = 0; i < cliArgs.size() ; i++) {
    System.out.println("Argument " + (i+1) + " = '" + cliArgs.get(i) + "'");
}

We can now supply the following arguments on the command line, for example

--host-ip 192.168.1.15 --host-port 8080 arg1 arg-2 " arg 3" "#4"

… And get the following output (omitting the printed properties themselves):

Argument 1 = 'arg1'
Argument 2 = 'arg-2'
Argument 3 = ' arg 3'
Argument 4 = '#4'

Since our example application, as it stands, doesn't take any arguments (that we want to process, anyway), let's cap the arguments at zero which will cause an error if callers add any additional arguments. To do so, update the overrides.clc file with the following entry:

args.capped-at-zero.length = 0

That's all. The decision to name the entry capped-at-zero is entirely arbitrary, and could have been called x, foo, etc. To test it, invoke the program with the following additional arguments as to those provided in the previous example:

--host-ip 192.168.1.15 --host-port 8080 --file-extensions mp3,avi,ogg --in-dir /tmp foo bar

Both foo and bar will be treated as non-switch arguments; however, since we've stated that the application doesn't take arguments, the following error is produced:

Error: Configuration does not accept arguments.

So let's turn things around - we can remove the two properties in.dir and out.dir, and replace them with argument configurations both specified as type dir. The properties file without in.dir and out.dir looks like so after the properties are removed:

host.ip = localhost
host.port = 1234
host.protocol = https
strip.exif = false
file.mimeTypes = text/html, media/jpg
file.extensions = mp3, jpeg

Since in.dir and out.dir will need to be treated as arguments, we need to add the following entries to before the args.capped-at-zero.length=0 line in overrides.clc so the end of the file now looks as follows:

args.in-dir.length = 1
args.in-dir.type = dir
args.in-dir.properties = dirType = exists

args.out-dir.length = 1
args.out-dir.type = dir
args.out-dir.properties = dirType = exists

args.capped-at-zero.length = 0

The properties declaration dirType = exists means that when the arguments are processed, an error will be thrown if either directory does not exist. There is another property !exists that can be used to ensure that an argument that is a directory doesn't exist, throwing an error if it does.

This means that any time the application is run, it will expect there to be two (and only two) arguments left once all options have been processed. The will enable us to make the call

<program> <options> <in-dir> <out-dir>

For example:

--host-ip 192.168.1.15 --host-port 8080 --file-extensions mp3,avi,ogg audio/input audio/output

Notice how audio/input and audio/output are no longer defined by command line switches but are part of the arguments passed to the application. Furthermore, if either directory doesn't exist, an error will be thrown.

To access the arguments via the API, the last two lines to obtain in-dir and out-dir need to be removed from the property println statements and replaced with the following code:

File inDir = (File) Clc.getInstance().getArgsValueTypes().get(0);
System.out.println("Input Dir    : " + inDir.getAbsolutePath());
File outDir = (File) Clc.getInstance().getArgsValueTypes().get(1);
System.out.println("Output Dir   : " + outDir.getAbsolutePath());

Summary

That's it. It's been a fairly brief demonstration of how to:

  • Load a properties file and override properties for an imagined application using command line arguments, and that help is provided out of the box to show what properties are available to be overridden via the command line;
  • Versioning of the application is available either using the manifest Implementation-Version or a properties version can be used;
  • That type inferrence can be used to coerce underlying properties to integers, floating points or booleans when encountered with any of the properties;
  • That CLC properties can be overridden to enable customisation of options, including overriding the property names that are presented on the command line (including using short options); and
  • Arguments (non-switches) can be accessed and defined via the API and by providing CLC overrides.

The following sections focus on the CLC format, as well as how to add listeners to receive updates for options and arguments.

Both the Apache and Java properties final implementations are available in the examples/ directory.