View Javadoc

1   /*
2    * $Id: CommandLineArgumentEvaluator.java 178 2010-10-31 18:01:20Z roland $
3    * Copyright (C) 2007 Roland Krueger
4    * Created on 21.02.2006
5    *
6    * Author: Roland Krueger (www.rolandkrueger.info)
7    *
8    * This file is part of RoKlib.
9    *
10   * This library is free software; you can redistribute it and/or
11   * modify it under the terms of the GNU Lesser General Public License
12   * as published by the Free Software Foundation; either version 2.1 of
13   * the License, or (at your option) any later version.
14   *
15   * This library is distributed in the hope that it will be useful, but
16   * WITHOUT ANY WARRANTY; without even the implied warranty of
17   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18   * Lesser General Public License for more details.
19   *
20   * You should have received a copy of the GNU Lesser General Public
21   * License along with this library; if not, write to the Free Software
22   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
23   * USA
24   */
25  package info.rolandkrueger.roklib.cli;
26  
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashSet;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Set;
35  
36  /**
37   * This class can be used to handle a program's command line options in an easy
38   * and uncomplicated way. To use a {@link CommandLineArgumentEvaluator}, you
39   * first have to define the set of all available command line options and pass
40   * them to the {@link CommandLineArgumentEvaluator} with the
41   * {@link CommandLineArgumentEvaluator#addOptions(CommandLineOption...)} or
42   * {@link CommandLineArgumentEvaluator#addOptions(Collection)} methods. After
43   * that, the evaluator object can be queried if the program was provided with
44   * the correct options.
45   * 
46   * @see CommandLineOption
47   * @author Roland Krueger
48   */
49  public class CommandLineArgumentEvaluator
50  {
51    /**
52     * Enum which is used to describe whether the arguments provided by the user
53     * via the command line were valid and if not so, what was wrong about them.
54     * After the command line arguments have been evaluated with
55     * {@link CommandLineArgumentEvaluator#evaluate()} the result of this
56     * evaluation can be queried with
57     * {@link CommandLineArgumentEvaluator#getInputStatus()}. This method returns
58     * {@link UserInputValidity#OK} if the argument list was valid or some of the
59     * other values if it was not. In case of an invalid argument list, the
60     * returned value indicates the type of error made by the user.
61     * 
62     * @see CommandLineArgumentEvaluator#getInputStatus()
63     */
64    public enum UserInputValidity
65    {
66      /**
67       * Information about the validity of the user input is not yet available.
68       * This is only the case after invoking
69       * {@link CommandLineArgumentEvaluator#evaluate()}. If
70       * {@link CommandLineArgumentEvaluator#getInputStatus()} is called before
71       * evaluating the argument list an exception is raised.
72       */
73      NOT_YET_EVALUATED,
74      /**
75       * Everything is OK, the user input is valid.
76       */
77      OK,
78      /**
79       * The user has given an option that is unknown, i.e. it is not contained in
80       * the internal option list.
81       */
82      UNRECOGNIZED_OPTION,
83      /**
84       * There are too few options. Not every mandatory option has been provided.
85       */
86      TOO_FEW_OPTIONS,
87      /**
88       * The user has either provided a value for a flag option or given more than
89       * one values for a singular option.
90       */
91      TOO_MANY_VALUES,
92      /**
93       * There are two {@link CommandLineOption}s which have been configured as
94       * associate options. Thus, if one of them is used in the argument list, the
95       * other option must also be provided. If only one of two associate options
96       * is used, though, the input status of this
97       * {@link CommandLineArgumentEvaluator} is set to this value.
98       */
99      MISSING_ASSOCIATE_OPTION,
100     /**
101      * If a ({@link CommandLineOptionType#LIST}) option or a (
102      * {@link CommandLineOptionType#SINGULAR}) option was used like a (
103      * {@link CommandLineOptionType#FLAG}) option this type of error will be
104      * indicated. I. e., if such an option has not been provided with any value
105      * the input status of this {@link CommandLineArgumentEvaluator} is set to
106      * this value.
107      */
108     MISSING_ARGUMENT,
109     /**
110      * <p>
111      * If two options have been configured as being mutually exclusive then
112      * using both of them in the argument list will result in this error type.
113      * </p>
114      * <p>
115      * The two mutually exclusive options which have both been used can be
116      * queried with
117      * {@link CommandLineArgumentEvaluator#getMutuallyExclusiveOptionsBeingUsed()}
118      * .
119      * </p>
120      * 
121      */
122     MUTUALLY_EXCLUSIVE_OPTIONS_GIVEN,
123     /**
124      * <p>
125      * One option has been given more than once. This option hasn't been
126      * configured as an option which can be given multiple times. Therefore, it
127      * is not allowed to repeat this option.
128      * </p>
129      * <p>
130      * The duplicate option can be queried with
131      * {@link CommandLineArgumentEvaluator#getErroneousOption()}.
132      * </p>
133      * 
134      * @see CommandLineOption#allowOptionRepetition(boolean)
135      */
136     DUPLICATE_OPTION
137   };
138 
139   private String mShortOptionsMarker = "-";
140   private String mLongOptionsMarker = "--";
141 
142   private Set<CommandLineOption> mOptionSet;
143   private Set<CommandLineOption> mMandatoryList;
144   private String[] mArgs;
145   private UserInputValidity mStatus = UserInputValidity.NOT_YET_EVALUATED;
146   private boolean mEvaluated = false;
147   private boolean mAllowClusteredShortOptions = true;
148   private boolean mAllowStandaloneValues = false;
149   private String mDescription;
150   private List<String> mStandaloneValues;
151 
152   private String mUnrecognizedOption;
153   private CommandLineOption mErroneousOption;
154   private List<CommandLineOption> mMutuallyExclusiveOptionsGiven;
155 
156   public CommandLineArgumentEvaluator ()
157   {
158     mOptionSet = new HashSet<CommandLineOption> ();
159     mMandatoryList = new HashSet<CommandLineOption> ();
160     mDescription = "";
161   }
162 
163   /**
164    * Creates a new {@link CommandLineArgumentEvaluator} object that is supposed
165    * to check the given command line options.
166    * 
167    * @param arguments
168    *          the command line options for this application as optained by the
169    *          <code>main()</code> method, for example
170    */
171   public CommandLineArgumentEvaluator (String[] arguments)
172   {
173     this ();
174     mArgs = arguments;
175   }
176 
177   public void setArgumentList (String[] args)
178   {
179     if (args == null) throw new NullPointerException ("Argument list is null.");
180     mArgs = args;
181   }
182 
183   /**
184    * Adds one or more options to the internal list of all available options.
185    * 
186    * @param options
187    *          an option array which contains all necessary information of a
188    *          specific command line option
189    */
190   public void addOptions (CommandLineOption... options)
191   {
192     for (CommandLineOption option : options)
193     {
194       for (CommandLineOption opt : mOptionSet)
195       {
196         if (opt.equals (option))
197           throw new IllegalArgumentException (String.format ("Option %s has already been added.",
198               option));
199       }
200       mOptionSet.add (option);
201       if (option.isMandatory ())
202       {
203         mMandatoryList.add (option);
204       }
205     }
206   }
207 
208   public void allowClusteredShortOptions (boolean yesNo)
209   {
210     mAllowClusteredShortOptions = yesNo;
211   }
212 
213   public void allowStandAloneValues (boolean yesNo)
214   {
215     mAllowStandaloneValues = yesNo;
216   }
217 
218   public List<String> getStandaloneValues ()
219   {
220     if (! mAllowStandaloneValues)
221       throw new IllegalStateException ("Stand-alone values are not allowed for this evaluator.");
222 
223     if (mStandaloneValues == null) return Collections.emptyList ();
224     return mStandaloneValues;
225   }
226 
227   /**
228    * Adds a list of {@link CommandLineOption} objects to the internal option
229    * list of this evaluator
230    * 
231    * @param options
232    *          a collection of {@link CommandLineOption} objects
233    */
234   public void addOptions (Collection<CommandLineOption> options)
235   {
236     for (CommandLineOption option : options)
237     {
238       addOptions (option);
239     }
240   }
241 
242   /**
243    * Returns the internal list of {@link CommandLineOption} objects.
244    * 
245    * @return the internal list of {@link CommandLineOption} objects.
246    */
247   public Set<CommandLineOption> getOptions ()
248   {
249     return mOptionSet;
250   }
251 
252   /**
253    * After all available options have been configured and added to the evaluator
254    * object, the command line options given through the constructor are
255    * evaluated, such that after that it can be checked whether the user-provided
256    * options are correct or not. Note that this method can only be called once
257    * and only once.
258    * 
259    * @throws IllegalStateException
260    *           if this method was called more than once
261    */
262   public void evaluate ()
263   {
264     if (mEvaluated) throw new IllegalStateException ("evaluate() has already been called.");
265     if (mArgs == null)
266       throw new IllegalStateException (
267           "No argument list has been set yet. Call setArgumentList() first.");
268 
269     mEvaluated = true;
270     mStatus = UserInputValidity.OK;
271 
272     List<String> argList = new ArrayList<String> (Arrays.asList (mArgs));
273     // first, break up arguments in the form "argument=value" so that they can
274     // be evaluated in the same
275     // way as all other arguments
276     for (String value : new ArrayList<String> (argList))
277     {
278       if (value.indexOf ('=') != - 1)
279       {
280         List<String> splitList = splitAtEqualSign (value);
281         if (getOptionFor (splitList.get (0), true) != null)
282         {
283           int index = argList.indexOf (value);
284           argList.remove (value);
285           if (splitList.size () > 1) argList.add (index, splitList.get (1));
286           argList.add (index, splitList.get (0));
287         }
288       }
289     }
290 
291     CommandLineOption currentOption = null;
292 
293     // walk over the argument list
294     for (String value : argList)
295     {
296       // try to find an option object for the current argument fragment
297       CommandLineOption option = getOptionFor (value, true);
298 
299       if (option == null && currentOption == null)
300       {
301         // No option object could be found matching the current argument
302         // fragment and
303         // there is no current list option to which the current value could be
304         // added.
305         // Check if the current argument fragment is a flag cluster.
306         if (mAllowClusteredShortOptions && value.startsWith (mShortOptionsMarker))
307         {
308           if (! disassembleFlagCluster (value))
309           {
310             if (setUnrecognizedOption (value)) return;
311           }
312         } else
313         {
314           if (setUnrecognizedOption (value)) return;
315         }
316       }
317 
318       if (option == null && currentOption != null)
319       {
320         // No option could be found but there is a current list option to which
321         // the current
322         // value could be added
323         if (! currentOption.addUserInput (value) && ! disassembleFlagCluster (value))
324         {
325           mStatus = UserInputValidity.TOO_MANY_VALUES;
326           setErroneousOption (currentOption);
327           return;
328         }
329         continue;
330       }
331 
332       if (option != null)
333       {
334         if (option.isFlag ())
335         {
336           if (option.isSet () && option.isOptionRepetitionAllowed ())
337           {
338             option.increaseRepetitions ();
339           } else if (option.isSet () && ! option.isOptionRepetitionAllowed ())
340           {
341             mStatus = UserInputValidity.DUPLICATE_OPTION;
342             setErroneousOption (option);
343             return;
344           } else
345           {
346             option.set ();
347             continue;
348           }
349         }
350         currentOption = option;
351         currentOption.set ();
352       }
353     }
354 
355     for (CommandLineOption option : mMandatoryList)
356     {
357       if (! option.isSet ()) mStatus = UserInputValidity.TOO_FEW_OPTIONS;
358     }
359 
360     for (CommandLineOption option : mOptionSet)
361     {
362       if (option.isSet ())
363       {
364         if ((option.getType () == CommandLineOptionType.LIST || option.getType () == CommandLineOptionType.SINGULAR)
365             && option.getInputList ().length == 0)
366         {
367           mStatus = UserInputValidity.MISSING_ARGUMENT;
368           setErroneousOption (option);
369           return;
370         }
371 
372         for (CommandLineOption other : mOptionSet)
373         {
374           if (option.isMutuallyExclusiveWith (other) && other.isSet ())
375           {
376             mStatus = UserInputValidity.MUTUALLY_EXCLUSIVE_OPTIONS_GIVEN;
377             mMutuallyExclusiveOptionsGiven = new LinkedList<CommandLineOption> ();
378             mMutuallyExclusiveOptionsGiven.add (option);
379             mMutuallyExclusiveOptionsGiven.add (other);
380             return;
381           }
382 
383           if (option.isAssociateTo (other) && ! other.isSet ())
384           {
385             mStatus = UserInputValidity.MISSING_ASSOCIATE_OPTION;
386             setErroneousOption (option);
387             return;
388           }
389         }
390       }
391     }
392   }
393 
394   private boolean setUnrecognizedOption (String option)
395   {
396     if (mAllowStandaloneValues)
397     {
398       if (mStandaloneValues == null) mStandaloneValues = new LinkedList<String> ();
399       mStandaloneValues.add (option);
400       return false;
401     }
402     mUnrecognizedOption = option;
403     mStatus = UserInputValidity.UNRECOGNIZED_OPTION;
404     return true;
405   }
406 
407   public String getUnrecognizedOption ()
408   {
409     if (mStatus != UserInputValidity.UNRECOGNIZED_OPTION)
410       throw new IllegalStateException ("No unrecognized option was given.");
411     return mUnrecognizedOption;
412   }
413 
414   private void setErroneousOption (CommandLineOption erroneous)
415   {
416     mErroneousOption = erroneous;
417   }
418 
419   public CommandLineOption getErroneousOption ()
420   {
421     if (mErroneousOption == null)
422       throw new IllegalStateException ("No option has been set incorrectly.");
423     return mErroneousOption;
424   }
425 
426   public String getDescriptiveSummaryFor (CommandLineOption option)
427   {
428     return getDescriptiveSummaryFor (option, "\n\t\t");
429   }
430 
431   public String getDescriptiveSummaryFor (CommandLineOption option, String descriptionSeparator)
432   {
433     String description = option.getDescription ();
434     String longOption = "";
435     if (! option.getLongOption ().equals (""))
436       longOption = String.format ("%s%s", getLongOptionsMarker (), option.getLongOption ());
437     String shortOption = "";
438     if (! option.getShortOption ().equals (""))
439       shortOption = String.format ("%s%s", getShortOptionsMarker (), option.getShortOption ());
440 
441     String separator = description.equals ("") ? "" : descriptionSeparator;
442     if (shortOption.equals (""))
443       return String.format ("%s%s%s", longOption, separator, description);
444 
445     if (longOption.equals (""))
446       return String.format ("%s%s%s", shortOption, separator, description);
447 
448     return String.format ("%s, %s%s%s", shortOption, longOption, separator, description);
449   }
450 
451   public List<CommandLineOption> getMutuallyExclusiveOptionsBeingUsed ()
452   {
453     if (mMutuallyExclusiveOptionsGiven == null)
454       throw new IllegalStateException ("No mutually exclusive options have been used");
455     return mMutuallyExclusiveOptionsGiven;
456   }
457 
458   private boolean disassembleFlagCluster (String value)
459   {
460     List<CommandLineOption> optionsToSet = new LinkedList<CommandLineOption> ();
461     boolean result = true;
462 
463     value = value.replaceAll (mShortOptionsMarker, "");
464     for (int i = 0; i < value.length (); ++i)
465     {
466       String flag = String.valueOf (value.charAt (i));
467       CommandLineOption option = getOptionFor (flag, false);
468       if (option == null)
469       {
470         result = false;
471         break;
472       } else
473       {
474         optionsToSet.add (option);
475       }
476     }
477 
478     if (result)
479     {
480       for (CommandLineOption option : optionsToSet)
481       {
482         if (option.isOptionRepetitionAllowed ())
483         {
484           option.increaseRepetitions ();
485         } else if (option.isSet ())
486         {
487           mStatus = UserInputValidity.DUPLICATE_OPTION;
488           setErroneousOption (option);
489           result = false;
490         }
491         option.set ();
492       }
493     }
494 
495     return result;
496   }
497 
498   private List<String> splitAtEqualSign (String value)
499   {
500     List<String> result = new LinkedList<String> ();
501     assert value.indexOf ('=') != - 1;
502 
503     result.add (value.substring (0, value.indexOf ('=')));
504     result.add (value.substring (value.indexOf ('=') + 1));
505     return result;
506   }
507 
508   private CommandLineOption getOptionFor (String value, boolean valueContainsMarker)
509   {
510     String longOption;
511     String shortOption;
512 
513     for (CommandLineOption option : mOptionSet)
514     {
515       longOption = valueContainsMarker ? mLongOptionsMarker + option.getLongOption () : option
516           .getLongOption ();
517       shortOption = valueContainsMarker ? mShortOptionsMarker + option.getShortOption () : option
518           .getShortOption ();
519       if (shortOption.equals (value) || longOption.equals (value))
520       {
521         return option;
522       }
523     }
524 
525     return null;
526   }
527 
528   public boolean isValid ()
529   {
530     return getInputStatus () == UserInputValidity.OK;
531   }
532 
533   public void evaluateAndInvokeCallbacks ()
534   {
535     evaluate ();
536     invokeCallbacks ();
537   }
538 
539   public void invokeCallbacks ()
540   {
541     if (! mEvaluated)
542       throw new IllegalStateException (
543           "Command line arguments havn't been evaluated yet. Call evaluate() first.");
544 
545     for (CommandLineOption option : mOptionSet)
546     {
547       if (option.isSet ())
548       {
549         option.invokeCallback (this);
550       }
551     }
552   }
553 
554   /**
555    * Returns the status of the command line options given by the user. This
556    * method can only be called after invoking method
557    * {@link CommandLineArgumentEvaluator#evaluate()}. Otherwise, a
558    * {@link IllegalStateException} will be thrown.
559    * 
560    * @throws IllegalStateException
561    *           if this method is called without invoking
562    *           {@link CommandLineArgumentEvaluator#evaluate()} beforehand
563    * @return the validity status of the user's command line options
564    * @see UserInputValidity
565    */
566   public UserInputValidity getInputStatus ()
567   {
568     if (mStatus == UserInputValidity.NOT_YET_EVALUATED)
569       throw new IllegalStateException (
570           "Command line options haven't been evaluated yet. Call evaluate() first.");
571     return mStatus;
572   }
573 
574   /**
575    * This method will assemble a string containing an overview of all available
576    * options together with a general blueprint of how to correctly use these
577    * options. Additionally for each option a short description is provided as
578    * defined by the respective {@link CommandLineOption} object.
579    * 
580    * @return a string containing an overview over all available options
581    */
582   public String getOptionDescription ()
583   {
584     StringBuffer result = new StringBuffer ();
585     String leftBracket;
586     String rightBracket;
587     String argument;
588     String optionString;
589     for (CommandLineOption option : mOptionSet)
590     {
591       if (option.isMandatory ())
592       {
593         leftBracket = "";
594         rightBracket = "";
595       } else
596       {
597         leftBracket = "[";
598         rightBracket = "]";
599       }
600 
601       if (option.getType () == CommandLineOptionType.LIST)
602         argument = " argument_list...";
603       else if (option.getType () == CommandLineOptionType.SINGULAR)
604         argument = " argument";
605       else
606         argument = "";
607 
608       if (option.getShortOption ().equals (""))
609         optionString = option.getLongOption ();
610       else if (option.getLongOption ().equals (""))
611         optionString = option.getShortOption ();
612       else
613         optionString = String.format ("(%s|%s)", option.getShortOption (), option.getLongOption ());
614 
615       result
616           .append (String.format ("%s%s%s%s ", leftBracket, optionString, argument, rightBracket));
617     }
618     return result.toString ();
619   }
620 
621   /**
622    * If after having specified the set of available and mandatory options and
623    * having called {@link CommandLineArgumentEvaluator#evaluate()} the user has
624    * omitted one or more of the mandatory options, these missing options can be
625    * queried as a list.
626    * 
627    * @return list of mandatory options that were omitted by the user
628    */
629   public List<CommandLineOption> getMissingOptions ()
630   {
631     ArrayList<CommandLineOption> result = new ArrayList<CommandLineOption> ();
632     for (CommandLineOption option : mOptionSet)
633     {
634       if (! option.isSet () && option.isMandatory ()) result.add (option);
635     }
636     return result;
637   }
638 
639   public String getShortOptionsMarker ()
640   {
641     return mShortOptionsMarker;
642   }
643 
644   public void setShortOptionsMarker (String shortOptionsMarker)
645   {
646     if (shortOptionsMarker == null)
647       throw new NullPointerException ("Short options marker is null.");
648 
649     if (mEvaluated) throw new IllegalStateException ("Arguments have already been evaluated.");
650 
651     mShortOptionsMarker = shortOptionsMarker;
652   }
653 
654   public String getLongOptionsMarker ()
655   {
656     return mLongOptionsMarker;
657   }
658 
659   public void setDescription (String description)
660   {
661     if (description == null) mDescription = "";
662     mDescription = description;
663   }
664 
665   public String getDescription ()
666   {
667     return mDescription;
668   }
669 
670   public void setLongOptionsMarker (String longOptionsMarker)
671   {
672     if (longOptionsMarker == null) throw new NullPointerException ("Long options marker is null.");
673 
674     if (mEvaluated) throw new IllegalStateException ("Arguments have already been evaluated.");
675 
676     mLongOptionsMarker = longOptionsMarker;
677   }
678 }