CommandLineOptions tutorial

From unkrig.de
Revision as of 21:50, 1 May 2024 by Admin (talk | contribs) (Created page with "===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 <code>CommandLineOptions</code> can help you: <pre> package com.acme.clo_demo; public class Diff { public static void main(String[] args) { boolean brief = false; boolean reportIdenticalFiles = false; int context = 3;...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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, the System.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!