Using CliApplication - smacklib/dev_smack GitHub Wiki

Overview

To simplify the development of console commands Smack offers the CliApplication-base class that significantly simplifies development of console commands. It has the following functions:

  • Command line parameter parsing – One of the main functions of CliApplication is to offer a comfortable and simple strategy to offer command functionality. Command line parsing is based on a sub-command system as offered by tools like the Java keytool.

  • Command line parameter type transformations – This is the ability to convert the string-based parameters passed on the command line to the data types used in the implementation. Common types like integers get converted and validated automatically. Imagine that you never again have to code parsing a string into an integer value...

  • Console IO – The system offers unified io streams equivalent to stdin, stdout, stderr.

  • Error handling – The system offers a simple error handling model based on Java exceptions that in many cases does not require additional coding effort for error message output. Just let the exception fly and trust the system to nicely print the message.

  • Help output – CliApplication generates for every command a help page accessible by a question mark and uses this same information to validate the command lines. Imagine that your docs are never out of sync with the implementation...

The following gives more detail for each of the described functional areas. Note that it is also helpful to have a look at the interface of the implementation that defines all the details that are not discussed here.

CliApplication Basics

The basic implementation of a command based on CliApplication takes always the same form:

First create a class that has the same simple name as the cli name on the console command line. Simple name means that only the actual class name is taken into account for determining the command name, the package name is removed. The class has to extend org.smack.application.CliApplication. This makes the CliApplication APIs available for the implementation.

Then mark the operations that should be callable from the command line with the @Command-annotation.

Finally, in the main function call launch( ... ), passing a reference to your class' constructor and the command line parameters. If your class has no constructor this is fine since the Java compiler in this case generates an empty default constructor.

The following code gives an overview on all of the items above.

/*
 * CliApplication demo.
 */
package smack.examples;

import org.smack.application.CliApplication;

public final class Demo extends CliApplication {
 
    @Command public void add( int a, int b ) {
        System.out.println( a + b );
    }
    @Command public void concat( String a, String b ) {
        System.out.println( a + b );
    }

    public static void main(String[] argv) {
        launch( Demo::new, argv );
    }
}

The class extending CliApplication is required to offer a default constructor. This can be used for instance-specific initialisation tasks since CliApplication-based commands are executed as object instances, not as static implementations.

The above cli can then be executed. Below I assume that the application is packaged in an executable jar named like the cli implementation class. You can also execute the Class from your development environment.

First we execute the plain program without any parameters ...

> java -jar Demo.jar
Demo
The following commands are supported:
add: int, int
concat: String, String
>

A help page is printed, describing the supported subcommands 'add' and 'concat' and their respective parameters. Pretty cool.

Now let's try the 'add' command ...

> java -jar Demo.jar add 301 12
313
>

Adds the numbers and prints the result. Pretty much what we expected.
What happened behind the scenes? CliApplication parsed the command line, found 'add' as the command selector which matched the @Command-annotated operation 'add'. The remaining arguments from the command line were then converted to the parameter types expected by add(int a, int b) and the operation was called. Cool again.

For completeness we also try the 'concat' command ...

> java -jar Demo.jar concat wonder land
wonderland
>

Not much to say. Does what we expect and what a good cli should do!

But how about errors? Let's try to 'add' 1 and 'two' ...

> java -jar Demo.jar add 1 two
Command 'add' failed. Cannot convert 'two' to integer.
>

A great error message for zero lines error handling in our cli implementation!

So we are through with the basics. Looks good so far, ain't it? :)

CliApplication scripting

It is possible to write Java applications that use smack::java and can be executed directly from the command line. This technique is described in JEP-330.

A key element is that the CLASSPATH environment variable needs to be defined and refer to the smack jar-file. Download the smack jar from here ...

export CLASSPATH=~/svn/dev_smack/target/smack-11.406.jar

If this is done, smack can be used in Java code that is executed as a source code script from the command line. Place the following code in a file named SmackSample:

#!/usr/bin/java --source 11

import org.smack.util.*;
import org.smack.application.*;
import org.smack.application.CliApplication.Named;

@Named( description="A Java-based script implementation using Smack." )
public final class SmackSample extends CliApplication
{
    @Command( description="Add two numbers." )
    public void add( 
        int a, int b )
    {
        out( "%s%n", a + b );
    }

    @Command( description="Subtract two numbers." )
    public void subtract( int a, int b )
    {
        out( "%s%n", a - b );
    }

    public static void main(String[] argv) 
    {
        launch( SmackSample::new, argv );
    }
}

Add the execute permission to the file:

chmod +x SmackSample

After this simple setup, it is possible to execute the above on the command line:

beeme@wonderland:~/lab$ ./SmackSample
SmackSample -- A Java-based script implementation using Smack.
The following commands are supported:
add: int, int
    Add two numbers.
subtract: int, int
    Subtract two numbers.
beeme@wonderland:~/lab$ ./SmackSample subtract 0 313
-313
beeme@wonderland:~/lab$ ./SmackSample add 13 300
313
beeme@wonderland:~/lab$

Reference

The application class

Using CliApplication as a base class.

  • Inherit from CliApplication and mark commands with the @Command annotation.

Using no inheritance

  • Implement a class that does not inherit from CliApplication, mark commands with the @Command annotation.

Command Line Parameter Parsing

