CLC Application Examples

In the previous examples properties files were used to drive command line arguments, the properties being converted to options that could then be overridden. Furthermore a custom CLC file could be used to modify and supplement properties to be able to temper command line arguments to coerce them to be specific value types, add properties to value types for error checking and other overrides to be able to customise properties as required.

Under the hood a CLC configuration was generated - hidden from the user - to ultimately pass those values down to the Apache Commons cli API for processing. However, the API enables developers to develop CLC configurations and to write listeners that enables moving away from specific property implementations. This section devotes itself to doing exactly that, by taking existing properties and an overrides file to generate a CLC and adding in Java class listeners to be able to receive updates from the API as arguments are parsed against the underlying CLC definition.

In this example, we're going to use two tools - cli-gen-clc and cli-gen-src - to take those original projects and write a new application that doesn't use properties, but instead uses just the generated CLC configuration and Java listeners to do the work for us.

Both tools are not required to be able to create such a configuration - it's entirely possible, when one understands the CLC format and how to write listeners - to be able to craft such configurations by hand. Using tools is a quick ways to achieve this goal while at the same time learning how the different parts of the API hang together.

The two tools we'll use to achieve this goal are:

In particular, take a look at the cli-gen-clc.clc and cli-gen-src.clc configurations for each project, maintained in the src/main/resources/ directories for each project: These define what options (and, in the case of cli-gen-clc, what arguments) are available. The OptionHelper classes to each project determine how values are stored when values are overridden on the command line. And, finally, the buildCliOptions(String[]) method of each class is used to amalgamate bindings to the API prior to processing the command line options and arguments before running each application.

That being said, let's continue with taking the previous properties and CLC overrides from the properties-based examples and use both cli-gen-clc and cli-gen-src to rapidly develop a CLC-based application.

The following examples assume that both cli-gen-clc and cli-gen-src are on the PATH and that the tools are being run from the root of the project.

Generating a CLC Configuration using cli-gen-clc

cli-gen-clc is a command line tool to take any number of properties and generate a CLC file. By default the tool assumes properties are Apache Commons configuration properties; Java properties are also supported, as well as different implementations. Other implementations will need to supply a valid properties builder implementation along with any value type implementations, if different value types are required.

We can take the previously defined properties file, overrides CLC file and add in the required options using the --infer-types and --false-as-unary switches as follows to generate a full CLC configuration file:

cli-gen-clc --config examples/01-apache-properties/src/main/resources/overrides.clc \
    --infer-types \
    --false-as-unary \
    --insert-defaults \
    --output examples/04-clc-example/src/main/resources/app.clc
    examples/01-apache-properties/src/main/resources/cliapp.props

This assumes the properties file is an Apache Configuration-based properties file; to use a Java properties file, add the following argument: --java-properties. Using a different implementation is also possible and the com.typesafe implementation is discussed below.

Inference of types and false-as-unary will be recognisable as options passed to the properties API. The new option, --insert-defaults, is worth mentioning: The intent is that with a full-blown CLC configuration, we'll not be using properties files any longer; inserting defaults enables us to preserve the default values of the properties and bringing them across to the CLC implementation.

The final argument:

examples/01-apache-properties/src/main/resources/cliapp.props

… Is the properties file we'll convert to CLC format; any number of these files can be passed in, as well as using the - character to read from standard input.

This generates the following CLC data in our new project to a file named examples/04-clc-example/src/main/resources/app.clc:

# Global options
global.options.opts-type = ANY
global.help.option.name = help
global.help.command.usage = ${manifest:app-name} [options] <in-dir> <out-dir>
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
global.help.switch.opts = help
global.help.format.auto-usage = false
global.help.format.column-spacing = 5
global.help.format.left-pad = 1
global.help.format.width = 74
global.help.format.width-from-env = false
global.help.format.sort-options = false
global.version.name = version
global.version.text = ${manifest:app-name}, version ${manifest:Implementation-Version}

# Options configuration:
option.help.opts = h / help
option.help.description = Print this help then exit.
option.help.ignoreCliArgs = true

option.host-ip.opts = ip
option.host-ip.description = Overrides property 'host.ip', default value 'localhost'
option.host-ip.hasArg = true

option.file-mimeTypes.opts = 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.
option.file-mimeTypes.hasArg = true
option.file-mimeTypes.argName = mimeTypes...
option.file-mimeTypes.type = list

option.host-protocol.opts = P / protocol
option.host-protocol.description = Overrides property 'host.protocol', default value 'https'
option.host-protocol.hasArg = true

option.file-extensions.opts = extensions
option.file-extensions.description = Overrides property 'file.extensions', default value 'mp3, jpeg'
option.file-extensions.hasArg = true
option.file-extensions.type = list

option.host-port.opts = p / port
option.host-port.description = Overrides property 'host.port', default value '1234'
option.host-port.hasArg = true
option.host-port.type = int
option.host-port.properties = min = 1025, max = 65535

option.strip-exif.opts = strip-exif
option.strip-exif.description = Overrides property 'strip.exif', default value 'false'
option.strip-exif.hasArg = false

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

Generating a CLC using different implementation: Typesafe HOCON properties

To read properties from a different implementation such as com.typesafe, the builder implementation and (if present) value types will need to be included in the CLASSPATH_PREFIX and the builder name passed to the application using the --builder option. Project-specific value types will need to be passed via the --value-types option. Assuming the application is being run from the root of the project, then this can be achieved using the following command (replace export with set if using Windows):

