Sometimes, you try to solve one problem and end up creating a solution for something completely different. Such is the story of Clamshell-Cli. On several occasions I came across the need of a purely console-based JMX shell. Since I did some work in JMX before, I decided to create one that was usable and basically provided some of the more useful features of JConsole. After looking around for a framework to build comand-line shells-based apps, it became clear that I would have to create one (yes, I considered OSGi and runtime implementations such as Felix, but OSGi comes with its own set of constraints that solve problems other than what I wanted to solve).
So I created my own framework to build command-line shell tools. My requirements were simple: create a flexible, component-based, highly-extensible framework that would not get in developer’s way. The API had to be light, easy to learn, and the runtime had to be super simple to use.
Enter: Clamshell-Cli – http://code.google.com/p/clamshell-cli/
Clamshell-Cli Features
- Easy to get started
- Small API footprint with low learning curve
- Ability to build complex CLI tools such as REPL using plugin architecture
- Simple component model that imposes little constraints on your design
- The plugin architecture is designed for extensibility and feature-scalability:
- If you don’t like how the default implementation works, you can change it completely
- Implement the components you want to change and your feature will be included next time the console is restarted
- Each extension point is mapped to a Java type for easy implementation
- Plugins are deployed as simple jar files
- Support for input hints (tab-press at the console)
- Support for input buffer history
The Design
The Clamshell-Cli API is kept simple on purpose. It uses a plugin paradigm based on Java’s ServiceLoader API. The idea is not to make Clamshell-Cli a bloated piece of software trying to handle everything, but rather provide an extensible platform that lets developers build console-based tools by implementing pieces of functionality via plugins.
All major aspects of a working command-line shell are represented by statically defined interfaces. For instance, if you want to change the console prompt, you simply implement the Prompt interface to return the prompt you want displayed. The Clamshell-Cli interfaces include :
- SplashScreen - interface to render the first splash screen of console-app
- ConsoleIO - interface to handle input and output streams
- Prompt - interface to provide command prompt for the shell tool
- InputController - Interface to handle text input at command prompt
- Command - interface to handle action to be taken based on command input
The Default Runtime
When you download the default runtime, you get a basic shell environment with the following directory structure:
- cli.config - Clamshell-Cli configuration file
- cli.jar - the launcher jar file
- clilib - lib files to boot Clamshell-Cli
- lib - place your dependency jars here
- plugins - location for Clamshell-Cli plugin jars
When you start the default runtime, you immediately encounter the SplashScreen plugin and the Pormpt plugin:
.d8888b. 888 .d8888b. 888 888 888 d88P Y88b 888 d88P Y88b 888 888 888 888 888 888 Y88b. 888 888 888 888 888 8888b. 88888b.d88b. :Y888b. 88888b. .d88b. 888 888 888 888 :88b 888 :888 :88b :Y88b. 888 :88b d8P Y8b 888 888 888 888 888 .d888888 888 888 888 :888 888 888 88888888 888 888 Y88b d88P 888 888 888 888 888 888 Y88b d88P 888 888 Y8b. 888 888 :Y8888P: 888 :Y888888 888 888 888 :Y8888P: 888 888 :Y8888 888 888 Command-Line Interpreter Java version: 1.6.0_22 Java Home: /usr/lib/jvm/java-6-openjdk/jre OS: Linux, Version: 2.6.38-10-generic prompt> _
The defaul runtime implements an input controller that interprets the input one line at time. It assumes that the first word of the input is the command. The controller then delegates the handling of the command to one of the registered Command instances. For instance, if you type ‘help’ at the prompt, the InputController a) locates the Command mapped to command word ‘help’, then delegate handling of the command to the HelpCmd plugin:
prompt> help Available Commands ------------------ exit Exits ClamShell. help Displays help information for available commands. sysinfo Displays current JVM runtime information. time Prints current date/time prompt> _
Using the API
To extend the default runtime and add your own command, a developer would simply provide a plugin that implements the Command interface. Package the class as jar that obeys the configuration rules of a Service Loader / Service Provider (see here), then place the jar in the plugins directory. Next time the Clamshell-Cli console is restarted, the command would be available. The following code shows how simple it is to create your own command using the Command interface. Class TimeCmd, shown below, implements the ‘time’ command as part of the default runtime::
public class TimeCmd implements Command {
private static final String NAMESPACE = "syscmd";
private static final String ACTION_NAME = "time";
@Override
public Object execute(Context ctx) {
IOConsole console = ctx.getIoConsole();
console.writeOutput(String.format("%n%s%n%n",new Date().toString()));
return null;
}
@Override
public void plug(Context plug) {
// no load-time setup needed
}
@Override
public Command.Descriptor getDescriptor(){
return new Command.Descriptor() {
@Override public String getNamespace() {return NAMESPACE;}
@Override
public String getName() {
return ACTION_NAME;
}
@Override
public String getDescription() {
return "Prints current date/time";
}
@Override
public String getUsage() {
return "Type 'time'";
}
@Override
public Map<String, String> getArguments() {
return Collections.emptyMap();
}
};
}
}
A quick explanation of the code is in order:
- Method execute() - invoked by the input controller instance when it detects the String time from the command-line. The method retrieves the IOConsole from the context object and use it to print the time. It returns null to the controller (indicating the command did not generate a result).
- Method plug() - a lifecycle method that is invoked by the framework when the command is first initialized. For our example, there nothing to do.
- Method getDescriptor() - returns an instance of interface Command.Descriptor which is used to describe the features and document the Command. For our example, the Descriptor interface is implemented anonymously with the following methods:
- Method Descriptor.getNamespace() - returns a string identifying the command’s namespace. This value can be used by input controllers to avoid command name collisions.
- Method Descriptor.getName() - returns the string mapped to this command object. In our implementation, it returns “time”.
- Method Descriptor.getUsage() - intended to provide a descriptive way of using the command.
- Method Descriptor.getArguments() - returns a Map containing the description for each arguments that may be attached to the command. This example uses none.
Clamshell-Cli Examples
The best way to learn how to use the Clamshell-Cli API is to download the source code and look at how the plugins are implemented. You can also check out:
- Jmx-Cli, a fully-functional JMX command-line tool implemented using the Clamshell-Cli API https://github.com/vladimirvivien/jmx-cli
References
http://code.google.com/p/clamshell-cli/ - Clamshell-Cli Home
http://code.google.com/p/clamshell-cli/wiki/CreatingCommandPlugin - How to create a Command plugin
http://code.google.com/p/clamshell-cli/wiki/DeployPlugins - How to deploy a Plugin
https://github.com/vladimirvivien/jmx-cli - A JMX command-line tool (yes I did build it) – built using Clamshell-Cli (discussed in future post)
http://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html - Documentation on ServiceLoader API
http://java.sun.com/developer/technicalArticles/javase/extensible/ - Article on creating extensible application using Java
