Introduction: The Use Case
We all often need to experiment with code: maybe to learn a new algorithm, or to play with a library to discover its peculiarities. Or, we develop a tool just for ourselves and don't want to spend any time designing complex UI for the software, just make it work as quickly as possible.
The tool described here serves the very purpose: make Java code runnable in no time by providing extremely simple yet powerful UI auto-generation.
I came up with the idea to create this tool when I was working on an administration console for database management of a web app. I had the database class which already had methods for all I need, but there were quite a lot of them, so creating a 'normal' UI would be a difficult task. I decided to create an interactive command-line UI, kind of a shell. To avoid writing a lot of similar code for command-line processing, I went with Reflection-based mapping of methods to commands. Thus, the Cliché Shell was born.
"Hello, World!"
Let's write a method to add two numbers. Since it's Java, you'll need a class and other stuff.
public class HelloWorld {
public int add(int a, int b) {
return a + b;
}
}
Now we want to make the code runnable.
The simplest traditional way is to add a main()
method, then convert params[]
to int
. Not so difficult, but this approach doesn't scale: if there are several methods with varying arguments, you'll have to write a huge switching construct. I hope you understand all the evil.
The Cliché way:
import asg.cliche.ShellFactory;
import asg.cliche.Command;
import java.io.IOException;
public class HelloWorld {
@Command
public int add(int a, int b) {
return a + b;
}
public static void main(String[] params) throws IOException {
ShellFactory.createConsoleShell(“helloâ€, null, new HelloWorld())
.commandLoop();
}
}
Run the code and type add 4 6 at the prompt, 'hello>'. Type exit to quit the shell.
It's simple, and it's scalable: for each new method, you'll need to add just a @Command
annotation.
The key limitation to this simplicity is type conversion: if your method requires a type other than String
or a primitive (and their Object analogs, like Integer
), then you'll need a wrapper method or a Converter. Converters are Cliché Shell extensions, and are discussed later.
The Shell Language
Here are the language basics.
Naming convention is different from that of Java. Namely, commandName
becomes command-name
by automatic name translation.
Abbreviations of command names consist of first letters of words in the command name, e.g., the default abbreviation for command-name
is cn
.
If a command method name starts with cmd
or cli
, the prefix doesn't get included in the command name. E.g., cmdSomeMethod
becomes some-method
.
Quotation is also different: single and double quotes are equivalent, and there's no escape sequence except for quotes:
"a string"
is the same as 'a string'
and translates to a string
'a "string" 2'
is the same as "a ""string"" 2'
and translates to "string" 2
- You can write
a" "string
, that's the same as a string
.
Here are the most important built-in commands:
exit
is the command to quit the shell.?list
(or ?l
) lists all the user-defined commands.?list-all
(or ?la
) lists all the available commands, including built-in ones.?help command-name
outputs detailed information on the command (if any).
See ?la
for the complete list.
Shell Functionality
Documenting Commands
While default command names are based on method names and are rather descriptive, Java doesn't keep information on method parameter names (if I'm wrong, please let me know). Cliché Shell has support for parameter naming through the @Param
annotation. You can also rename the command or add detailed information using the @Command
annotation:
@Command(description="Varargs example")
public Integer add(
@Param(name="numbers", description="some numbers to add")
Integer... numbers) {
int result = 0;
for (int i : numbers) {
result += i;
}
return result;
}
Here, you also see that Cliché supports varargs
methods. (Note the use of the Integer
class: Cliché supports primitive types such as int
, except when you want to override type conversion).
Input Conversion
You can either define converters for new classes, or override the built-in conversion, like in this code:
public static final InputConverter[] CLI_INPUT_CONVERTERS = {
new InputConverter() {
public Integer convertInput(String original, Class toClass) throws Exception {
if (toClass.equals(Integer.class)) {
if (original.equals("one")) return 1;
if (original.equals("two")) return 2;
if (original.equals("three")) return 3;
}
return null;
}
}
};
Now, you could write add one two
and get 3
.
CLI_INPUT_CONVERTERS
is a special field which is examined by the Shell in search of input converters.
Three points to consider:
- Converters in
CLI_INPUT_CONVERTERS
take precedence over built-in conversion rules. - If you don't know what to do with a given type, return
null
: the Shell will try to find a more appropriate converter. - Since it works with objects, you can't convert to primitive types, and thus can't override conversion for primitive types.
Output Conversion
You can also override the way the Shell converts command method results to strings (the default is to call toString()
).
public static final OutputConverter[] CLI_OUTPUT_CONVERTERS = {
new OutputConverter() {
public Object convertOutput(Object o) {
if (o.getClass().equals(Integer.class)) {
int num = (Integer) o;
if (num == 1) return "one";
if (num == 2) return "two";
if (num == 3) return "three";
}
return null;
}
}
};
Now, add one two
returns three
. Of course, there are better uses. I once wrote a converter for double[]
that displayed a Swing frame with a graph.
And, since there's no need to make the CLI_OUTPUT_CONVERTERS
field static final
, you can control the conversion with other commands:
private boolean displayResults = false;
@Command(description="Turns on table function display")
public void enableResults() {
displayResults = true;
}
public OutputConverter[] CLI_OUTPUT_CONVERTERS = {
new OutputConverter() {
public Object convertOutput(Object toBeFormatted) {
if (toBeFormatted instanceof U[]) {
return displayResults ? toBeFormatted : String.format(
"[%d rows skipped; see '?h er']", ((U[])toBeFormatted).length);
} else {
return null;
}
}
}
};
There are some other features that I didn't describe here. See the example code, the Shell is very simple and useful!