Monday, November 10, 2008

Command-line Parsing with Apache Commons CLI

From time to time, I find myself needing to handle command-line arguments in Java either for Java-based applications or for main() function implementations that provide a simple testing mechanism directly within the class being tested. The Java developer has many choices for command-line parsing. When there is only one, two, or a small number of command-line arguments (especially if the presence or absence of a flag is all that is needed rather than an accompanying value), writing a few lines of code to process these command-line options is not a big deal. When there are more options and/or some options have values, it is nice to access more sophisticated support for command-line parsing.

In this blog entry, I will look at using the Apache Commons CLI library, but there are numerous other choices such as args4j, TE-Code command line parsing, CLAJR (Command-Line Arguments with Java Reflection), JArgs, JSAP (Java Simple Argument Processor), and several others (even more here).

Although Apache Commons CLI library is part of Apache Commons, it is a separate (JAR) download from the JAR download for Apache Commons Modeler and from the JAR download for Apache Commons Lang that I talked about in previous blog entries available here and here. For this blog entry, I am using CLI 1.1 because there is no anticipated release for CLI 2.0 (more details on this at the end of this entry).

I will demonstrate some very simple examples of Apache Common CLI and include some links to other resources on use of this library.

Two important classes in use of Apache Common CLI are the org.apache.commons.cli.Option class and the closely related org.apache.commons.cli.Options (contains multiple instances of the Option class). These classes are used to represent the expected command-line options. The following two code snippets demonstrate setting up of an Options class for Posix-style options and GNU-style options.

Using the Options Class with Multiple Option Instances

/**
 * Construct and provide Posix-compatible Options.
 *
 * @return Options expected from command-line of Posix form.
 */
public static Options constructPosixOptions()
{
   final Options posixOptions = new Options();
   posixOptions.addOption("display", false, "Display the state.");
   return posixOptions;
}

/**
 * Construct and provide GNU-compatible Options.
 *
 * @return Options expected from command-line of GNU form.
 */
public static Options constructGnuOptions()
{
   final Options gnuOptions = new Options();
   gnuOptions.addOption("p", "print", false, "Option for printing")
             .addOption("g", "gui", false, "HMI option")
             .addOption("n", true, "Number of copies");
   return gnuOptions;
}

Note in the examples of setting up Options that there is no difference yet in the handling of Posix-style versus GNU-style options. So far, the options can be treated the same

Before moving onto demonstrating CLI's parsing of command-line arguments based on these anticipated options, it is worth noting CLI's support for usage information and help information via the org.apache.commons.cli.HelpFormatter class. This useful utility class contains methods such as overloaded versions of printHelp, overloaded versions of printUsage, and several other output and related methods.

The following code snippet demonstrates a method that makes use of one of HelpFormatter's printUsage methods and one of that class's printHelp methods.

printUsage() and printHelp()

/**
 * Print usage information to provided OutputStream.
 *
 * @param applicationName Name of application to list in usage.
 * @param options Command-line options to be part of usage.
 * @param out OutputStream to which to write the usage information.
 */
public static void printUsage(
   final String applicationName,
   final Options options,
   final OutputStream out)
{
   final PrintWriter writer = new PrintWriter(out);
   final HelpFormatter usageFormatter = new HelpFormatter();<
   usageFormatter.printUsage(writer, 80, applicationName, options);
   writer.close();
}

/**
 * Write "help" to the provided OutputStream.
 */
public static void printHelp(
   final Options options,
   final int printedRowWidth,
   final String header,
   final String footer,
   final int spacesBeforeOption,
   final int spacesBeforeOptionDescription,
   final boolean displayUsage,
   final OutputStream out)
{
   final String commandLineSyntax = "java -cp ApacheCommonsCLI.jar";
   final PrintWriter writer = new PrintWriter(out);
   final HelpFormatter helpFormatter = new HelpFormatter();
   helpFormatter.printHelp(
      writer,
      printedRowWidth,
      commandLineSyntax,
      header,
      options,
      spacesBeforeOption,
      spacesBeforeOptionDescription,
      footer,
      displayUsage);
   writer.close();
}

