001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014: * implied.
015: *
016: * See the License for the specific language governing permissions and
017: * limitations under the License.
018: */
019: package org.apache.commons.cli.avalon;
020:
021: // Renamed from org.apache.avalon.excalibur.cli
022:
023: import java.text.ParseException;
024: import java.util.Hashtable;
025: import java.util.Vector;
026:
027: /**
028: * Parser for command line arguments.
029: *
030: * This parses command lines according to the standard (?) of GNU utilities.
031: *
032: * Note: This is still used in 1.1 libraries so do not add 1.2+ dependencies.
033: *
034: * Note that CLArgs uses a backing hashtable for the options index and so
035: * duplicate arguments are only returned by getArguments().
036: *
037: * @see ParserControl
038: * @see CLOption
039: * @see CLOptionDescriptor
040: */
041: public final class CLArgsParser {
042: // cached character == Integer.MAX_VALUE when invalid
043: private static final int INVALID = Integer.MAX_VALUE;
044:
045: private static final int STATE_NORMAL = 0;
046:
047: private static final int STATE_REQUIRE_2ARGS = 1;
048:
049: private static final int STATE_REQUIRE_ARG = 2;
050:
051: private static final int STATE_OPTIONAL_ARG = 3;
052:
053: private static final int STATE_NO_OPTIONS = 4;
054:
055: private static final int STATE_OPTION_MODE = 5;
056:
057: // Values for creating tokens
058: private static final int TOKEN_SEPARATOR = 0;
059:
060: private static final int TOKEN_STRING = 1;
061:
062: private static final char[] ARG_SEPARATORS = new char[] { (char) 0,
063: '=' };
064:
065: private static final char[] NULL_SEPARATORS = new char[] { (char) 0 };
066:
067: private final CLOptionDescriptor[] m_optionDescriptors;
068:
069: private final Vector m_options;
070:
071: private Hashtable m_optionIndex;
072:
073: private final ParserControl m_control;
074:
075: private String m_errorMessage;
076:
077: private String[] m_unparsedArgs = new String[] {};
078:
079: // variables used while parsing options.
080: private char m_ch;
081:
082: private String[] m_args;
083:
084: private boolean m_isLong;
085:
086: private int m_argIndex;
087:
088: private int m_stringIndex;
089:
090: private int m_stringLength;
091:
092: private int m_lastChar = INVALID;
093:
094: private int m_lastOptionId;
095:
096: private CLOption m_option;
097:
098: private int m_state = STATE_NORMAL;
099:
100: /**
101: * Retrieve an array of arguments that have not been parsed due to the
102: * parser halting.
103: *
104: * @return an array of unparsed args
105: */
106: public final String[] getUnparsedArgs() {
107: return m_unparsedArgs;
108: }
109:
110: /**
111: * Retrieve a list of options that were parsed from command list.
112: *
113: * @return the list of options
114: */
115: public final Vector getArguments() {
116: // System.out.println( "Arguments: " + m_options );
117: return m_options;
118: }
119:
120: /**
121: * Retrieve the {@link CLOption} with specified id, or <code>null</code>
122: * if no command line option is found.
123: *
124: * @param id
125: * the command line option id
126: * @return the {@link CLOption} with the specified id, or <code>null</code>
127: * if no CLOption is found.
128: * @see CLOption
129: */
130: public final CLOption getArgumentById(final int id) {
131: return (CLOption) m_optionIndex.get(new Integer(id));
132: }
133:
134: /**
135: * Retrieve the {@link CLOption} with specified name, or <code>null</code>
136: * if no command line option is found.
137: *
138: * @param name
139: * the command line option name
140: * @return the {@link CLOption} with the specified name, or
141: * <code>null</code> if no CLOption is found.
142: * @see CLOption
143: */
144: public final CLOption getArgumentByName(final String name) {
145: return (CLOption) m_optionIndex.get(name);
146: }
147:
148: /**
149: * Get Descriptor for option id.
150: *
151: * @param id
152: * the id
153: * @return the descriptor
154: */
155: private final CLOptionDescriptor getDescriptorFor(final int id) {
156: for (int i = 0; i < m_optionDescriptors.length; i++) {
157: if (m_optionDescriptors[i].getId() == id) {
158: return m_optionDescriptors[i];
159: }
160: }
161:
162: return null;
163: }
164:
165: /**
166: * Retrieve a descriptor by name.
167: *
168: * @param name
169: * the name
170: * @return the descriptor
171: */
172: private final CLOptionDescriptor getDescriptorFor(final String name) {
173: for (int i = 0; i < m_optionDescriptors.length; i++) {
174: if (m_optionDescriptors[i].getName().equals(name)) {
175: return m_optionDescriptors[i];
176: }
177: }
178:
179: return null;
180: }
181:
182: /**
183: * Retrieve an error message that occured during parsing if one existed.
184: *
185: * @return the error string
186: */
187: public final String getErrorString() {
188: // System.out.println( "ErrorString: " + m_errorMessage );
189: return m_errorMessage;
190: }
191:
192: /**
193: * Require state to be placed in for option.
194: *
195: * @param descriptor
196: * the Option Descriptor
197: * @return the state
198: */
199: private final int getStateFor(final CLOptionDescriptor descriptor) {
200: final int flags = descriptor.getFlags();
201: if ((flags & CLOptionDescriptor.ARGUMENTS_REQUIRED_2) == CLOptionDescriptor.ARGUMENTS_REQUIRED_2) {
202: return STATE_REQUIRE_2ARGS;
203: } else if ((flags & CLOptionDescriptor.ARGUMENT_REQUIRED) == CLOptionDescriptor.ARGUMENT_REQUIRED) {
204: return STATE_REQUIRE_ARG;
205: } else if ((flags & CLOptionDescriptor.ARGUMENT_OPTIONAL) == CLOptionDescriptor.ARGUMENT_OPTIONAL) {
206: return STATE_OPTIONAL_ARG;
207: } else {
208: return STATE_NORMAL;
209: }
210: }
211:
212: /**
213: * Create a parser that can deal with options and parses certain args.
214: *
215: * @param args
216: * the args, typically that passed to the
217: * <code>public static void main(String[] args)</code> method.
218: * @param optionDescriptors
219: * the option descriptors
220: * @param control
221: * the parser control used determine behaviour of parser
222: */
223: public CLArgsParser(final String[] args,
224: final CLOptionDescriptor[] optionDescriptors,
225: final ParserControl control) {
226: m_optionDescriptors = optionDescriptors;
227: m_control = control;
228: m_options = new Vector();
229: m_args = args;
230:
231: try {
232: parse();
233: checkIncompatibilities(m_options);
234: buildOptionIndex();
235: } catch (final ParseException pe) {
236: m_errorMessage = pe.getMessage();
237: }
238:
239: // System.out.println( "Built : " + m_options );
240: // System.out.println( "From : " + Arrays.asList( args ) );
241: }
242:
243: /**
244: * Check for duplicates of an option. It is an error to have duplicates
245: * unless appropriate flags is set in descriptor.
246: *
247: * @param arguments
248: * the arguments
249: */
250: private final void checkIncompatibilities(final Vector arguments)
251: throws ParseException {
252: final int size = arguments.size();
253:
254: for (int i = 0; i < size; i++) {
255: final CLOption option = (CLOption) arguments.elementAt(i);
256: final int id = option.getDescriptor().getId();
257: final CLOptionDescriptor descriptor = getDescriptorFor(id);
258:
259: // this occurs when id == 0 and user has not supplied a descriptor
260: // for arguments
261: if (null == descriptor) {
262: continue;
263: }
264:
265: final int[] incompatible = descriptor.getIncompatible();
266:
267: checkIncompatible(arguments, incompatible, i);
268: }
269: }
270:
271: private final void checkIncompatible(final Vector arguments,
272: final int[] incompatible, final int original)
273: throws ParseException {
274: final int size = arguments.size();
275:
276: for (int i = 0; i < size; i++) {
277: if (original == i) {
278: continue;
279: }
280:
281: final CLOption option = (CLOption) arguments.elementAt(i);
282: final int id = option.getDescriptor().getId();
283:
284: for (int j = 0; j < incompatible.length; j++) {
285: if (id == incompatible[j]) {
286: final CLOption originalOption = (CLOption) arguments
287: .elementAt(original);
288: final int originalId = originalOption
289: .getDescriptor().getId();
290:
291: String message = null;
292:
293: if (id == originalId) {
294: message = "Duplicate options for "
295: + describeDualOption(originalId)
296: + " found.";
297: } else {
298: message = "Incompatible options -"
299: + describeDualOption(id) + " and "
300: + describeDualOption(originalId)
301: + " found.";
302: }
303: throw new ParseException(message, 0);
304: }
305: }
306: }
307: }
308:
309: private final String describeDualOption(final int id) {
310: final CLOptionDescriptor descriptor = getDescriptorFor(id);
311: if (null == descriptor) {
312: return "<parameter>";
313: } else {
314: final StringBuffer sb = new StringBuffer();
315: boolean hasCharOption = false;
316:
317: if (Character.isLetter((char) id)) {
318: sb.append('-');
319: sb.append((char) id);
320: hasCharOption = true;
321: }
322:
323: final String longOption = descriptor.getName();
324: if (null != longOption) {
325: if (hasCharOption) {
326: sb.append('/');
327: }
328: sb.append("--");
329: sb.append(longOption);
330: }
331:
332: return sb.toString();
333: }
334: }
335:
336: /**
337: * Create a parser that deals with options and parses certain args.
338: *
339: * @param args
340: * the args
341: * @param optionDescriptors
342: * the option descriptors
343: */
344: public CLArgsParser(final String[] args,
345: final CLOptionDescriptor[] optionDescriptors) {
346: this (args, optionDescriptors, null);
347: }
348:
349: /**
350: * Create a string array that is subset of input array. The sub-array should
351: * start at array entry indicated by index. That array element should only
352: * include characters from charIndex onwards.
353: *
354: * @param array
355: * the original array
356: * @param index
357: * the cut-point in array
358: * @param charIndex
359: * the cut-point in element of array
360: * @return the result array
361: */
362: private final String[] subArray(final String[] array,
363: final int index, final int charIndex) {
364: final int remaining = array.length - index;
365: final String[] result = new String[remaining];
366:
367: if (remaining > 1) {
368: System
369: .arraycopy(array, index + 1, result, 1,
370: remaining - 1);
371: }
372:
373: result[0] = array[index].substring(charIndex - 1);
374:
375: return result;
376: }
377:
378: /**
379: * Actually parse arguments
380: */
381: private final void parse() throws ParseException {
382: if (0 == m_args.length) {
383: return;
384: }
385:
386: m_stringLength = m_args[m_argIndex].length();
387:
388: while (true) {
389: m_ch = peekAtChar();
390:
391: if (m_argIndex >= m_args.length) {
392: break;
393: }
394:
395: if (null != m_control
396: && m_control.isFinished(m_lastOptionId)) {
397: // this may need mangling due to peeks
398: m_unparsedArgs = subArray(m_args, m_argIndex,
399: m_stringIndex);
400: return;
401: }
402:
403: if (STATE_OPTION_MODE == m_state) {
404: // if get to an arg barrier then return to normal mode
405: // else continue accumulating options
406: if (0 == m_ch) {
407: getChar(); // strip the null
408: m_state = STATE_NORMAL;
409: } else {
410: parseShortOption();
411: }
412: } else if (STATE_NORMAL == m_state) {
413: parseNormal();
414: } else if (STATE_NO_OPTIONS == m_state) {
415: // should never get to here when stringIndex != 0
416: addOption(new CLOption(m_args[m_argIndex++]));
417: } else {
418: parseArguments();
419: }
420: }
421:
422: // Reached end of input arguments - perform final processing
423: if (m_option != null) {
424: if (STATE_OPTIONAL_ARG == m_state) {
425: m_options.addElement(m_option);
426: } else if (STATE_REQUIRE_ARG == m_state) {
427: final CLOptionDescriptor descriptor = getDescriptorFor(m_option
428: .getDescriptor().getId());
429: final String message = "Missing argument to option "
430: + getOptionDescription(descriptor);
431: throw new ParseException(message, 0);
432: } else if (STATE_REQUIRE_2ARGS == m_state) {
433: if (1 == m_option.getArgumentCount()) {
434: m_option.addArgument("");
435: m_options.addElement(m_option);
436: } else {
437: final CLOptionDescriptor descriptor = getDescriptorFor(m_option
438: .getDescriptor().getId());
439: final String message = "Missing argument to option "
440: + getOptionDescription(descriptor);
441: throw new ParseException(message, 0);
442: }
443: } else {
444: throw new ParseException("IllegalState " + m_state
445: + ": " + m_option, 0);
446: }
447: }
448: }
449:
450: private final String getOptionDescription(
451: final CLOptionDescriptor descriptor) {
452: if (m_isLong) {
453: return "--" + descriptor.getName();
454: } else {
455: return "-" + (char) descriptor.getId();
456: }
457: }
458:
459: private final char peekAtChar() {
460: if (INVALID == m_lastChar) {
461: m_lastChar = readChar();
462: }
463: return (char) m_lastChar;
464: }
465:
466: private final char getChar() {
467: if (INVALID != m_lastChar) {
468: final char result = (char) m_lastChar;
469: m_lastChar = INVALID;
470: return result;
471: } else {
472: return readChar();
473: }
474: }
475:
476: private final char readChar() {
477: if (m_stringIndex >= m_stringLength) {
478: m_argIndex++;
479: m_stringIndex = 0;
480:
481: if (m_argIndex < m_args.length) {
482: m_stringLength = m_args[m_argIndex].length();
483: } else {
484: m_stringLength = 0;
485: }
486:
487: return 0;
488: }
489:
490: if (m_argIndex >= m_args.length) {
491: return 0;
492: }
493:
494: return m_args[m_argIndex].charAt(m_stringIndex++);
495: }
496:
497: private char m_tokesep; // Keep track of token separator
498:
499: private final Token nextToken(final char[] separators) {
500: m_ch = getChar();
501:
502: if (isSeparator(m_ch, separators)) {
503: m_tokesep = m_ch;
504: m_ch = getChar();
505: return new Token(TOKEN_SEPARATOR, null);
506: }
507:
508: final StringBuffer sb = new StringBuffer();
509:
510: do {
511: sb.append(m_ch);
512: m_ch = getChar();
513: } while (!isSeparator(m_ch, separators));
514:
515: m_tokesep = m_ch;
516: return new Token(TOKEN_STRING, sb.toString());
517: }
518:
519: private final boolean isSeparator(final char ch,
520: final char[] separators) {
521: for (int i = 0; i < separators.length; i++) {
522: if (ch == separators[i]) {
523: return true;
524: }
525: }
526:
527: return false;
528: }
529:
530: private final void addOption(final CLOption option) {
531: m_options.addElement(option);
532: m_lastOptionId = option.getDescriptor().getId();
533: m_option = null;
534: }
535:
536: private final void parseOption(final CLOptionDescriptor descriptor,
537: final String optionString) throws ParseException {
538: if (null == descriptor) {
539: throw new ParseException("Unknown option " + optionString,
540: 0);
541: }
542:
543: m_state = getStateFor(descriptor);
544: m_option = new CLOption(descriptor);
545:
546: if (STATE_NORMAL == m_state) {
547: addOption(m_option);
548: }
549: }
550:
551: private final void parseShortOption() throws ParseException {
552: m_ch = getChar();
553: final CLOptionDescriptor descriptor = getDescriptorFor(m_ch);
554: m_isLong = false;
555: parseOption(descriptor, "-" + m_ch);
556:
557: if (STATE_NORMAL == m_state) {
558: m_state = STATE_OPTION_MODE;
559: }
560: }
561:
562: private final void parseArguments() throws ParseException {
563: if (STATE_REQUIRE_ARG == m_state) {
564: if ('=' == m_ch || 0 == m_ch) {
565: getChar();
566: }
567:
568: final Token token = nextToken(NULL_SEPARATORS);
569: m_option.addArgument(token.getValue());
570:
571: addOption(m_option);
572: m_state = STATE_NORMAL;
573: } else if (STATE_OPTIONAL_ARG == m_state) {
574: if ('-' == m_ch || 0 == m_ch) {
575: getChar(); // consume stray character
576: addOption(m_option);
577: m_state = STATE_NORMAL;
578: return;
579: }
580:
581: if (m_isLong && '=' != m_tokesep) { // Long optional arg must have = as separator
582: addOption(m_option);
583: m_state = STATE_NORMAL;
584: return;
585: }
586:
587: if ('=' == m_ch) {
588: getChar();
589: }
590:
591: final Token token = nextToken(NULL_SEPARATORS);
592: m_option.addArgument(token.getValue());
593:
594: addOption(m_option);
595: m_state = STATE_NORMAL;
596: } else if (STATE_REQUIRE_2ARGS == m_state) {
597: if (0 == m_option.getArgumentCount()) {
598: /*
599: * Fix bug: -D arg1=arg2 was causing parse error; however
600: * --define arg1=arg2 is OK This seems to be because the parser
601: * skips the terminator for the long options, but was not doing
602: * so for the short options.
603: */
604: if (!m_isLong) {
605: if (0 == peekAtChar()) {
606: getChar();
607: }
608: }
609: final Token token = nextToken(ARG_SEPARATORS);
610:
611: if (TOKEN_SEPARATOR == token.getType()) {
612: final CLOptionDescriptor descriptor = getDescriptorFor(m_option
613: .getDescriptor().getId());
614: final String message = "Unable to parse first argument for option "
615: + getOptionDescription(descriptor);
616: throw new ParseException(message, 0);
617: } else {
618: m_option.addArgument(token.getValue());
619: }
620: // Are we about to start a new option?
621: if (0 == m_ch && '-' == peekAtChar()) {
622: // Yes, so the second argument is missing
623: m_option.addArgument("");
624: m_options.addElement(m_option);
625: m_state = STATE_NORMAL;
626: }
627: } else // 2nd argument
628: {
629: final StringBuffer sb = new StringBuffer();
630:
631: m_ch = getChar();
632: while (!isSeparator(m_ch, NULL_SEPARATORS)) {
633: sb.append(m_ch);
634: m_ch = getChar();
635: }
636:
637: final String argument = sb.toString();
638:
639: // System.out.println( "Arguement:" + argument );
640:
641: m_option.addArgument(argument);
642: addOption(m_option);
643: m_option = null;
644: m_state = STATE_NORMAL;
645: }
646: }
647: }
648:
649: /**
650: * Parse Options from Normal mode.
651: */
652: private final void parseNormal() throws ParseException {
653: if ('-' != m_ch) {
654: // Parse the arguments that are not options
655: final String argument = nextToken(NULL_SEPARATORS)
656: .getValue();
657: addOption(new CLOption(argument));
658: m_state = STATE_NORMAL;
659: } else {
660: getChar(); // strip the -
661:
662: if (0 == peekAtChar()) {
663: throw new ParseException("Malformed option -", 0);
664: } else {
665: m_ch = peekAtChar();
666:
667: // if it is a short option then parse it else ...
668: if ('-' != m_ch) {
669: parseShortOption();
670: } else {
671: getChar(); // strip the -
672: // -- sequence .. it can either mean a change of state
673: // to STATE_NO_OPTIONS or else a long option
674:
675: if (0 == peekAtChar()) {
676: getChar();
677: m_state = STATE_NO_OPTIONS;
678: } else {
679: // its a long option
680: final String optionName = nextToken(
681: ARG_SEPARATORS).getValue();
682: final CLOptionDescriptor descriptor = getDescriptorFor(optionName);
683: m_isLong = true;
684: parseOption(descriptor, "--" + optionName);
685: }
686: }
687: }
688: }
689: }
690:
691: /**
692: * Build the m_optionIndex lookup map for the parsed options.
693: */
694: private final void buildOptionIndex() {
695: final int size = m_options.size();
696: m_optionIndex = new Hashtable(size * 2);
697:
698: for (int i = 0; i < size; i++) {
699: final CLOption option = (CLOption) m_options.get(i);
700: final CLOptionDescriptor optionDescriptor = getDescriptorFor(option
701: .getDescriptor().getId());
702:
703: m_optionIndex.put(new Integer(option.getDescriptor()
704: .getId()), option);
705:
706: if (null != optionDescriptor
707: && null != optionDescriptor.getName()) {
708: m_optionIndex.put(optionDescriptor.getName(), option);
709: }
710: }
711: }
712: }
|