/* * Copyright 2001-2004 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // This file is pulled from package org.apache.avalon.excalibur.cli Excalibur // version 4.1 (Jan 30, 2002). Only the package name has been changed. package org.apache.axis.utils; import java.text.ParseException; import java.util.Hashtable; import java.util.Vector; /** * Parser for command line arguments. * * This parses command lines according to the standard (?) of * GNU utilities. * * Note: This is still used in 1.1 libraries so do not add 1.2+ dependencies. * * @author Peter Donald * @since 4.0 */ public final class CLArgsParser { private static final int STATE_NORMAL = 0; private static final int STATE_REQUIRE_2ARGS = 1; private static final int STATE_REQUIRE_ARG = 2; private static final int STATE_OPTIONAL_ARG = 3; private static final int STATE_NO_OPTIONS = 4; private static final int STATE_OPTION_MODE = 5; private static final int TOKEN_SEPARATOR = 0; private static final int TOKEN_STRING = 1; private static final char[] ARG2_SEPARATORS = new char[] { (char)0, '=', '-' }; private static final char[] ARG_SEPARATORS = new char[] { (char)0, '=' }; private static final char[] NULL_SEPARATORS = new char[] { (char)0 }; private final CLOptionDescriptor[] m_optionDescriptors; private final Vector m_options; private Hashtable m_optionIndex; private final ParserControl m_control; private String m_errorMessage; private String[] m_unparsedArgs = new String[] {}; //variables used while parsing options. private char ch; private String[] args; private boolean isLong; private int argIndex; private int stringIndex; private int stringLength; //cached character == Integer.MAX_VALUE when invalid private static final int INVALID = Integer.MAX_VALUE; private int m_lastChar = INVALID; private int m_lastOptionId; private CLOption m_option; private int m_state = STATE_NORMAL; public final String[] getUnparsedArgs() { return m_unparsedArgs; } /** * Retrieve a list of options that were parsed from command list. * * @return the list of options */ public final Vector getArguments() { //System.out.println( "Arguments: " + m_options ); return m_options; } /** * Retrieve the {@link CLOption} with specified id, or * null if no command line option is found. * * @param id the command line option id * @return the {@link CLOption} with the specified id, or * null if no CLOption is found. * @see CLOption */ public final CLOption getArgumentById( final int id ) { return (CLOption)m_optionIndex.get( new Integer( id ) ); } /** * Retrieve the {@link CLOption} with specified name, or * null if no command line option is found. * * @param name the command line option name * @return the {@link CLOption} with the specified name, or * null if no CLOption is found. * @see CLOption */ public final CLOption getArgumentByName( final String name) { return (CLOption)m_optionIndex.get( name ); } /** * Get Descriptor for option id. * * @param id the id * @return the descriptor */ private final CLOptionDescriptor getDescriptorFor( final int id ) { for( int i = 0; i < m_optionDescriptors.length; i++ ) { if( m_optionDescriptors[i].getId() == id ) { return m_optionDescriptors[i]; } } return null; } /** * Retrieve a descriptor by name. * * @param name the name * @return the descriptor */ private final CLOptionDescriptor getDescriptorFor( final String name ) { for( int i = 0; i < m_optionDescriptors.length; i++ ) { if( m_optionDescriptors[i].getName().equals( name ) ) { return m_optionDescriptors[i]; } } return null; } /** * Retrieve an error message that occured during parsing if one existed. * * @return the error string */ public final String getErrorString() { //System.out.println( "ErrorString: " + m_errorMessage ); return m_errorMessage; } /** * Require state to be placed in for option. * * @param descriptor the Option Descriptor * @return the state */ private final int getStateFor( final CLOptionDescriptor descriptor ) { int flags = descriptor.getFlags(); if( ( flags & CLOptionDescriptor.ARGUMENTS_REQUIRED_2 ) == CLOptionDescriptor.ARGUMENTS_REQUIRED_2 ) { return STATE_REQUIRE_2ARGS; } else if( ( flags & CLOptionDescriptor.ARGUMENT_REQUIRED ) == CLOptionDescriptor.ARGUMENT_REQUIRED ) { return STATE_REQUIRE_ARG; } else if( ( flags & CLOptionDescriptor.ARGUMENT_OPTIONAL ) == CLOptionDescriptor.ARGUMENT_OPTIONAL ) { return STATE_OPTIONAL_ARG; } else { return STATE_NORMAL; } } /** * Create a parser that can deal with options and parses certain args. * * @param args the args, typically that passed to the * public static void main(String[] args) method. * @param optionDescriptors the option descriptors */ public CLArgsParser( final String[] args, final CLOptionDescriptor[] optionDescriptors, final ParserControl control ) { m_optionDescriptors = optionDescriptors; m_control = control; m_options = new Vector(); this.args = args; try { parse(); checkIncompatibilities( m_options ); buildOptionIndex(); } catch( final ParseException pe ) { m_errorMessage = pe.getMessage(); } //System.out.println( "Built : " + m_options ); //System.out.println( "From : " + Arrays.asList( args ) ); } /** * Check for duplicates of an option. * It is an error to have duplicates unless appropriate flags is set in descriptor. * * @param arguments the arguments */ private final void checkIncompatibilities( final Vector arguments ) throws ParseException { final int size = arguments.size(); for( int i = 0; i < size; i++ ) { final CLOption option = (CLOption)arguments.elementAt( i ); final int id = option.getId(); final CLOptionDescriptor descriptor = getDescriptorFor( id ); //this occurs when id == 0 and user has not supplied a descriptor //for arguments if( null == descriptor ) { continue; } final int[] incompatible = descriptor.getIncompatible(); checkIncompatible( arguments, incompatible, i ); } } private final void checkIncompatible( final Vector arguments, final int[] incompatible, final int original ) throws ParseException { final int size = arguments.size(); for( int i = 0; i < size; i++ ) { if( original == i ) { continue; } final CLOption option = (CLOption)arguments.elementAt( i ); final int id = option.getId(); // final CLOptionDescriptor descriptor = getDescriptorFor( id ); for( int j = 0; j < incompatible.length; j++ ) { if( id == incompatible[ j ] ) { final CLOption originalOption = (CLOption)arguments.elementAt( original ); final int originalId = originalOption.getId(); String message = null; if( id == originalId ) { message = "Duplicate options for " + describeDualOption( originalId ) + " found."; } else { message = "Incompatible options -" + describeDualOption( id ) + " and " + describeDualOption( originalId ) + " found."; } throw new ParseException( message, 0 ); } } } } private final String describeDualOption( final int id ) { final CLOptionDescriptor descriptor = getDescriptorFor( id ); if( null == descriptor ) { return ""; } else { final StringBuffer sb = new StringBuffer(); boolean hasCharOption = false; if( Character.isLetter( (char)id ) ) { sb.append( '-' ); sb.append( (char)id ); hasCharOption = true; } final String longOption = descriptor.getName(); if( null != longOption ) { if( hasCharOption ) { sb.append( '/' ); } sb.append( "--" ); sb.append( longOption ); } return sb.toString(); } } /** * Create a parser that deals with options and parses certain args. * * @param args the args * @param optionDescriptors the option descriptors */ public CLArgsParser( final String[] args, final CLOptionDescriptor[] optionDescriptors ) { this( args, optionDescriptors, null ); } /** * Create a string array that is subset of input array. * The sub-array should start at array entry indicated by index. That array element * should only include characters from charIndex onwards. * * @param array[] the original array * @param index the cut-point in array * @param charIndex the cut-point in element of array * @return the result array */ private final String[] subArray( final String[] array, final int index, final int charIndex ) { final int remaining = array.length - index; final String[] result = new String[ remaining ]; if( remaining > 1 ) { System.arraycopy( array, index + 1, result, 1, remaining - 1 ); } result[0] = array[ index ].substring( charIndex - 1 ); return result; } /** * Actually parse arguments * * @param args[] arguments */ private final void parse() throws ParseException { if( 0 == args.length ) { return; } stringLength = args[ argIndex ].length(); //ch = peekAtChar(); while( true ) { ch = peekAtChar(); //System.out.println( "Pre State=" + m_state ); //System.out.println( "Pre Char=" + (char)ch + "/" + (int)ch ); if( argIndex >= args.length ) { break; } if( null != m_control && m_control.isFinished( m_lastOptionId ) ) { //this may need mangling due to peeks m_unparsedArgs = subArray( args, argIndex, stringIndex ); return; } //System.out.println( "State=" + m_state ); //System.out.println( "Char=" + (char)ch + "/" + (int)ch ); if( STATE_OPTION_MODE == m_state ) { //if get to an arg barrier then return to normal mode //else continue accumulating options if( 0 == ch ) { getChar(); //strip the null m_state = STATE_NORMAL; } else { parseShortOption(); } } else if( STATE_NORMAL == m_state ) { parseNormal(); } else if( STATE_NO_OPTIONS == m_state ) { //should never get to here when stringIndex != 0 addOption( new CLOption( args[ argIndex++ ] ) ); } else if( STATE_OPTIONAL_ARG == m_state && '-' == ch ) { m_state = STATE_NORMAL; addOption( m_option ); } else { parseArguments(); } } if( m_option != null ) { if( STATE_OPTIONAL_ARG == m_state ) { m_options.addElement( m_option ); } else if( STATE_REQUIRE_ARG == m_state ) { final CLOptionDescriptor descriptor = getDescriptorFor( m_option.getId() ); final String message = "Missing argument to option " + getOptionDescription( descriptor ); throw new ParseException( message, 0 ); } else if( STATE_REQUIRE_2ARGS == m_state ) { if( 1 == m_option.getArgumentCount() ) { m_option.addArgument( "" ); m_options.addElement( m_option ); } else { final CLOptionDescriptor descriptor = getDescriptorFor( m_option.getId() ); final String message = "Missing argument to option " + getOptionDescription( descriptor ); throw new ParseException( message, 0 ); } } else { throw new ParseException( "IllegalState " + m_state + ": " + m_option, 0 ); } } } private final String getOptionDescription( final CLOptionDescriptor descriptor ) { if( isLong ) { return "--" + descriptor.getName(); } else { return "-" + (char)descriptor.getId(); } } private final char peekAtChar() { if( INVALID == m_lastChar ) { m_lastChar = readChar(); } return (char)m_lastChar; } private final char getChar() { if( INVALID != m_lastChar ) { final char result = (char)m_lastChar; m_lastChar = INVALID; return result; } else { return readChar(); } } private final char readChar() { if( stringIndex >= stringLength ) { argIndex++; stringIndex = 0; if( argIndex < args.length ) { stringLength = args[ argIndex ].length(); } else { stringLength = 0; } return 0; } if( argIndex >= args.length ) return 0; return args[ argIndex ].charAt( stringIndex++ ); } private final Token nextToken( final char[] separators ) { ch = getChar(); if( isSeparator( ch, separators ) ) { ch = getChar(); return new Token( TOKEN_SEPARATOR, null ); } final StringBuffer sb = new StringBuffer(); do { sb.append( ch ); ch = getChar(); } while( !isSeparator( ch, separators ) ); return new Token( TOKEN_STRING, sb.toString() ); } private final boolean isSeparator( final char ch, final char[] separators ) { for( int i = 0; i < separators.length; i++ ) { if( ch == separators[ i ] ) { return true; } } return false; } private final void addOption( final CLOption option ) { m_options.addElement( option ); m_lastOptionId = option.getId(); m_option = null; } private final void parseOption( final CLOptionDescriptor descriptor, final String optionString ) throws ParseException { if( null == descriptor ) { throw new ParseException( "Unknown option " + optionString, 0 ); } m_state = getStateFor( descriptor ); m_option = new CLOption( descriptor.getId() ); if( STATE_NORMAL == m_state ) { addOption( m_option ); } } private final void parseShortOption() throws ParseException { ch = getChar(); final CLOptionDescriptor descriptor = getDescriptorFor( ch ); isLong = false; parseOption( descriptor, "-" + ch ); if( STATE_NORMAL == m_state ) { m_state = STATE_OPTION_MODE; } } private final void parseArguments() throws ParseException { if( STATE_REQUIRE_ARG == m_state ) { if( '=' == ch || 0 == ch ) { getChar(); } final Token token = nextToken( NULL_SEPARATORS ); m_option.addArgument( token.getValue() ); addOption( m_option ); m_state = STATE_NORMAL; } else if( STATE_OPTIONAL_ARG == m_state ) { if( '-' == ch || 0 == ch ) { getChar(); //consume stray character addOption( m_option ); m_state = STATE_NORMAL; return; } if( '=' == ch ) { getChar(); } final Token token = nextToken( NULL_SEPARATORS ); m_option.addArgument( token.getValue() ); addOption( m_option ); m_state = STATE_NORMAL; } else if( STATE_REQUIRE_2ARGS == m_state ) { if( 0 == m_option.getArgumentCount() ) { final Token token = nextToken( ARG_SEPARATORS ); if( TOKEN_SEPARATOR == token.getType() ) { final CLOptionDescriptor descriptor = getDescriptorFor( m_option.getId() ); final String message = "Unable to parse first argument for option " + getOptionDescription( descriptor ); throw new ParseException( message, 0 ); } else { m_option.addArgument( token.getValue() ); } } else //2nd argument { final StringBuffer sb = new StringBuffer(); ch = getChar(); if( '-' == ch ) { m_lastChar = ch; } while( !isSeparator( ch, ARG2_SEPARATORS ) ) { sb.append( ch ); ch = getChar(); } final String argument = sb.toString(); //System.out.println( "Arguement:" + argument ); m_option.addArgument( argument ); addOption( m_option ); m_option = null; m_state = STATE_NORMAL; } } } /** * Parse Options from Normal mode. */ private final void parseNormal() throws ParseException { if( '-' != ch ) { //Parse the arguments that are not options final String argument = nextToken( NULL_SEPARATORS ).getValue(); addOption( new CLOption( argument ) ); m_state = STATE_NORMAL; } else { getChar(); // strip the - if( 0 == peekAtChar() ) { throw new ParseException( "Malformed option -", 0 ); } else { ch = peekAtChar(); //if it is a short option then parse it else ... if( '-' != ch ) { parseShortOption(); } else { getChar(); // strip the - //-- sequence .. it can either mean a change of state //to STATE_NO_OPTIONS or else a long option if( 0 == peekAtChar() ) { getChar(); m_state = STATE_NO_OPTIONS; } else { //its a long option final String optionName = nextToken( ARG_SEPARATORS ).getValue(); final CLOptionDescriptor descriptor = getDescriptorFor( optionName ); isLong = true; parseOption( descriptor, "--" + optionName ); } } } } } /** * Build the m_optionIndex lookup map for the parsed options. */ private final void buildOptionIndex() { m_optionIndex = new Hashtable( m_options.size() * 2 ); for( int i = 0; i < m_options.size(); i++ ) { final CLOption option = (CLOption)m_options.get( i ); final CLOptionDescriptor optionDescriptor = getDescriptorFor( option.getId() ); m_optionIndex.put( new Integer( option.getId() ), option ); if( null != optionDescriptor ) { m_optionIndex.put( optionDescriptor.getName(), option ); } } } }