The next code snippet shows some calls to the printHelp()and printUsage() methods shown above and is followed by a screen snapshot showing the output from running those.

System.out.println("-- USAGE --");
printUsage(applicationName + " (Posix)",
constructPosixOptions(), System.out);
displayBlankLines(1, System.out);
printUsage(applicationName + " (Gnu)", constructGnuOptions(), System.out);
displayBlankLines(4, System.out);
System.out.println("-- HELP --");
printHelp(
   constructPosixOptions(), 80, "POSIX HELP", "End of POSIX Help",
   3, 5, true, System.out);
displayBlankLines(1, System.out);
printHelp(
   constructGnuOptions(), 80, "GNU HELP", "End of GNU Help",
   5, 3, true, System.out);

The first screen snapshot shows the results when the code above is executed exactly as shown (with true passed to both uses of the printHelp method to indicate that options should be included in the usage portion). The second screen snapshot shows what happens when the second call to printHelp has false passed to it so that the options are not displayed.

printUsage and printHelp

printUsage and printHelp with One printHelp Not Displaying Options

While the usage and help information about the options is, as their names imply, helpful and useful, the real reason for using command-line arguments is usually to control the behavior of the application. The next code listing shows two methods for parsing GNU-style and Posix-style command-line arguments. While the setting up of the Options did not care about the specific style other than specifying the options themselves, the type of option is important now for determining the appropriate parser to use.

usePosixParser() and useGnuParser()

/**
 * Apply Apache Commons CLI PosixParser to command-line arguments.
 *
 * @param commandLineArguments Command-line arguments to be processed with
 *    Posix-style parser.
 */
public static void usePosixParser(final String[] commandLineArguments)
{
   final CommandLineParser cmdLinePosixParser = new PosixParser();
   final Options posixOptions = constructPosixOptions();
   CommandLine commandLine;
   try
   {
      commandLine = cmdLinePosixParser.parse(posixOptions, commandLineArguments);
      if ( commandLine.hasOption("display") )
      {
         System.out.println("You want a display!");
      }
   }
   catch (ParseException parseException)  // checked exception
   {
      System.err.println(
           "Encountered exception while parsing using PosixParser:\n"
         + parseException.getMessage() );
   }
}

/**
 * Apply Apache Commons CLI GnuParser to command-line arguments.
 *
 * @param commandLineArguments Command-line arguments to be processed with
 *    Gnu-style parser.
 */
public static void useGnuParser(final String[] commandLineArguments)
{
   final CommandLineParser cmdLineGnuParser = new GnuParser();
   final Options gnuOptions = constructGnuOptions();
   CommandLine commandLine;
   try
   {
      commandLine = cmdLineGnuParser.parse(gnuOptions, commandLineArguments);
      if ( commandLine.hasOption("p") )
      {
         System.out.println("You want to print (p chosen)!");
      }
      if ( commandLine.hasOption("print") )
      {
         System.out.println("You want to print (print chosen)!");
      }
      if ( commandLine.hasOption('g') )
      {
         System.out.println("You want a GUI!");
      }
      if ( commandLine.hasOption("n") )
      {
         System.out.println(
            "You selected the number " + commandLine.getOptionValue("n"));
      }
   }
   catch (ParseException parseException)  // checked exception
   {
      System.err.println(
           "Encountered exception while parsing using GnuParser:\n"
         + parseException.getMessage() );
   }
}

When the above code is executed, its output looks like that shown in the next two screen snapshots:

PosixParser Results

GNU Parser Results

The Complete Example

The complete code for the example application from which portions were shown above is now listed for convenience.

package dustin.examples.cli;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;

/**
 * Main example demonstrating Apache Commons CLI.  Apache Commons CLI and more
 * details on it are available at http://commons.apache.org/cli/.
 * 
 * @author Dustin
 */
public class MainCliExample
{
   private static Options options = new Options();

