/*---------------------------------------------------------------------------*\
  $Id: b50480b544f47b0e1db597547056e7a0dffd339b $
  ---------------------------------------------------------------------------
  This software is released under a BSD-style license:

  Copyright (c) 2004-2007 Brian M. Clapper. All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:

  1.  Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.

  2.  The end-user documentation included with the redistribution, if any,
      must include the following acknowlegement:

        "This product includes software developed by Brian M. Clapper
        (bmc@clapper.org, http://www.clapper.org/bmc/). That software is
        copyright (c) 2004-2007 Brian M. Clapper."

      Alternately, this acknowlegement may appear in the software itself,
      if wherever such third-party acknowlegements normally appear.

  3.  Neither the names "clapper.org", "clapper.org Java Utility Library",
      nor any of the names of the project contributors may be used to
      endorse or promote products derived from this software without prior
      written permission. For written permission, please contact
      bmc@clapper.org.

  4.  Products derived from this software may not be called "clapper.org
      Java Utility Library", nor may "clapper.org" appear in their names
      without prior written permission of Brian M. Clapper.

  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
  NO EVENT SHALL BRIAN M. CLAPPER BE LIABLE FOR ANY DIRECT, INDIRECT,
  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\*---------------------------------------------------------------------------*/

package org.clapper.util.cmdline;


import org.clapper.util.logging.Logger;
import org.clapper.util.misc.BundleUtil;

import java.util.Iterator;
import java.util.Locale;
import java.util.NoSuchElementException;

