CLC Listeners

Once a configuration has been defined, the next step is to define listeners to be informed by the API of when commands, switches and arguments are passed to the application by the command line.

Commands, if present, are parsed first, followed by switches, then finally (if present) followed by arguments. If commands are present they are consumed and then remaining options and arguments passed to that command for processing; otherwise, options and arguments are treated as top-level options and arguments.

There are two types of listeners - option listeners and arguments listeners, each with a command-based version for a total of four listeners:

Each of these are discussed in the following sections.

Option Listeners

To listen for updates to top-level switches, implementations must implement the org.statefive.clic.OptionListener interface with the following method:

void option(String option, Object value)

The option will be the switch passed in from the command line without any leading hyphens; for example, if the switch -f config.txt is passed in via the command line then the option will be f and the value will be config.txt as a string, unless a value type was associated with the configuration for that option. For example if the file value type was set for the given option configuration, then the value will be a java.io.File.

Let's look at the implementation of this, assuming that there's a short option -f, long option --file and the value type is a string:

private String filename;

// getters omitted

public void option(String option, Object value) {
	switch(option) {
		case "f":
		case "file":
			filename = value.toString();
			// or filename = (String) value;
		break;
		// other options omitted
	}
}

If we're using the file/java.io.File value type, the code would be:

private File filename;

// getters omitted

public void option(String option, Object value) {
	switch(option) {
		case "f":
		case "file":
			filename = (File) value;
		break;
		// other options omitted
	}
}

In both cases we can access the values of the listener by calling the get methods for the different members of the listener.

For unary switches, it's a little bit different: The value for all unary switches will always be null. It is advised to use boolean values for such switches. For example, let's assume there's also a unary switch short and long option, -L and --no-limit, respectively; then the code for our application (including the file option) would look as follows:

private File filename;
private boolean noLimit;

// getters omitted

public void option(String option, Object value) {
	switch(option) {
		case "f":
		case "file":
			filename = (File) value;
		break;
		case "L":
		case "no-limit":
			noLimit = true;
		break;
		// other options omitted
	}
}

Finally, if global.help or global.version were defined, we simply need to add System.exit(0) calls to the switch/case statement for the corresponding match; by the time this reaches the listener, help or version will have been output already. This example assumes both help and version are using the default switch values provided by the API:

public void option(String option, Object value) {
	switch(option) {
		case "h":
		case "help":
			System.exit(0);
		break;
		case "v":
		case "version":
			System.exit(0);
		break;
		// other options omitted
	}
}

Argument Listeners

As such, applications do not necessarily need to define argument listeners for top-level arguments or command arguments - API callers can access arguments directly through the API by calling Clc.getInstance().getArgs() which returns List<String>. There's also the Clc.getInstance().getArgsValueTypes() that returns List<Object>. The listeners are provided for extra flexibility in case callers wish to be informed of options as they are parsed in the order they are defined. For example callers may want to provide validation of arguments as they are processed.

To receive top-level arguments, callers must implement the org.statefive.clic.ArgsListener interface and implement the following method:

void argument(String name, int index, Object value);

If no argument configurations are defined then the name will be the same as value which will be the string value of the argument. The index will be the position of the argument, indexing starting at 0.

If argument configurations are defined, then the name will be the argument configuration name - that is, the args.<config-name> value. The value of the argument will be a string if no value type was defined, otherwise it will be the type defined in the configuration.

Let's look at code for updating arguments when no argument configurations are defined. In such a case this means that the argument list can either be empty or any number of arguments.

public void argument(String name, int index, Object value) {
	switch(index) {
		case 0:
			// perform checks on first argument
		break;	
	}
	if (index >= 5) {
		// checks for arguments greater than or equals to index 5, i.e. arguments 6 and onward
	}
}

For defined argument configuration, consider the following CLC declaration for an application that takes an operator and a list of doubles to perform the operation on:

args.operator.type = char
args.operator.properties = includes = +-/*^, regex = false
args.operator.length = 1

args.double-vals.type = double

The above configuration defines that the first argument will be a character for addition, subtraction, division, multiplication or power; the second configuration defines any number of non-optional (i.e. must be at least one) decimal numbers.

Now, the implementation:


private Double doubleVal = null;
private Character operator = null;

public void argument(String name, int index, Object value) {
    switch (name) {
        case "operator":
            operator = (Character) value;
        break;
        case "float-vals":
            if (doubleVal == null) {
                doubleVal = (Double) value;
            } else {
                switch (operator) {
                    case '+':
                        doubleVal = doubleVal + (Double) value;
                    break;
                    case '-':
                        doubleVal = doubleVal - (Double) value;
                    break;
                    case '/':
                        doubleVal = doubleVal / (Double) value;
                    break;
                    case '*':
                        doubleVal = doubleVal * (Double) value;
                    break;
                    case '^':
                        doubleVal = Math.pow(doubleVal, (Double) value);
                    break;
                }
            }
            break;
    }
}