   /**
    * Apply Apache Commons CLI PosixParser to command-line arguments.
    * 
    * @param commandLineArguments Command-line arguments to be processed with
    *    Posix-style parser.
    */
   public static void usePosixParser(final String[] commandLineArguments)
   {
      final CommandLineParser cmdLinePosixParser = new PosixParser();
      final Options posixOptions = constructPosixOptions();
      CommandLine commandLine;
      try
      {
         commandLine = cmdLinePosixParser.parse(posixOptions, commandLineArguments);
         if ( commandLine.hasOption("display") )
         {
            System.out.println("You want a display!");
         }
      }
      catch (ParseException parseException)  // checked exception
      {
         System.err.println(
              "Encountered exception while parsing using PosixParser:\n"
            + parseException.getMessage() );
      }
   }

   /**
    * Apply Apache Commons CLI GnuParser to command-line arguments.
    * 
    * @param commandLineArguments Command-line arguments to be processed with
    *    Gnu-style parser.
    */
   public static void useGnuParser(final String[] commandLineArguments)
   {
      final CommandLineParser cmdLineGnuParser = new GnuParser();

      final Options gnuOptions = constructGnuOptions();
      CommandLine commandLine;
      try
      {
         commandLine = cmdLineGnuParser.parse(gnuOptions, commandLineArguments);
         if ( commandLine.hasOption("p") )
         {
            System.out.println("You want to print (p chosen)!");
         }
         if ( commandLine.hasOption("print") )
         {
            System.out.println("You want to print (print chosen)!");
         }
         if ( commandLine.hasOption('g') )
         {
            System.out.println("You want a GUI!");
         }
         if ( commandLine.hasOption("n") )
         {
            System.out.println(
               "You selected the number " + commandLine.getOptionValue("n"));
         }
      }
      catch (ParseException parseException)  // checked exception
      {
         System.err.println(
              "Encountered exception while parsing using GnuParser:\n"
            + parseException.getMessage() );
      }
   }

   /**
    * Construct and provide Posix-compatible Options.
    * 
    * @return Options expected from command-line of Posix form.
    */
   public static Options constructPosixOptions()
   {
      final Options posixOptions = new Options();
      posixOptions.addOption("display", false, "Display the state.");
      return posixOptions;
   }

   /**
    * Construct and provide GNU-compatible Options.
    * 
    * @return Options expected from command-line of GNU form.
    */
   public static Options constructGnuOptions()
   {
      final Options gnuOptions = new Options();
      gnuOptions.addOption("p", "print", false, "Option for printing")
                .addOption("g", "gui", false, "HMI option")
                .addOption("n", true, "Number of copies");
      return gnuOptions;
   }

   /**
    * Display command-line arguments without processing them in any further way.
    * 
    * @param commandLineArguments Command-line arguments to be displayed.
    */
   public static void displayProvidedCommandLineArguments(
      final String[] commandLineArguments,
      final OutputStream out)
   {
      final StringBuffer buffer = new StringBuffer();
      for ( final String argument : commandLineArguments )
      {
         buffer.append(argument).append(" ");
      }
      try
      {
         out.write((buffer.toString() + "\n").getBytes());
      }
      catch (IOException ioEx)
      {
         System.err.println(
            "WARNING: Exception encountered trying to write to OutputStream:\n"
            + ioEx.getMessage() );
         System.out.println(buffer.toString());
      }
   }

   /**
    * Display example application header.
    * 
    * @out OutputStream to which header should be written.
    */
   public static void displayHeader(final OutputStream out)
   {
      final String header =
           "[Apache Commons CLI Example from Dustin's Software Development "
         + "Cogitations and Speculations Blog]\n";
      try
      {
         out.write(header.getBytes());
      }
      catch (IOException ioEx)
      {
         System.out.println(header);
      }
   }

   /**
    * Write the provided number of blank lines to the provided OutputStream.
    * 
    * @param numberBlankLines Number of blank lines to write.
    * @param out OutputStream to which to write the blank lines.
    */
   public static void displayBlankLines(
      final int numberBlankLines,
      final OutputStream out)
   {
      try
      {
         for (int i=0; i<numberBlankLines; ++i)
         {
            out.write("\n".getBytes());
         }
      }
      catch (IOException ioEx)
      {
         for (int i=0; i<numberBlankLines; ++i)
         {
            System.out.println();
         }
      }
   }

