CommandLineParser.java
001 /*
002  *
003  * Licensed to the Apache Software Foundation (ASF) under one
004  * or more contributor license agreements.  See the NOTICE file
005  * distributed with this work for additional information
006  * regarding copyright ownership.  The ASF licenses this file
007  * to you under the Apache License, Version 2.0 (the
008  * "License"); you may not use this file except in compliance
009  * with the License.  You may obtain a copy of the License at
010  *
011  *   http://www.apache.org/licenses/LICENSE-2.0
012  *
013  * Unless required by applicable law or agreed to in writing,
014  * software distributed under the License is distributed on an
015  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016  * KIND, either express or implied.  See the License for the
017  * specific language governing permissions and limitations
018  * under the License.
019  *
020  */
021 package org.apache.qpid.util;
022 
023 import java.util.ArrayList;
024 import java.util.HashMap;
025 import java.util.Iterator;
026 import java.util.List;
027 import java.util.Map;
028 import java.util.Properties;
029 import java.util.regex.*;
030 
031 /**
032  * CommandLineParser provides a utility for specifying the format of a command line and parsing command lines to ensure
033  * that they fit their specified format. A command line is made up of flags and options, both may be refered to as
034  * options. A flag is an option that does not take an argument (specifying it means it has the value 'true' and not
035  * specifying it means it has the value 'false'). Options must take arguments but they can be set up with defaults so
036  * that they take a default value when not set. Options may be mandatory in wich case it is an error not to specify
037  * them on the command line. Flags are never mandatory because they are implicitly set to false when not specified.
038  *
039  <p/>Some example command lines are:
040  *
041  <ul>
042  <li>This one has two options that expect arguments:
043  <pre>
044  * cruisecontrol -configfile cruisecontrol.xml -port 9000
045  </pre>
046  <li>This has one no-arg flag and two 'free' arguments:
047  <pre>
048  * zip -r project.zip project/*
049  </pre>
050  <li>This one concatenates multiple flags into a single block with only one '-':
051  <pre>
052  * jar -tvf mytar.tar
053  </pre>
054  *
055  <p/>The parsing rules are:
056  *
057  <ol>
058  <li>Flags may be combined after a single '-' because they never take arguments. Normally such flags are single letter
059  * flags but this is only a convention and not enforced. Flags of more than one letter are usually specified on their own.
060  <li>Options expecting arguments must always be on their own.
061  <li>The argument to an option may be seperated from it by whitespace or appended directly onto the option.
062  <li>The argument to an option may never begin with a '-' character.
063  <li>All other arguments not beginning with a '-' character are free arguments that do not belong to any option.
064  <li>The second or later of a set of duplicate or repeated flags are ignored.
065  <li>Options are matched up to the shortest matching option. This is because of the possibility of having no space
066  * between an option and its argument. This rules out the possibility of using two options where one is an opening
067  * substring of the other. For example, the options "foo" and "foobar" cannot be used on the same command line because
068  * it is not possible to distinguish the argument "-foobar" from being the "foobar" option or the "foo" option with
069  * the "bar" argument.
070  </ol>
071  *
072  <p/>By default, unknown options are simply ignored if specified on the command line. This behaviour may be changed
073  * so that the parser reports all unknowns as errors by using the {@link #setErrorsOnUnknowns} method.
074  *
075  <p><table id="crc"><caption>CRC Card</caption>
076  <tr><th> Responsibilities <th> Collaborations
077  <tr><td> Accept a command line specification.
078  <tr><td> Parse a command line into properties, validating it against its specification.
079  <tr><td> Report all errors between a command line and its specification.
080  <tr><td> Provide a formatted usage string for a command line.
081  <tr><td> Provide a formatted options in force string for a command line.
082  <tr><td> Allow errors on unknowns behaviour to be turned on or off.
083  </table>
084  */
085 public class CommandLineParser
086 {
087     /** Holds a mapping from command line option names to detailed information about those options. */
088     private Map<String, CommandLineOption> optionMap = new HashMap<String, CommandLineOption>();
089 
090     /** Holds a list of parsing errors. */
091     private List<String> parsingErrors = new ArrayList<String>();
092 
093     /** Holds the regular expression matcher to match command line options with. */
094     private Matcher optionMatcher = null;
095 
096     /** Holds the parsed command line properties after parsing. */
097     private Properties parsedProperties = null;
098 
099     /** Flag used to indicate that errors should be created for unknown options. False by default. */
100     private boolean errorsOnUnknowns = false;
101 
102     /**
103      * Creates a command line options parser from a command line specification. This is passed to this constructor
104      * as an array of arrays of strings. Each array of strings specifies the command line for a single option. A static
105      * array may therefore easily be used to configure the command line parser in a single method call with an easily
106      * readable format.
107      *
108      <p/>Each array of strings must be 2, 3, 4 or 5 elements long. If any of the last three elements are missing they
109      * are assumed to be null. The elements specify the following parameters:
110      <ol>
111      <li>The name of the option without the leading '-'. For example, "file".  To specify the format of the 'free'
112      *     arguments use the option names "1", "2", ... and so on.
113      <li>The option comment. A line of text describing the usage of the option. For example, "The file to be processed."
114      <li>The options argument. This is a very short description of the argument to the option, often a single word
115      *     or a reminder as to the arguments format. When this element is null the option is a flag and does not
116      *     accept any arguments. For example, "filename" or "(unix | windows)" or null. The actual text specified
117      *     is only used to print in the usage message to remind the user of the usage of the option.
118      <li>The mandatory flag. When set to "true" an option must always be specified. Any other value, including null,
119      *     means that the option is mandatory. Flags are always mandatory (see class javadoc for explanation of why) so
120      *     this is ignored for flags.
121      <li>A regular expression describing the format that the argument must take. Ignored if null.
122      </ol>
123      <p/>An example call to this constructor is:
124      *
125      <pre>
126      * CommandLineParser commandLine = new CommandLineParser(
127      *     new String[][] {{"file", "The file to be processed. ", "filename", "true"},
128      *                     {"dir", "Directory to store results in. Current dir used if not set.", "out dir"},
129      *                     {"os", "Operating system EOL format to use.", "(windows | unix)", null, "windows\|unix"},
130      *                     {"v", "Verbose mode. Prints information about the processing as it goes."},
131      *                     {"1", "The processing command to run.", "command", "true", "add\|remove\|list"}});
132      </pre>
133      *
134      @param config The configuration as an array of arrays of strings.
135      */
136     public CommandLineParser(String[][] config)
137     {
138         // Loop through all the command line option specifications creating details for each in the options map.
139         for (int i = 0; i < config.length; i++)
140         {
141             String[] nextOptionSpec = config[i];
142 
143             addOption(nextOptionSpec[0], nextOptionSpec[1](nextOptionSpec.length > 2? nextOptionSpec[2: null,
144                 (nextOptionSpec.length > 3("true".equals(nextOptionSpec[3]) true false: false,
145                 (nextOptionSpec.length > 4? nextOptionSpec[4null);
146         }
147     }
148 
149     /**
150      * Lists all the parsing errors from the most recent parsing in a string.
151      *
152      @return All the parsing errors from the most recent parsing.
153      */
154     public String getErrors()
155     {
156         // Return the empty string if there are no errors.
157         if (parsingErrors.isEmpty())
158         {
159             return "";
160         }
161 
162         // Concatenate all the parsing errors together.
163         String result = "";
164 
165         for (String s : parsingErrors)
166         {
167             result += s;
168         }
169 
170         return result;
171     }
172 
173     /**
174      * Lists the properties set from the most recent parsing or an empty string if no parsing has been done yet.
175      *
176      @return The properties set from the most recent parsing or an empty string if no parsing has been done yet.
177      */
178     public String getOptionsInForce()
179     {
180         // Check if there are no properties to report and return and empty string if so.
181         if (parsedProperties == null)
182         {
183             return "";
184         }
185 
186         // List all the properties.
187         String result = "Options in force:\n";
188 
189         for (Map.Entry<Object, Object> property : parsedProperties.entrySet())
190         {
191             result += property.getKey() " = " + property.getValue() "\n";
192         }
193 
194         return result;
195     }
196 
197     /**
198      * Generates a usage string consisting of the name of each option and each options argument description and
199      * comment.
200      *
201      @return A usage string for all the options.
202      */
203     public String getUsage()
204     {
205         String result = "Options:\n";
206 
207         // Print usage on each of the command line options.
208         for (CommandLineOption optionInfo : optionMap.values())
209         {
210             result +=
211                 optionInfo.option + " " ((optionInfo.argument != null(optionInfo.argument + " """)
212                 + optionInfo.comment + "\n";
213         }
214 
215         return result;
216     }
217 
218     /**
219      * Control the behaviour of the errors on unkowns reporting. When turned on this reports all unkowns options
220      * as errors. When turned off, all unknowns are simply ignored.
221      *
222      @param errors The setting of the errors on unkown flag. True to turn it on.
223      */
224     public void setErrorsOnUnknowns(boolean errors)
225     {
226         errorsOnUnknowns = errors;
227     }
228 
229     /**
230      * Parses a set of command line arguments into a set of properties, keyed by the argument flag. The free arguments
231      * are keyed by integers as strings starting at "1" and then "2", ... and so on.
232      *
233      <p/>See the class level comment for a description of the parsing rules.
234      *
235      @param args The command line arguments.
236      *
237      @return The arguments as a set of properties.
238      *
239      @throws IllegalArgumentException If the command line cannot be parsed against its specification. If this exception
240      *                                  is thrown a call to {@link #getErrors} will provide a diagnostic of the command
241      *                                  line errors.
242      */
243     public Properties parseCommandLine(String[] argsthrows IllegalArgumentException
244     {
245         Properties options = new Properties();
246 
247         // Used to keep count of the current 'free' argument.
248         int free = 1;
249 
250         // Used to indicate that the most recently parsed option is expecting arguments.
251         boolean expectingArgs = false;
252 
253         // The option that is expecting arguments from the next element of the command line.
254         String optionExpectingArgs = null;
255 
256         // Used to indicate that the most recently parsed option is a duplicate and should be ignored.
257         boolean ignore = false;
258 
259         // Create the regular expression matcher for the command line options.
260         String regexp = "^(";
261         int optionsAdded = 0;
262 
263         for (Iterator<String> i = optionMap.keySet().iterator(); i.hasNext();)
264         {
265             String nextOption = i.next();
266 
267             // Check that the option is not a free argument definition.
268             boolean notFree = false;
269 
270             try
271             {
272                 Integer.parseInt(nextOption);
273             }
274             catch (NumberFormatException e)
275             {
276                 notFree = true;
277             }
278 
279             // Add the option to the regular expression matcher if it is not a free argument definition.
280             if (notFree)
281             {
282                 regexp += nextOption + (i.hasNext() "|" "");
283                 optionsAdded++;
284             }
285         }
286 
287         // There has to be more that one option in the regular expression or else the compiler complains that the close
288         // cannot be nullable if the '?' token is used to make the matched option string optional.
289         regexp += ")" ((optionsAdded > 0"?" """(.*)";
290         Pattern pattern = Pattern.compile(regexp);
291 
292         // Loop through all the command line arguments.
293         for (int i = 0; i < args.length; i++)
294         {
295             // Check if the next command line argument begins with a '-' character and is therefore the start of
296             // an option.
297             if (args[i].startsWith("-"))
298             {
299                 // Extract the value of the option without the leading '-'.
300                 String arg = args[i].substring(1);
301 
302                 // Match up to the longest matching option.
303                 optionMatcher = pattern.matcher(arg);
304                 optionMatcher.matches();
305 
306                 String matchedOption = optionMatcher.group(1);
307 
308                 // Match any argument directly appended onto the longest matching option.
309                 String matchedArg = optionMatcher.group(2);
310 
311                 // Check that a known option was matched.
312                 if ((matchedOption != null&& !"".equals(matchedOption))
313                 {
314                     // Get the command line option information for the matched option.
315                     CommandLineOption optionInfo = optionMap.get(matchedOption);
316 
317                     // Check if this option is expecting arguments.
318                     if (optionInfo.expectsArgs)
319                     {
320                         // The option is expecting arguments so swallow the next command line argument as an
321                         // argument to this option.
322                         expectingArgs = true;
323                         optionExpectingArgs = matchedOption;
324 
325                         // In the mean time set this options argument to the empty string in case no argument is ever
326                         // supplied.
327                         // options.put(matchedOption, "");
328                     }
329 
330                     // Check if the option was matched on its own and is a flag in which case set that flag.
331                     if ("".equals(matchedArg&& !optionInfo.expectsArgs)
332                     {
333                         options.put(matchedOption, "true");
334                     }
335                     // The option was matched as a substring with its argument appended to it or is a flag that is
336                     // condensed together with other flags.
337                     else if (!"".equals(matchedArg))
338                     {
339                         // Check if the option is a flag and therefore is allowed to be condensed together
340                         // with other flags.
341                         if (!optionInfo.expectsArgs)
342                         {
343                             // Set the first matched flag.
344                             options.put(matchedOption, "true");
345 
346                             // Repeat the longest matching process on the remainder but ensure that the remainder
347                             // consists only of flags as only flags may be condensed together in this fashion.
348                             do
349                             {
350                                 // Match the remainder against the options.
351                                 optionMatcher = pattern.matcher(matchedArg);
352                                 optionMatcher.matches();
353 
354                                 matchedOption = optionMatcher.group(1);
355                                 matchedArg = optionMatcher.group(2);
356 
357                                 // Check that an option was matched.
358                                 if (matchedOption != null)
359                                 {
360                                     // Get the command line option information for the next matched option.
361                                     optionInfo = optionMap.get(matchedOption);
362 
363                                     // Ensure that the next option is a flag or raise an error if not.
364                                     if (optionInfo.expectsArgs == true)
365                                     {
366                                         parsingErrors.add("Option " + matchedOption + " cannot be combined with flags.\n");
367                                     }
368 
369                                     options.put(matchedOption, "true");
370                                 }
371                                 // The remainder could not be matched against a flag it is either an unknown flag
372                                 // or an illegal argument to a flag.
373                                 else
374                                 {
375                                     parsingErrors.add("Illegal argument to a flag in the option " + arg + "\n");
376 
377                                     break;
378                                 }
379                             }
380                             // Continue until the remainder of the argument has all been matched with flags.
381                             while (!"".equals(matchedArg));
382                         }
383                         // The option is expecting an argument, so store the unmatched portion against it
384                         // as its argument.
385                         else
386                         {
387                             // Check the arguments format is correct against any specified format.
388                             checkArgumentFormat(optionInfo, matchedArg);
389 
390                             // Store the argument against its option (regardless of its format).
391                             options.put(matchedOption, matchedArg);
392 
393                             // The argument to this flag has already been supplied to it. Do not swallow the
394                             // next command line argument as an argument to this flag.
395                             expectingArgs = false;
396                         }
397                     }
398                 }
399                 else // No matching option was found.
400                 {
401                     // Add this to the list of parsing errors if errors on unkowns is being used.
402                     if (errorsOnUnknowns)
403                     {
404                         parsingErrors.add("Option " + matchedOption + " is not a recognized option.\n");
405                     }
406                 }
407             }
408             // The command line argument did not being with a '-' so it is an argument to the previous flag or it
409             // is a free argument.
410             else
411             {
412                 // Check if a previous flag is expecting to swallow this next argument as its argument.
413                 if (expectingArgs)
414                 {
415                     // Get the option info for the option waiting for arguments.
416                     CommandLineOption optionInfo = optionMap.get(optionExpectingArgs);
417 
418                     // Check the arguments format is correct against any specified format.
419                     checkArgumentFormat(optionInfo, args[i]);
420 
421                     // Store the argument against its option (regardless of its format).
422                     options.put(optionExpectingArgs, args[i]);
423 
424                     // Clear the expecting args flag now that the argument has been swallowed.
425                     expectingArgs = false;
426                     optionExpectingArgs = null;
427                 }
428                 // This command line option is not an argument to any option. Add it to the set of 'free' options.
429                 else
430                 {
431                     // Get the option info for the free option, if there is any.
432                     CommandLineOption optionInfo = optionMap.get(Integer.toString(free));
433 
434                     if (optionInfo != null)
435                     {
436                         // Check the arguments format is correct against any specified format.
437                         checkArgumentFormat(optionInfo, args[i]);
438                     }
439 
440                     // Add to the list of free options.
441                     options.put(Integer.toString(free), args[i]);
442 
443                     // Move on to the next free argument.
444                     free++;
445                 }
446             }
447         }
448 
449         // Scan through all the specified options to check that all mandatory options have been set and that all flags
450         // that were not set are set to false in the set of properties.
451         for (CommandLineOption optionInfo : optionMap.values())
452         {
453             // Check if this is a flag.
454             if (!optionInfo.expectsArgs)
455             {
456                 // Check if the flag is not set in the properties and set it to false if so.
457                 if (!options.containsKey(optionInfo.option))
458                 {
459                     options.put(optionInfo.option, "false");
460                 }
461             }
462             // Check if this is a mandatory option and was not set.
463             else if (optionInfo.mandatory && !options.containsKey(optionInfo.option))
464             {
465                 // Create an error for the missing option.
466                 parsingErrors.add("Option " + optionInfo.option + " is mandatory but not was not specified.\n");
467             }
468         }
469 
470         // Check if there were any errors.
471         if (!parsingErrors.isEmpty())
472         {
473             // Throw an illegal argument exception to signify that there were parsing errors.
474             throw new IllegalArgumentException();
475         }
476 
477         // Convert any name/value pairs in the free arguments into properties in the parsed options.
478         options = takeFreeArgsAsProperties(options, 1);
479 
480         parsedProperties = options;
481 
482         return options;
483     }
484 
485     /**
486      * If a command line has been parsed, calling this method sets all of its parsed options into the specified properties.
487      */
488     public void addCommandLineToProperties(Properties properties)
489     {
490         if (parsedProperties != null)
491         {
492             for (Object propKey : parsedProperties.keySet())
493             {
494                 String name = (StringpropKey;
495                 String value = parsedProperties.getProperty(name);
496 
497                 properties.setProperty(name, value);
498             }
499         }
500     }
501 
502     /**
503      * Resets this command line parser after it has been used to parse a command line. This method will only need
504      * to be called to use this parser a second time which is not likely seeing as a command line is usually only
505      * specified once. However, it is exposed as a public method for the rare case where this may be done.
506      *
507      <p/>Cleans the internal state of this parser, removing all stored errors and information about the options in
508      * force.
509      */
510     public void reset()
511     {
512         parsingErrors = new ArrayList<String>();
513         parsedProperties = null;
514     }
515 
516     /**
517      * Adds the option to list of available command line options.
518      *
519      @param option       The option to add as an available command line option.
520      @param comment      A comment for the option.
521      @param argument     The text that appears after the option in the usage string.
522      @param mandatory    When true, indicates that this option is mandatory.
523      @param formatRegexp The format that the argument must take, defined as a regular expression.
524      */
525     protected void addOption(String option, String comment, String argument, boolean mandatory, String formatRegexp)
526     {
527         // Check if usage text has been set in which case this option is expecting arguments.
528         boolean expectsArgs = ((argument == null|| argument.equals("")) false true;
529 
530         // Add the option to the map of command line options.
531         CommandLineOption opt = new CommandLineOption(option, expectsArgs, comment, argument, mandatory, formatRegexp);
532         optionMap.put(option, opt);
533     }
534 
535     /**
536      * Converts the free arguments into property declarations. After parsing the command line the free arguments
537      * are numbered from 1, such that the parsed properties contain values for the keys "1", "2", ... This method
538      * converts any free arguments declared using the 'name=value' syntax into properties with key 'name', value
539      * 'value'.
540      *
541      <p/>For example the comand line:
542      <pre>
543      * ... debug=true
544      </pre>
545      *
546      <p/>After parsing has properties:
547      <pre>[[1, debug=true]]</pre>
548      *
549      <p/>After applying this method the properties are:
550      <pre>[[1, debug=true], [debug, true]]</pre>
551      *
552      @param properties The parsed command line properties.
553      @param from       The free argument index to convert to properties from.
554      *
555      @return The parsed command line properties, with free argument name value pairs too.
556      */
557     private Properties takeFreeArgsAsProperties(Properties properties, int from)
558     {
559         for (int i = from; true; i++)
560         {
561             String nextFreeArg = properties.getProperty(Integer.toString(i));
562 
563             // Terminate the loop once all free arguments have been consumed.
564             if (nextFreeArg == null)
565             {
566                 break;
567             }
568 
569             // Split it on the =, strip any whitespace and set it as a system property.
570             String[] nameValuePair = nextFreeArg.split("=");
571 
572             if (nameValuePair.length == 2)
573             {
574                 properties.setProperty(nameValuePair[0], nameValuePair[1]);
575             }
576         }
577 
578         return properties;
579     }
580 
581     /**
582      * Checks the format of an argument to an option against its specified regular expression format if one has
583      * been set. Any errors are added to the list of parsing errors.
584      *
585      @param optionInfo The command line option information for the option which is havings its argument checked.
586      @param matchedArg The string argument to the option.
587      */
588     private void checkArgumentFormat(CommandLineOption optionInfo, String matchedArg)
589     {
590         // Check if this option enforces a format for its argument.
591         if (optionInfo.argumentFormatRegexp != null)
592         {
593             Pattern pattern = Pattern.compile(optionInfo.argumentFormatRegexp);
594             Matcher argumentMatcher = pattern.matcher(matchedArg);
595 
596             // Check if the argument does not meet its required format.
597             if (!argumentMatcher.matches())
598             {
599                 // Create an error for this badly formed argument.
600                 parsingErrors.add("The argument to option " + optionInfo.option + " does not meet its required format.\n");
601             }
602         }
603     }
604 
605     /**
606      * Extracts all name=value pairs from the command line, sets them all as system properties and also returns
607      * a map of properties containing them.
608      *
609      @param args        The command line.
610      @param commandLine The command line parser.
611      @param properties  The properties object to inject all parsed properties into (optional may be <tt>null</tt>).
612      *
613      @return A set of properties containing all name=value pairs from the command line.
614      */
615     public static Properties processCommandLine(String[] args, CommandLineParser commandLine, Properties properties)
616     {
617         // Capture the command line arguments or display errors and correct usage and then exit.
618         Properties options = null;
619 
620         try
621         {
622             options = commandLine.parseCommandLine(args);
623 
624             // Add all the trailing command line options (name=value pairs) to system properties. They may be picked up
625             // from there.
626             commandLine.addCommandLineToProperties(properties);
627         }
628         catch (IllegalArgumentException e)
629         {
630             System.out.println(commandLine.getErrors());
631             System.out.println(commandLine.getUsage());
632             System.exit(1);
633         }
634 
635         return options;
636     }
637 
638     /**
639      * Holds information about a command line options. This includes what its name is, whether or not it is a flag,
640      * whether or not it is mandatory, what its user comment is, what its argument reminder text is and what its
641      * regular expression format is.
642      *
643      <p><table id="crc"><caption>CRC Card</caption>
644      <tr><th> Responsibilities <th> Collaborations
645      <tr><td> Hold details of a command line option.
646      </table>
647      */
648     protected class CommandLineOption
649     {
650         /** Holds the text for the flag to match this argument with. */
651         public String option = null;
652 
653         /** Holds a string describing how to use this command line argument. */
654         public String argument = null;
655 
656         /** Flag that determines whether or not this command line argument can take arguments. */
657         public boolean expectsArgs = false;
658 
659         /** Holds a short comment describing what this command line argument is for. */
660         public String comment = null;
661 
662         /** Flag that determines whether or not this is an mandatory command line argument. */
663         public boolean mandatory = false;
664 
665         /** A regular expression describing what format the argument to this option muist have. */
666         public String argumentFormatRegexp = null;
667 
668         /**
669          * Create a command line option object that holds specific information about a command line option.
670          *
671          @param option       The text that matches the option.
672          @param expectsArgs  Whether or not the option expects arguments. It is a flag if this is false.
673          @param comment      A comment explaining how to use this option.
674          @param argument     A short reminder of the format of the argument to this option/
675          @param mandatory    Set to true if this option is mandatory.
676          @param formatRegexp The regular expression that the argument to this option must meet to be valid.
677          */
678         public CommandLineOption(String option, boolean expectsArgs, String comment, String argument, boolean mandatory,
679             String formatRegexp)
680         {
681             this.option = option;
682             this.expectsArgs = expectsArgs;
683             this.comment = comment;
684             this.argument = argument;
685             this.mandatory = mandatory;
686             this.argumentFormatRegexp = formatRegexp;
687         }
688     }
689 }