export CLASSPATH_PREFIX=implementations/com.typesafe/target/com-typesafe-clic-properties-2-jar-with-dependencies.jar
cli-gen-clc --config examples/03-com-typesafe-properties/src/main/resources/overrides.clc \
    --builder com.typesafe.clic.properties.TypesafeConfigBuilder \
    --value-types list:com.typesafe.clic.properties.ConfigListType \
    --infer-types \
    --false-as-unary \
    --output examples/04-clc-example/src/main/resources/app.clc \
    examples/03-com-typesafe-properties/src/main/resources/cliapp.conf

Note both the --builder and --value-types options. The former ensures the correct implementation builder is loaded. The latter ensures that when a list value type is used, the com.typesafe.clic.properties.ConfigListType implementation will be used and override the default org.statefive.clic.valuetype.ListType implementation.

Specifying the correct list type will ensure that any list-based properties are rendered correctly when loading list types; for example, with the above declaration, for the file-extensions option, the description is output as:

option.file-extensions.description = Overrides property 'file.extensions', default value 'mp3, jpeg'

Without specifying the correct value type, the description is output without the list being rendered correctly:

option.file-extensions.description = Overrides property 'file.extensions', default value 'Quoted("mp3"), Quoted("jpeg")'

Generating Java listeners using cli-gen-src

Once a CLIC configuration file has been defined, the next step is to write a Java class to receive options from the CLIC API. It's a relatively simple process and involves writing a helper class that implements the correct listeners and stores variables based on the configuration with getters, and including a case statement to switch on values and assign them as they are received via the API.

It's permissible to generate the listeners for an application by hand but luckily there's a tool, cli-gen-src, to do the job for us. This takes as input via the -c/--clc switch to pass in a valid CLC file. All we need to do then is add some extra parameters. By default the generated source is output to standard output; however, by supplying --output-dir, the code will get generated in a file named OptionHelper.java (configurable by passing in the --class-name switch). We add the --package-name so that the class will compile when we build it:

cli-gen-src --clc examples/04-clc-example/src/main/resources/app.clc \
    --output-dir examples/04-clc-example/src/main/java/org/statefive/clc/examples/example4/ \
    --package-name "org.statefive.clic.examples.example4"

This generates the following code:

package org.statefive.clic.examples.example4;

import java.io.File;
import java.lang.Integer;
import java.lang.String;
import java.util.List;
import org.statefive.clic.ArgsListener;
import org.statefive.clic.OptionListener;

public class OptionHelper implements OptionListener, ArgsListener {

    private File args0InDir;
    private File args1OutDir;
    private String ip;
    private List<String> mimetypes;
    private String protocol;
    private List<String> extensions;
    private Integer port;
    private boolean stripExif;

    @Override
    public void option(String option, Object value) {
        switch (option) {
            case "h":
            case "help":
                System.exit(0);
            case "ip":
                ip = (String) value;
                break;
            case "mimeTypes":
                mimetypes = (List<String>) value;
                break;
            case "P":
            case "protocol":
                protocol = (String) value;
                break;
            case "extensions":
                extensions = (List<String>) value;
                break;
            case "p":
            case "port":
                port = (int) value;
                break;
            case "strip-exif":
                stripExif = true;
                break;
            case "v":
            case "version":
                System.exit(0);
        }

    }

    @Override
    public void argument(String name, int index, Object value) {
        switch (name) {
            case "in-dir":
                switch (index) {
                    case 0:
                        args0InDir = (File) value;
                        break;
                }
                break;
            case "out-dir":
                switch (index) {
                    case 1:
                        args1OutDir = (File) value;
                        break;
                }
                break;
        }

    }

    public File getArgs0InDir() {
        return args0InDir;
    }

    public File getArgs1OutDir() {
        return args1OutDir;
    }

    public List<String> getExtensions() {
        return extensions;
    }

    public String getIp() {
        return ip;
    }

    public List<String> getMimetypes() {
        return mimetypes;
    }

    public Integer getPort() {
        return port;
    }

    public String getProtocol() {
        return protocol;
    }

    public boolean isStripExif() {
        return stripExif;
    }

}

Explanation of the above code. First, prepare the input stream for the CLC:

InputStream is = ClcDemo.class.getResourceAsStream("/app.clc");

Next, construct our option helper and add it as an option and arguments listener to the CLC API:

Clc clc = Clc.getInstance();
OptionHelper optionHelper = new OptionHelper();
clc.addOptionListener(optionHelper);
clc.addArgsListener(optionHelper);

Finally, pass the application arguments with the CLC stream to the API:

clc.process(is, "UTF-8", args);

At this point, we'd normally start running an application, using the options and arguments to work in a meaningful way; however, in the spirit of brevity, we simply print out the values:

System.out.println("IP address   : " + optionHelper.getIp());
System.out.println("Port         : " + optionHelper.getPort());
System.out.println("Protocol     : " + optionHelper.getProtocol());
System.out.println("MIME types   : " + optionHelper.getMimetypes());
System.out.println("Extensions   : " + optionHelper.getExtensions());
System.out.println("Strip EXIF   : " + optionHelper.isStripExif());

File inDir = (File) optionHelper.getArgs0InDir();
System.out.println("Input Dir    : " + inDir.getAbsolutePath());
File outDir = (File) optionHelper.getArgs1OutDir();
System.out.println("Output Dir   : " + outDir.getAbsolutePath());

Note that unlike with the properties API, we don't need to interrogate to determine if arguments should be parsed or not; this is dealt with by the API and the listener that simple calls exit if (for example) help is invoked.