   /**
    * Print usage information to provided OutputStream.
    * 
    * @param applicationName Name of application to list in usage.
    * @param options Command-line options to be part of usage.
    * @param out OutputStream to which to write the usage information.
    */
   public static void printUsage(
      final String applicationName,
      final Options options,
      final OutputStream out)
   {
      final PrintWriter writer = new PrintWriter(out);
      final HelpFormatter usageFormatter = new HelpFormatter();
      usageFormatter.printUsage(writer, 80, applicationName, options);
      writer.flush();
   }

   /**
    * Write "help" to the provided OutputStream.
    */
   public static void printHelp(
      final Options options,
      final int printedRowWidth,
      final String header,
      final String footer,
      final int spacesBeforeOption,
      final int spacesBeforeOptionDescription,
      final boolean displayUsage,
      final OutputStream out)
   {
      final String commandLineSyntax = "java -cp ApacheCommonsCLI.jar";
      final PrintWriter writer = new PrintWriter(out);
      final HelpFormatter helpFormatter = new HelpFormatter();
      helpFormatter.printHelp(
         writer,
         printedRowWidth,
         commandLineSyntax,
         header,
         options,
         spacesBeforeOption,
         spacesBeforeOptionDescription,
         footer,
         displayUsage);
      writer.flush();
   }

   /**
    * Main executable method used to demonstrate Apache Commons CLI.
    * 
    * @param commandLineArguments Commmand-line arguments.
    */
   public static void main(final String[] commandLineArguments)
   {
      final String applicationName = "MainCliExample";
      displayBlankLines(1, System.out);
      displayHeader(System.out);
      displayBlankLines(2, System.out);
      if (commandLineArguments.length < 1)
      {
         System.out.println("-- USAGE --");
         printUsage(applicationName + " (Posix)", constructPosixOptions(), System.out);
         displayBlankLines(1, System.out);
         printUsage(applicationName + " (Gnu)", constructGnuOptions(), System.out);

         displayBlankLines(4, System.out);

         System.out.println("-- HELP --");
         printHelp(
            constructPosixOptions(), 80, "POSIX HELP", "End of POSIX Help",
               3, 5, true, System.out);
         displayBlankLines(1, System.out);
         printHelp(
            constructGnuOptions(), 80, "GNU HELP", "End of GNU Help",
               5, 3, true, System.out);
      }
      displayProvidedCommandLineArguments(commandLineArguments, System.out);
      usePosixParser(commandLineArguments);
      //useGnuParser(commandLineArguments);
   }
}

Drawback of CLI: Version Issues

One of the most significant drawbacks of Apache Commons CLI is the CLI version paradox advertised on the CLI's main page. This main CLI page points out that "the 2.x design is generally preferred" while also pointing out that, because there is no planned 2.0 release, "the 1.1 release is recommended to most users." I used CLI 1.1 for the examples in this blog entry.

Conclusion

The Apache Commons CLI is one of many Java-based command-line argument parsing libraries that is available to make writing text-based and command-line based Java applications and tests easier. This example has shown how to use CLI to implement command-line parsing along with output of help and usage information. However, CLI has many more options and uses than shown here. Some of these are demonstrated in the easy-to-read CLI Usage Scenarios document. Other useful introductions to Apache Commons CLI include Parsing Simple Command Line Arguments in Java Using the Commons CLI Library, Process the Command Line with CLI in Java, and Using the Jakarta Commons, Part 1.

4 comments:

James Adams said...

Thanks for this very well written article, it really helped get me started using Apache commons-cli.

@DustinMarx said...

James,

I am glad this was helpful. Thanks for the feedback and for the kind words.

Dustin

Indrit Selimi said...

Today I'm trying to use Apache Commons CLI in order to refactor some command line argument handling in a standalone application. But it seems to me that this library is not satisfactory because of some difficult api nomenclature and because (it seems to me) does not exists a way to install a custom type parser. I see that there exists a TypeHandler with an unnerving comment and reading the source code it's simply terrifying(for example there exists a date parser template method that ignores the dateformat...). I decided to not use this library in production. May be we have to wait for the next release.

vizzdoom said...

Very helpful, thanks for this post!