blob: 36e891926a15b87a1d67a914e38b9d6b5702cbf8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*
*/
package org.apache.commons.cli.avalon;
//Renamed from org.apache.avalon.excalibur.cli
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.
*
* Note that CLArgs uses a backing hashtable for the options index and so duplicate
* arguments are only returned by getArguments().
*
* @see ParserControl
* @see CLOption
* @see CLOptionDescriptor
*/
public final class CLArgsParser
{
//cached character == Integer.MAX_VALUE when invalid
private static final int INVALID = Integer.MAX_VALUE;
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;
// Values for creating tokens
private static final int TOKEN_SEPARATOR = 0;
private static final int TOKEN_STRING = 1;
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 m_ch;
private String[] m_args;
private boolean m_isLong;
private int m_argIndex;
private int m_stringIndex;
private int m_stringLength;
private int m_lastChar = INVALID;
private int m_lastOptionId;
private CLOption m_option;
private int m_state = STATE_NORMAL;
/**
* Retrieve an array of arguments that have not been parsed
* due to the parser halting.
*
* @return an array of unparsed args
*/
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
* <code>null</code> if no command line option is found.
*
* @param id the command line option id
* @return the {@link CLOption} with the specified id, or
* <code>null</code> 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
* <code>null</code> if no command line option is found.
*
* @param name the command line option name
* @return the {@link CLOption} with the specified name, or
* <code>null</code> 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 )
{
final 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
* <code>public static void main(String[] args)</code> method.
* @param optionDescriptors the option descriptors
* @param control the parser control used determine behaviour of parser
*/
public CLArgsParser( final String[] args,
final CLOptionDescriptor[] optionDescriptors,
final ParserControl control )
{
m_optionDescriptors = optionDescriptors;
m_control = control;
m_options = new Vector();
m_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.getDescriptor().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.getDescriptor().getId();
for( int j = 0; j < incompatible.length; j++ )
{
if( id == incompatible[j] )
{
final CLOption originalOption = (CLOption)arguments.elementAt( original );
final int originalId = originalOption.getDescriptor().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 "<parameter>";
}
else
{
final StringBuffer sb = new StringBuffer();
boolean hasCharOption = false;
if( Character.isLetter( (char)id ) )
{
sb.append( '-' ); // $NON-NLS-1$
sb.append( (char)id );
hasCharOption = true;
}
final String longOption = descriptor.getName();
if( null != longOption )
{
if( hasCharOption )
{
sb.append( '/' ); // $NON-NLS-1$
}
sb.append( "--" ); // $NON-NLS-1$
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
*/
private final void parse()
throws ParseException
{
if( 0 == m_args.length )
{
return;
}
m_stringLength = m_args[m_argIndex].length();
//ch = peekAtChar();
while( true )
{
m_ch = peekAtChar();
//System.out.println( "Pre State=" + m_state );
//System.out.println( "Pre Char=" + (char)ch + "/" + (int)ch );
if( m_argIndex >= m_args.length )
{
break;
}
if( null != m_control && m_control.isFinished( m_lastOptionId ) )
{
//this may need mangling due to peeks
m_unparsedArgs = subArray( m_args, m_argIndex, m_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 == m_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( m_args[m_argIndex++] ) );
}
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.getDescriptor().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( "" ); // $NON-NLS-1$
m_options.addElement( m_option );
}
else
{
final CLOptionDescriptor descriptor = getDescriptorFor(
m_option.getDescriptor().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( m_isLong )
{
return "--" + descriptor.getName(); // $NON-NLS-1$
}
else
{
return "-" + (char)descriptor.getId(); // $NON-NLS-1$
}
}
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( m_stringIndex >= m_stringLength )
{
m_argIndex++;
m_stringIndex = 0;
if( m_argIndex < m_args.length )
{
m_stringLength = m_args[m_argIndex].length();
}
else
{
m_stringLength = 0;
}
return 0;
}
if( m_argIndex >= m_args.length )
{
return 0;
}
return m_args[m_argIndex].charAt( m_stringIndex++ );
}
private char m_tokesep; // Keep track of token separator
private final Token nextToken( final char[] separators )
{
m_ch = getChar();
if( isSeparator( m_ch, separators ) )
{
m_tokesep = m_ch;
m_ch = getChar();
return new Token( TOKEN_SEPARATOR, null );
}
final StringBuffer sb = new StringBuffer();
do
{
sb.append( m_ch );
m_ch = getChar();
}
while( !isSeparator( m_ch, separators ) );
m_tokesep = m_ch;
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.getDescriptor().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 );
if( STATE_NORMAL == m_state )
{
addOption( m_option );
}
}
private final void parseShortOption()
throws ParseException
{
m_ch = getChar();
final CLOptionDescriptor descriptor = getDescriptorFor( m_ch );
m_isLong = false;
parseOption( descriptor, "-" + m_ch ); // $NON-NLS-1$
if( STATE_NORMAL == m_state )
{
m_state = STATE_OPTION_MODE;
}
}
private final void parseArguments()
throws ParseException
{
if( STATE_REQUIRE_ARG == m_state )
{
if( '=' == m_ch || 0 == m_ch ) // $NON-NLS-1$
{
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( '-' == m_ch || 0 == m_ch ) // $NON-NLS-1$
{
getChar(); //consume stray character
addOption( m_option );
m_state = STATE_NORMAL;
return;
}
if (m_isLong && '=' != m_tokesep) // Long optional arg must have = as separator
{
addOption(m_option);
m_state = STATE_NORMAL;
return;
}
if( '=' == m_ch ) // $NON-NLS-1$
{
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() )
{
/*
* Fix bug: -D arg1=arg2 was causing parse error; however --define arg1=arg2 is OK
* This seems to be because the parser skips the terminator for the long options,
* but was not doing so for the short options.
*/
if (!m_isLong){
if (0 == peekAtChar()){
getChar();
}
}
final Token token = nextToken( ARG_SEPARATORS );
if( TOKEN_SEPARATOR == token.getType() )
{
final CLOptionDescriptor descriptor = getDescriptorFor(
m_option.getDescriptor().getId() );
final String message =
"Unable to parse first argument for option " +
getOptionDescription( descriptor );
throw new ParseException( message, 0 );
}
else
{
m_option.addArgument( token.getValue() );
}
// Are we about to start a new option?
if (0 == m_ch && '-' == peekAtChar()){ // $NON-NLS-1$
// Yes, so the second argument is missing
m_option.addArgument( "" ); // $NON-NLS-1$
m_options.addElement( m_option );
m_state = STATE_NORMAL;
}
}
else //2nd argument
{
final StringBuffer sb = new StringBuffer();
m_ch = getChar();
while( !isSeparator( m_ch, NULL_SEPARATORS ) )
{
sb.append( m_ch );
m_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( '-' != m_ch ) // $NON-NLS-1$
{
//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
{
m_ch = peekAtChar();
//if it is a short option then parse it else ...
if( '-' != m_ch ) // $NON-NLS-1$
{
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 );
m_isLong = true;
parseOption( descriptor, "--" + optionName ); // $NON-NLS-1$
}
}
}
}
}
/**
* Build the m_optionIndex lookup map for the parsed options.
*/
private final void buildOptionIndex()
{
final int size = m_options.size();
m_optionIndex = new Hashtable( size * 2 );
for( int i = 0; i < size; i++ )
{
final CLOption option = (CLOption)m_options.get( i );
final CLOptionDescriptor optionDescriptor =
getDescriptorFor( option.getDescriptor().getId() );
m_optionIndex.put( new Integer( option.getDescriptor().getId() ), option );
if( null != optionDescriptor &&
null != optionDescriptor.getName() )
{
m_optionIndex.put( optionDescriptor.getName(), option );
}
}
}
}