One implementation goal of CliApplication is to offer a way to map a given command line to a corresponding operation call in the implementation of the command.

Subcommand system

This mapping strategy is used to partition a command into subcommands which is usually done for commands that want to offer a set of related operations. In this case a command can offer a keyword facility on the command line that allows to specify the requested subfunction.

For example the cli command 'device' offers the sub-commands 'flash', 'ping', 'listcfg'. Each command has the ability to add further subfunction-specific parameters. The following are valid command lines:

  • device ping NFC
  • device listcfg RELAY
  • device flash NFC /app/atop/nfc_18_24.fhex

The outline of the sourcecode for this command is shown below:

public class Device extends CliApplication {
 
    ...

    @Command
    public void ping( String component )
    {
        ...
    }
 
    @Command
    public void listCfg( String component )
    {
        ...
    }

    @Command
    public void listCfg()
    {
        ...
    }

    @Command
    public void flash( String component, File file )
    {
        ...
    }

    public static void main(String[] argv)
    {
        launch( Device::new, argv );
    }
}

Each subfunction is implemented by a public operation that has the name of the command and is marked by the @Command annotation. The first argument of the command line and the number of remaining arguments is used to select the operation that is called. That is, it is possible to have more than a single subfunction with a single name as long as the length of the parameter list differs. Note that only the length of the parameter list is taken into account for the operation dispatch, the types are not used. In the previous example 'flash' is an overlayed command where the selection of the command invoked is based on the passed parameters.

Argument mapping

As soon as the target operation is identified, an argument conversion is performed. In this step the implementation tries to convert the parameters given on the command line to the parameter types expected by the target operation. This conversion mechanism offers an automatic conversion for the following types:

  • Java primitive types: All primitives are converted. The integer-primitives accept decimal values like '313' and hex values like '0xb'. Boolean values support 'true' and 'false' as valid values.
  • String types: The trivial conversion. Note that strings containing spaces can be passed using double quotes like "Donald Duck".
  • File type: Use java.io.File in the type declaration. The file is checked to exist, if it does not exist then an error is signaled.

If type conversion fails, then an error message is printed and the cli terminates. Otherwise the selected operation is called with the converted argument values.

The conversion mechanism can be extended by the application. See CliApplication#addConverter(...).

Default command system

Sometimes the subcommand mapping strategy is not required. In this case a second command mapping strategy is supported, mapping the passed command line directly to operations named 'defaultCmd( ... )'. The argument mapping is the same as above. See the examples CliTest_1 and CliTest_2 below.

Console IO

The base class offers the operations out(...) for accessing the standard out stream used to write console messages. The operations err(...) return the error stream used for printing error messages.

Error Handling

All commands are allowed to throw exceptions. If an exception is thrown, it is catched by the CliApplication runtime and its message text is presented nicely to the user. This allows an implementation to simply throw an Exception in error cases and rely on CliApplication to present the error message and terminate the application. For the lazy this can be used by just simply not catching exceptions, since it is guaranteed that there's a handler in place transforming the exception to an error message in the console.

Command implementations should ensure that the message of the thrown exception is set to a user presentable value. If the message is not set then the name of the exception is printed in the error message.

Normal exceptions are written to cliErr and logged FINE. Less formally, this translates to the exception message is written to the user console and normally not logged.

Runtime exceptions are written to cliErr including a stack trace and logged SEVERE. Note that this should be understood as debugging support for a command implementer. Runtime exceptions should not be part of the finished command.

/**
 * CliApplication demo.
 */
public final class Cat extends CliApplication
{ 
    public void defaultCmd( File file ) 
		throws Exception
	{
		if ( file.isDirectory() )
            throw new Exception( "Passed argument is a directory: " + file.getName() );
    	...
	}
 
    ...
}

Help Output

Examples

package com.smack.demo;

import java.io.File;
import com...CliApplication;
import com...ConsoleCommand;
/**
 * Test default command handling.
 */
public class CliTest_1 extends CliApplication{
    /**
     *
     * @param file
     */
    public void defaultCmd( File file )
    {
        file.delete();
    }

    /**
     * Takes a single file name argument and tries to delete it.
     *
     * @param argv
     *            Must contain only the fully qualified name of the file to be deleted.
     */
    public static void main(String[] argv)
    {
        execute( CliTest_1.class, argv );
    }
}

package com.smack.demo;
import com...CliApplication;
import com...ConsoleCommand;
/**
 * Test default command handling.
 */
public class CliTest_2 extends CliApplication {
    /**
     * Throws an exception.  Expected is transfer of exception message
     * to a user visible message in the console error stream.
     */
    public void defaultCmd( String a ) throws Exception
    {
        cliErr().println( "defaultCmd: " + a );
        throw new Exception( "error" );
    }
    /**
     * Throws a RuntimeException.  Expected is a user-visible message and
     * a sever log entry.
     */
    public void defaultCmd( String a, int b ) throws Exception
    {
        cliErr().println( "defaultCmd: " + a + "," + b );
        throw new RuntimeException( "RuntimeException" );
    }
    /**
     * Takes a single file name argument and tries to delete it.
     *
     * @param argv
     *            Must contain only the fully qualified name of the file to be deleted.
     */
    public static void main(String[] argv)
    {
        execute( CliTest_2.class, argv );
    }
}