/**
 * 

CommandLineUtility is an abstract base class for * command-line utilities. It provides:

* * * * To use this class, subclass it, and have the subclass's main() * method instantiate the subclass, and then call the resulting object's * {@link #execute(String[]) execute()} method. The execute() method * (which resides in this base class) will:

* * * *

Note that this class does not parse options the way GNU * getopt() (or even traditional getopt()) does. In particular, * it does not permit combining multiple single-character options into one * command-line parameter. CommandLineUtility may be extended to * support that capability in the future; however, it doesn't do that yet.

* *

Here's a sample subclass. It takes the usual --logging * parameter, plus a -v (verbose) flag, a numeric count * (-n) and a file name. (Exactly what it does with those * parameters is left as an exercise for the reader.)

* *
*
 * public class Foo extends CommandLineUtility
 * {
 *     private boolean verbose  = false;
 *     private int     count    = 1;
 *     private String  filename = null;
 *
 *     public static void main (String[] args)
 *     {
 *         try
 *         {
 *             Foo foo = new Foo();
 *             foo.execute (args);
 *         }
 *
 *         catch (CommandLineUsageException ex)
 *         {
 *             // Already reported
 *
 *             System.exit (1);
 *         }
 *
 *         catch (CommandLineException ex)
 *         {
 *             System.err.println (ex.getMessage());
 *             System.exit (1);
 *         }
 *
 *         System.exit (0);
 *     }
 *
 *     private Foo()
 *     {
 *         super();
 *     }
 *
 *     protected void runCommand() throws CommandLineUtilityException
 *     {
 *         ...
 *     }
 *
 *     protected void parseCustomOption (char shortOption,
 *                                       String longOption,
 *                                       Iterator<String> it)
 *         throws CommandLineUsageException,
 *                NoSuchElementException
 *     {
 *         if (longOption.equals ("verbose") || (shortOption == 'v'))
 *             verbose = true;
 *
 *         else if (longOption.equals ("count") || (shortOption == 'n'))
 *             count = parseIntParameter ((String) it.next());
 *
 *         else
 *             throw new CommandLineUsageException ("Unknown option: " + option);
 *     }
 *
 *     protected void processPostOptionCommandLine (Iterator<String> it)
 *         throws CommandLineUsageException,
 *                NoSuchElementException
 *     {
 *         filename = it.next();
 *     }
 *
 *     protected void getCustomUsageInfo (UsageInfo info)
 *     {
 *         info.addOption ('v', "verbose", null, "Enable verbose messages")
 *         info.addOption ('n', "count", "total", "Read specified file  times. Defaults to 1.");
 *         info.addParameter ("filename", "File to process.", false);
 *     }
 * }
 * 
*
* * @see ParameterParser * * @version $Revision$ * * @author Copyright © 2004-2007 Brian M. Clapper */ public abstract class CommandLineUtility { /*----------------------------------------------------------------------*\ Private Constants \*----------------------------------------------------------------------*/ /*----------------------------------------------------------------------*\ Private Data Elements \*----------------------------------------------------------------------*/ private ParameterParser paramParser = null; /*----------------------------------------------------------------------*\ Constructor \*----------------------------------------------------------------------*/ /** * Constructor. Initializes this base class. */ protected CommandLineUtility() { // Nothing to do } /*----------------------------------------------------------------------*\ Public Methods \*----------------------------------------------------------------------*/ /** * Called to initiate execution of the command line utility. This * method * * * * @param args The command-line parameters * * @throws CommandLineException command failed */ public final void execute (String[] args) throws CommandLineException { try { paramParser = new ParameterParser(getUsageInfo()); parseParams (args); runCommand(); } catch (CommandLineUsageException ex) { usage (ex.getMessage()); throw ex; } catch (CommandLineException ex) { throw ex; } catch (Exception ex) { throw new CommandLineException (ex); } } /*----------------------------------------------------------------------*\ Protected Methods \*----------------------------------------------------------------------*/ /** * Called by parseParams() to handle any option it doesn't * recognize. If the option takes any parameters, the overridden * method must extract the parameter by advancing the supplied * Iterator (which returns String objects). This * default method simply throws an exception. * * @param shortOption the short option character, or * {@link UsageInfo#NO_SHORT_OPTION} if there isn't * one (i.e., if this is a long-only option). * @param longOption the long option string, without any leading * "-" characters, or null if this is a short-only * option * @param it the Iterator for the remainder of the * command line, for extracting parameters. * * @throws CommandLineUsageException on error * @throws NoSuchElementException overran the iterator (i.e., missing * parameter) */ protected void parseCustomOption (char shortOption, String longOption, Iterator it) throws CommandLineUsageException, NoSuchElementException { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.parseCustomOption", "(BUG) custom option found, but class {0} provides no " + "parseCustomOption method.", new Object[] {this.getClass().getName()}); } /** *

Called by parseParams() once option parsing is complete, * this method must handle any additional parameters on the command * line. It's not necessary for the method to ensure that the iterator * has the right number of strings left in it. If you attempt to pull * too many parameters from the iterator, it'll throw a * NoSuchElementException, which parseParams() traps * and converts into a suitable error message. Similarly, if there are * any parameters left in the iterator when this method returns, * parseParams() throws an exception indicating that there are * too many parameters on the command line.

* *

This method is called unconditionally, even if there are no * parameters left on the command line, so it's a useful place to do * post-option consistency checks, as well.

* * @param it the Iterator for the remainder of the * command line * * @throws CommandLineUsageException on error * @throws NoSuchElementException attempt to iterate past end of args; * parseParams() automatically * handles this exception, so it's * safe for subclass implementations of * this method not to handle it */ protected void processPostOptionCommandLine (Iterator it) throws CommandLineUsageException, NoSuchElementException { if (it.hasNext()) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.extraParams", "Extra command line parameter(s)."); } } /** * Called by parseParams() to get the custom command-line * options and parameters handled by the subclass. This list is used * solely to build a usage message. The overridden method must fill the * supplied UsageInfo object: * *
    *
  • Each parameter must be added to the object, via the * {@link UsageInfo#addParameter UsageInfo.addParameter()} method. * *
  • Each option must be added to the object, via the * {@link UsageInfo#addOption UsageInfo.addOption()} method. *
* * That information will be combined with the common options supported * by the base class, and used to build a usage message. * * @param info The UsageInfo object to fill. * * @see UsageInfo#addOption * @see UsageInfo#addParameter */ protected void getCustomUsageInfo (UsageInfo info) { // Default: does nothing } /** * Actually runs the command. All subclasses are required to provide this * method. * * @throws CommandLineException on error */ protected abstract void runCommand() throws CommandLineException; /** * Convenience method that parses an integer value, throwing a * CommandLineUsageException, not a * NumberFormatException, * on error. * * @param value the string value to parse * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseIntParameter(String,int,int) * @see #parseIntOptionArgument(char,String,String) * @see #parseDoubleParameter(String) * @see #parseFloatParameter(String) */ protected int parseIntParameter (String value) throws CommandLineUsageException { try { return Integer.parseInt (value); } catch (NumberFormatException ex) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.badIntegerParam", "Bad integer command line parameter \"{0}\"", new Object[] {value}); } } /** * Convenience method that parses an integer value, throwing a * CommandLineUsageException, not a * NumberFormatException, on error. This version of the method * also does range checking. * * @param value the string value to parse * @param min the minimum legal value for the result * @param max the maximum legal value for the result * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseIntParameter(String) * @see #parseIntOptionArgument(char,String,String,int,int) * @see #parseDoubleParameter(String,double,double) * @see #parseFloatParameter(String,float,float) */ protected int parseIntParameter (String value, int min, int max) throws CommandLineUsageException { int result = parseIntParameter (value); if (result < min) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericParamTooSmall", "Numeric parameter {0} is less than the " + "minimum legal value of {1}", new Object[] { value, String.valueOf (min) }); } if (result > max) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericParamTooLarge", "Numeric parameter {0} is greater than the " + "maximum legal value of {1}", new Object[] { value, String.valueOf (max) }); } return result; } /** * Convenience method that parses an integer option argument, throwing * a CommandLineUsageException, not a * NumberFormatException, on error. * * @param shortOption short option, as passed to {@link #parseCustomOption} * @param longOption long option, as passed to {@link #parseCustomOption} * @param value the string value to parse * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseIntOptionArgument(char,String,String,int,int) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseFloatOptionArgument(char,String,String) */ protected int parseIntOptionArgument (char shortOption, String longOption, String value) throws CommandLineUsageException { try { return Integer.parseInt (value); } catch (NumberFormatException ex) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.badIntegerOption", "Bad integer command line parameter \"{0}\" " + "for option {1}", new Object[] { value, getOptionStringForError (shortOption, longOption) }); } } /** * Convenience method that parses an integer option argument, throwing * a CommandLineUsageException, not a * NumberFormatException, on error. This version of the method * also does range checking. * * @param shortOption short option, as passed to {@link #parseCustomOption} * @param longOption long option, as passed to {@link #parseCustomOption} * @param value the string value to parse * @param min the minimum legal value for the result * @param max the maximum legal value for the result * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseIntOptionArgument(char,String,String,int,int) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseFloatOptionArgument(char,String,String) */ protected int parseIntOptionArgument (char shortOption, String longOption, String value, int min, int max) throws CommandLineUsageException { int result = parseIntOptionArgument (shortOption, longOption, value); if (result < min) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericOptionParamTooSmall", "Numeric parameter {0} for option {1} is less " + "than the minimum legal value of {2}", new Object[] { value, getOptionStringForError (shortOption, longOption), String.valueOf (min) }); } if (result > max) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericOptionParamTooLarge", "Numeric parameter {0} for option {1} is " + "greater than the maximum legal value of {2}", new Object[] { value, getOptionStringForError (shortOption, longOption), String.valueOf (max) }); } return result; } /** * Convenience method that parses a floating point value, throwing a * CommandLineUsageException, not a * NumberFormatException, on error. * * @param value the string value to parse * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseFloatParameter(String,float,float) * @see #parseFloatOptionArgument(char,String,String) * @see #parseDoubleParameter(String) * @see #parseIntParameter(String) */ protected float parseFloatParameter (String value) throws CommandLineUsageException { try { return Float.parseFloat (value); } catch (NumberFormatException ex) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.badFloatParam", "Bad floating point command line parameter " + "\"{0}\"", new Object[] {value}); } } /** * Convenience method that parses a floating point value, throwing a * CommandLineUsageException, not a * NumberFormatException, on error. This version of the method * also does range checking. * * @param value the string value to parse * @param min the minimum legal value for the result * @param max the maximum legal value for the result * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseFloatParameter(String) * @see #parseFloatOptionArgument(char,String,String,float,float) * @see #parseDoubleParameter(String,double,double) * @see #parseIntParameter(String,int,int) */ protected float parseFloatParameter (String value, float min, float max) throws CommandLineUsageException { float result = parseFloatParameter (value); if (result < min) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericParamTooSmall", "Numeric parameter {0} is less than the " + "minimum legal value of {1}", new Object[] { value, String.valueOf (min) }); } if (result > max) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericParamTooLarge", "Numeric parameter {0} is greater than the " + "maximum legal value of {1}", new Object[] { value, String.valueOf (max) }); } return result; } /** * Convenience method that parses a float point option argument, * throwing a CommandLineUsageException, not a * NumberFormatException, on error. * * @param shortOption short option, as passed to {@link #parseCustomOption} * @param longOption long option, as passed to {@link #parseCustomOption} * @param value the string value to parse * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseFloatOptionArgument(char,String,String,float,float) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseIntOptionArgument(char,String,String) */ protected float parseFloatOptionArgument (char shortOption, String longOption, String value) throws CommandLineUsageException { try { return Integer.parseInt (value); } catch (NumberFormatException ex) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.badFloatParam", "Bad floating point command line parameter " + "\"{0}\" for option {1}", new Object[] { value, getOptionStringForError (shortOption, longOption) }); } } /** * Convenience method that parses a floating point option argument, * throwing a CommandLineUsageException, not a * NumberFormatException, on error. This version of the method * also does range checking. * * @param shortOption short option, as passed to {@link #parseCustomOption} * @param longOption long option, as passed to {@link #parseCustomOption} * @param value the string value to parse * @param min the minimum legal value for the result * @param max the maximum legal value for the result * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseFloatOptionArgument(char,String,String,float,float) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseIntOptionArgument(char,String,String) */ protected float parseFloatOptionArgument (char shortOption, String longOption, String value, float min, float max) throws CommandLineUsageException { float result = parseFloatOptionArgument (shortOption, longOption, value); if (result < min) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericOptionParamTooSmall", "Numeric parameter {0} for option {1} is less " + "than the minimum legal value of {2}", new Object[] { value, getOptionStringForError (shortOption, longOption), String.valueOf (min) }); } if (result > max) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericOptionParamTooLarge", "Numeric parameter {0} for option {1} is " + "greater than the maximum legal value of {2}", new Object[] { value, getOptionStringForError (shortOption, longOption), String.valueOf (max) }); } return result; } /** * Convenience method that parses a double value, throwing a * CommandLineUsageException, not a * NumberFormatException, on error. * * @param value the string value to parse * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseDoubleParameter(String,double,double) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseFloatParameter(String) * @see #parseIntParameter(String) */ protected double parseDoubleParameter (String value) throws CommandLineUsageException { try { return Double.parseDouble (value); } catch (NumberFormatException ex) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.badDoubleParam", "Bad double floating point command line " + "parameter \"{0}\"", new Object[] {value}); } } /** * Convenience method that parses a double value, throwing a * CommandLineUsageException, not a * NumberFormatException, on error. This version of the method * also does range checking. * * @param value the string value to parse * @param min the minimum legal value for the result * @param max the maximum legal value for the result * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseDoubleParameter(String) * @see #parseDoubleOptionArgument(char,String,String,double,double) * @see #parseFloatParameter(String,float,float) * @see #parseIntParameter(String,int,int) */ protected double parseDoubleParameter (String value, double min, double max) throws CommandLineUsageException { double result = parseDoubleParameter (value); if (result < min) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericParamTooSmall", "Numeric parameter {0} is less than the " + "minimum legal value of {1}", new Object[] { value, String.valueOf (min) }); } if (result > max) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericParamTooLarge", "Numeric parameter {0} is greater than the " + "maximum legal value of {1}", new Object[] { value, String.valueOf (max) }); } return result; } /** * Convenience method that parses a double option argument, throwing a * CommandLineUsageException, not a * NumberFormatException, on error. * * @param shortOption short option, as passed to {@link #parseCustomOption} * @param longOption long option, as passed to {@link #parseCustomOption} * @param value the string value to parse * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseDoubleOptionArgument(char,String,String,double,double) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseIntOptionArgument(char,String,String) */ protected double parseDoubleOptionArgument (char shortOption, String longOption, String value) throws CommandLineUsageException { try { return Double.parseDouble (value); } catch (NumberFormatException ex) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.badDoubleOption", "Bad double command line parameter " + "\"{0}\" for option {1}", new Object[] { value, getOptionStringForError (shortOption, longOption) }); } } /** * Convenience method that parses a double floating point option * argument, throwing a CommandLineUsageException, not a * NumberFormatException, on error. This version of the method * also does range checking. * * @param shortOption short option, as passed to {@link #parseCustomOption} * @param longOption long option, as passed to {@link #parseCustomOption} * @param value the string value to parse * @param min the minimum legal value for the result * @param max the maximum legal value for the result * * @return the numeric value * * @throws CommandLineUsageException bad numeric value * * @see #parseDoubleOptionArgument(char,String,String,double,double) * @see #parseDoubleOptionArgument(char,String,String) * @see #parseIntOptionArgument(char,String,String) */ protected double parseDoubleOptionArgument (char shortOption, String longOption, String value, double min, double max) throws CommandLineUsageException { double result = parseDoubleOptionArgument (shortOption, longOption, value); if (result < min) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericOptionParamTooSmall", "Numeric parameter {0} for option {1} is less " + "than the minimum legal value of {2}", new Object[] { value, getOptionStringForError (shortOption, longOption), String.valueOf (min) }); } if (result > max) { throw new CommandLineUsageException (Package.BUNDLE_NAME, "CommandLineUtility.numericOptionParamTooSmall", "Numeric parameter {0} for option {1} is " + "greater than the maximum legal value of {2}", new Object[] { value, getOptionStringForError (shortOption, longOption), String.valueOf (max) }); } return result; } /*----------------------------------------------------------------------*\ Private Methods \*----------------------------------------------------------------------*/ /** * Parse the command-line parameters. This method parses the common * options; any other option is passed to the * parseCustomOption() method, which should throw an exception * if the option isn't recognized. When the options have all been * satisfied, this method then invokes * processPostOptionCommandLine(). * * @param args the command line parameters * * @throws CommandLineUsageException command line error * * @see #processPostOptionCommandLine * @see #parseCustomOption */ private void parseParams (String args[]) throws CommandLineUsageException { ParameterHandler handler = new ParameterHandler() { public void parseOption(char shortOption, String longOption, Iterator it) throws CommandLineUsageException, NoSuchElementException { if ((longOption != null) && (longOption.equals ("logging"))) { Logger.enableLogging(); } else { parseCustomOption (shortOption, longOption, it); } } public void parsePostOptionParameters(Iterator it) throws CommandLineUsageException, NoSuchElementException { processPostOptionCommandLine(it); } }; paramParser.parse(args, handler); } /** * Print a usage message. * * @param prefixMsg a prefix message to display before dumping the * usage, or null */ private void usage (String prefixMsg) { System.err.println(paramParser.getUsageMessage(prefixMsg, 78)); } private UsageInfo getUsageInfo() { UsageInfo info = new UsageInfo(); getCustomUsageInfo (info); info.addOption (UsageInfo.NO_SHORT_OPTION, "logging", BundleUtil.getMessage (Package.BUNDLE_NAME, Locale.getDefault(), "CommandLineUtility.logging", "Enable logging via the " + "java.util.logging API.")); return info; } private String getOptionStringForError (char shortOption, String longOption) { StringBuffer buf = new StringBuffer(); boolean paren = false; if (longOption != null) { buf.append ("--"); buf.append (longOption); paren = true; } if (shortOption != UsageInfo.NO_SHORT_OPTION) { if (paren) buf.append (" ("); buf.append ('-'); buf.append (shortOption); if (paren) buf.append (')'); } assert (buf.length() > 0); return buf.toString(); } }

Ads help maintain this website.