CommandLineOptions tutorial
Processing command line options is easy, so why do I need a helper?
To illustrate the reason being, let's develop a command line application to show the typical problems and how CommandLineOptions
can help you:
package com.acme.clo_demo; public class Diff { public static void main(String[] args) { boolean brief = false; boolean reportIdenticalFiles = false; int context = 3; int idx = 0; while (idx < args.length) { String arg = args[idx]; if (!arg.startsWith("-")) break; idx++; if ("-q".equals(arg) || "--brief".equals(arg)) { brief = true; } else if ("-s".equals(arg) || "--report-identical-files".equals(arg)) { reportIdenticalFiles = true; } else if ("-C".equals(arg) || "--Context".equals(arg)) { context = Integer.parseInt(args[idx++]); } else { System.err.println("Unrecognized command line option \"" + arg + "\""); } } // Now process args[idx]... and do the "real work". } }
This implementation supports both "short" and "long" options, and detects invalid options. However there is room for improvement:
- You cannot have "compact" short options, e.g. "-qs" for "-q -s".
- You get an
ArrayIndexOutOfBoundsException
when "-C" is not followed by an argument. - You get a
NumberFormatException
when the argument of "-C" cannot be converted to an integer. - If the first argument after the options starts with a hyphen, it is identified as an invalid option.
- The code is not documented.
- The only support the user gets when it uses the tool is the "Unsupported command line option..." error message.
So here's the second version that overcomes these deficits:
package com.acme.clo_demo; /** * A (rudimentary) re-implementation of the well-known DIFF command line utility. */ public class Diff { /** * Parses the command line options and arguments, then prints the differences between two files or two * directories. * <p> * Supports the following command line options: * </p> * <dl> * <dt>--help</dt> * <dd>Print usage help and exit</dd> * <dt>-q, --brief</dt> * <dd>Don't report the differences, only which files have changed</dd> * <dt>-s, --report-identical-files</dt> * <dd>Also report about files that did NOT change</dd> * <dt>-C <var>NUM</var>, --context <var>NUM</var></dt> * <dd>Report differences in "context diff" format, with <var>NUM</var> lines of context</dd> * </dl> */ public static void main(String[] args) { boolean brief = false; boolean reportIdenticalFiles = false; int context = 3; int idx = 0; while (idx < args.length) { String arg = args[idx]; if (!arg.startsWith("-")) break; idx++; if ("--".equals(arg)) { break; } else if ("--help".equals(arg)) { System.out.println("Prints the differences between files."); System.out.println(); System.out.println("Usage:"); System.out.println(" diff [ <option> ] ... <file1> <file2>"); System.out.println(" Prints the differences between <file1> and <file2>."); System.out.println(" diff [ <option> ] ... <dir1> <dir2>"); System.out.println(" Prints the differences between the files under <dir1> and <dir2>."); System.out.println(); System.out.println("Supports the following command line options:"); System.out.println(" --help</dt>"); System.out.println(" Print usage help and exit"); System.out.println(" -q, --brief"); System.out.println(" Don't report the differences, only which files have changed"); System.out.println(" -s, --report-identical-files"); System.out.println(" Also report about files that did NOT change"); System.out.println(" -C <NUM>, --context <NUM>"); System.out.println(" Report differences in \"context diff\" format, with <NUM> lines of"); System.out.println(" context"); System.exit(0); } else if ("-q".equals(arg) || "--brief".equals(arg)) { brief = true; } else if ("-s".equals(arg) || "--report-identical-files".equals(arg)) { reportIdenticalFiles = true; } else if ("-C".equals(arg) || "--Context".equals(arg)) { if (idx == args.length) { System.err.println("Count missing after \"-C\" or \"--context\"."); System.exit(1); } try { context = Integer.parseInt(args[idx++]); } catch (NumberFormatException nfe) { System.err.println("Cannot convert \"" + args[idx - 1] + "\" to an integer"); System.exit(1); } } else { System.err.println("Unrecognized command line option \"" + arg + "\"; try \"--help\"."); } } // Now process args[idx]... and do the "real work". } }
Well, that's a lot of boilerplate code! And it's just this trivial example...
Also, people may want to read about the tool without installing or running it, so you fire up the word processor, or the HTML editor, or the Wiki...
But after all, you have written and documented a professional command line tool, congratulations!
But wait, now there is number of new problems:
- The code for processing the command line options is huge, and most of it deals with trivial error conditions.
- There is a lot of redundancy in the command line options: There's the CLO implementation, and the description appears in three places: The JAVADOC of the
main()
method, theSystem.out.println()
for the "--help" option, and in the online/paper documentation.
What a maintenance nightmare!
So how can CommandLineOptions
help?
Here's how it works:
First, you replace the command line option parsing with a call to CommandLineOptions.parse(). Also add a method for each command line option, annotate it with @CommandLineOption and add its description as JAVADOC. Put the usage text into the JAVADOC comment of the "main()
" method, with a placeholder "{@command-line-options} for the descriptions of the command line options.
Second, you generate HTML documentation with the MAIN doclet:
<javadoc doclet="de.unkrig.doclet.main.MainDoclet" docletpath="de.unkrig.doclet.main.jar" destdir="src"> <fileset file="src/com/acme/clo_demo/Diff.java" /> </javadoc>
Third, you convert the HTML documentation into plain text with the HTML2TXT utility:
<taskdef classpath="html2txt.jar" resource="de/unkrig/html2txt/antlib.xml" /> <html2txt> <fileset dir="src" includes="com/acme/clo_demo/Diff.main(String[]).html" /> </html2txt>
Fourth, rewrite the "--help" option to simply copy the plain text documentation to System.out
.
So here's the code
package com.acme.clo_demo; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.regex.Pattern; import de.unkrig.commons.text.pattern.PatternUtil; import de.unkrig.commons.util.CommandLineOptions; import de.unkrig.commons.util.annotation.CommandLineOption; /** * A (rudimentary) re-implementation of the well-known DIFF command line utility. */ public class Diff { /** * Prints the differences between files. * <h2>Usage:</h2> * <dl> * <dt>diff [ <var>option</var> ] ... <var>file1</var> <var>file2</var></dt> * <dd>Prints the differences between <var>file1</var> and <var>file2</var>.</dd> * <dt>diff [ <var>option</var> ] ... <var>dir1</var> <var>dir2</var></dt> * <dd>Prints the differences between the files under <var>dir1</var> and <var>dir2</var>.</dd> * </dl> * <h2>Options:</h2> * {@command-line-options} */ public static void main(String[] args) { Diff diff = new Diff(); args = CommandLineOptions.parse(args, diff); // Now process args[idx]... and do the "real work". } private boolean brief = false; private boolean reportIdenticalFiles = false; private int context = 3; /** * Print usage help and exit. */ @CommandLineOption public void help() throws IOException { CommandLineOptions.printResource(Diff.class, "main(String[]).txt", null, System.out); System.exit(0); } /** * Don't report the differences, only which files have changed. */ @CommandLineOption(name = { "-q", "--brief" }) public void setBrief() { this.brief = true; } /** * Also report about files that did NOT change. */ @CommandLineOption(name = { "-s", "--report-identical-files"}) public void setReportIdenticalFiles() { this.reportIdenticalFiles = true; } /** * Report differences in "context diff" format, with <var>number-of-lines</var> lines of context. */ @CommandLineOption(name = { "-C", "--Context" }) public void setContext(int numberOfLines) { this.context = numberOfLines; } }
When you run it with the "--help" option, you get:
Prints the differences between files. Usage: ====== diff [ <option> ] ... <file1> <file2> Prints the differences between <file1> and <file2>. diff [ <option> ] ... <dir1> <dir2> Prints the differences between the files under <dir1> and <dir2>. Options: ======== --help Print usage help and exit. -q --brief Don't report the differences, only which files have changed. -s --report-identical-files Also report about files that did NOT change. -C <number-of-lines> --context <number-of-lines> Report differences in "context diff" format, with <number-of-lines> lines of context.
And voila - you have zero redundancy, and HTML documentation for printing and/or online presentation for free!