001    /* Parser.java - parse command line options
002     Copyright (C) 2006, 2008 Free Software Foundation, Inc.
003    
004     This file is part of GNU Classpath.
005    
006     GNU Classpath is free software; you can redistribute it and/or modify
007     it under the terms of the GNU General Public License as published by
008     the Free Software Foundation; either version 2, or (at your option)
009     any later version.
010    
011     GNU Classpath is distributed in the hope that it will be useful, but
012     WITHOUT ANY WARRANTY; without even the implied warranty of
013     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     General Public License for more details.
015    
016     You should have received a copy of the GNU General Public License
017     along with GNU Classpath; see the file COPYING.  If not, write to the
018     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019     02110-1301 USA.
020    
021     Linking this library statically or dynamically with other modules is
022     making a combined work based on this library.  Thus, the terms and
023     conditions of the GNU General Public License cover the whole
024     combination.
025    
026     As a special exception, the copyright holders of this library give you
027     permission to link this library with independent modules to produce an
028     executable, regardless of the license terms of these independent
029     modules, and to copy and distribute the resulting executable under
030     terms of your choice, provided that you also meet, for each linked
031     independent module, the terms and conditions of the license of that
032     module.  An independent module is a module which is not derived from
033     or based on this library.  If you modify this library, you may extend
034     this exception to your version of the library, but you are not
035     obligated to do so.  If you do not wish to do so, delete this
036     exception statement from your version. */
037    
038    
039    package gnu.classpath.tools.getopt;
040    
041    import java.io.PrintStream;
042    import java.text.BreakIterator;
043    import java.text.MessageFormat;
044    import java.util.ArrayList;
045    import java.util.Iterator;
046    import java.util.Locale;
047    
048    /**
049     * An instance of this class is used to parse command-line options. It does "GNU
050     * style" argument recognition and also automatically handles "--help" and
051     * "--version" processing. It can also be put in "long option only" mode. In
052     * this mode long options are recognized with a single dash (as well as a double
053     * dash) and strings of options like "-abc" are never parsed as a collection of
054     * short options.
055     */
056    public class Parser
057    {
058      /** The maximum right column position. */
059      public static final int MAX_LINE_LENGTH = 80;
060    
061      private String programName;
062    
063      private String headerText;
064    
065      private String footerText;
066    
067      private boolean longOnly;
068    
069      // All of the options.  This is null initially; users must call
070      // requireOptions before access.
071      private ArrayList options;
072    
073      private ArrayList optionGroups = new ArrayList();
074    
075      private OptionGroup defaultGroup = new OptionGroup();
076    
077      private OptionGroup finalGroup;
078    
079      // These are used while parsing.
080      private int currentIndex;
081    
082      private String[] args;
083    
084      /**
085       * Create a new parser. The program name is used when printing error messages.
086       * The version string is printed verbatim in response to "--version".
087       * 
088       * @param programName the name of the program
089       * @param versionString the program's version information
090       */
091      public Parser(String programName, String versionString)
092      {
093        this(programName, versionString, false);
094      }
095    
096      /**
097       * Print a designated text to a {@link PrintStream}, eventually wrapping the
098       * lines of text so as to ensure that the width of each line does not overflow
099       * {@link #MAX_LINE_LENGTH} columns. The line-wrapping is done with a
100       * {@link BreakIterator} using the default {@link Locale}.
101       * <p>
102       * The text to print may contain <code>\n</code> characters. This method will
103       * force a line-break for each such character.
104       * 
105       * @param out the {@link PrintStream} destination of the formatted text.
106       * @param text the text to print.
107       * @see Parser#MAX_LINE_LENGTH
108       */
109      protected static void formatText(PrintStream out, String text)
110      {
111        formatText(out, text, Locale.getDefault());
112      }
113    
114      /**
115       * Similar to the method with the same name and two arguments, except that the
116       * caller MUST specify a non-null {@link Locale} instance.
117       * <p>
118       * Print a designated text to a {@link PrintStream}, eventually wrapping the
119       * lines of text so as to ensure that the width of each line does not overflow
120       * {@link #MAX_LINE_LENGTH} columns. The line-wrapping is done with a
121       * {@link BreakIterator} using the designated {@link Locale}.
122       * <p>
123       * The text to print may contain <code>\n</code> characters. This method will
124       * force a line-break for each such character.
125       * 
126       * @param out the {@link PrintStream} destination of the formatted text.
127       * @param text the text to print.
128       * @param aLocale the {@link Locale} instance to use when constructing the
129       *          {@link BreakIterator}.
130       * @see Parser#MAX_LINE_LENGTH
131       */
132      protected static void formatText(PrintStream out, String text, Locale aLocale)
133      {
134        BreakIterator bit = BreakIterator.getLineInstance(aLocale);
135        String[] lines = text.split("\n"); //$NON-NLS-1$
136        for (int i = 0; i < lines.length; i++)
137          {
138            text = lines[i];
139            bit.setText(text);
140            int length = 0;
141            int finish;
142            int start = bit.first();
143            while ((finish = bit.next()) != BreakIterator.DONE)
144              {
145                String word = text.substring(start, finish);
146                length += word.length();
147                if (length >= MAX_LINE_LENGTH)
148                  {
149                    out.println();
150                    length = word.length();
151                  }
152                out.print(word);
153                start = finish;
154              }
155            out.println();
156          }
157      }
158    
159      /**
160       * Create a new parser. The program name is used when printing error messages.
161       * The version string is printed verbatim in response to "--version".
162       * 
163       * @param programName the name of the program
164       * @param versionString the program's version information
165       * @param longOnly true if the parser should work in long-option-only mode
166       */
167      public Parser(String programName, final String versionString, boolean longOnly)
168      {
169        this.programName = programName;
170        this.longOnly = longOnly;
171    
172        // Put standard options in their own section near the end.
173        finalGroup = new OptionGroup(Messages.getString("Parser.StdOptions")); //$NON-NLS-1$
174        finalGroup.add(new Option("help", Messages.getString("Parser.PrintHelp")) //$NON-NLS-1$ //$NON-NLS-2$
175        {
176          public void parsed(String argument) throws OptionException
177          {
178            printHelp(System.out);
179            System.exit(0);
180          }
181        });
182        finalGroup.add(new Option("version", Messages.getString("Parser.PrintVersion")) //$NON-NLS-1$ //$NON-NLS-2$
183        {
184          public void parsed(String argument) throws OptionException
185          {
186            System.out.println(versionString);
187            System.exit(0);
188          }
189        });
190        add(finalGroup);
191    
192        add(defaultGroup);
193      }
194    
195      /**
196       * Set the header text that is printed by --help.
197       * 
198       * @param headerText the header text
199       */
200      public void setHeader(String headerText)
201      {
202        this.headerText = headerText;
203      }
204    
205      /**
206       * Set the footer text that is printed by --help.
207       * 
208       * @param footerText the footer text
209       */
210      public void setFooter(String footerText)
211      {
212        this.footerText = footerText;
213      }
214    
215      /**
216       * Add an option to this parser. The option is added to the default option
217       * group; this affects where it is placed in the help output.
218       * 
219       * @param opt the option
220       */
221      public synchronized void add(Option opt)
222      {
223        defaultGroup.add(opt);
224      }
225    
226      /**
227       * This is like {@link #add(Option)}, but adds the option to the "final"
228       * group.  This should be used sparingly, if at all; it is intended for
229       * other very generic options like --help or --version.
230       * @param opt the option to add
231       */
232      protected synchronized void addFinal(Option opt)
233      {
234        finalGroup.add(opt);
235      }
236    
237      /**
238       * Add an option group to this parser. All the options in this group will be
239       * recognized by the parser.
240       * 
241       * @param group the option group
242       */
243      public synchronized void add(OptionGroup group)
244      {
245        // This ensures that the final group always appears at the end
246        // of the options.
247        if (optionGroups.isEmpty())
248          optionGroups.add(group);
249        else
250          optionGroups.add(optionGroups.size() - 1, group);
251      }
252    
253      // Make sure the 'options' field is properly initialized.
254      private void requireOptions()
255      {
256        if (options != null)
257          return;
258        options = new ArrayList();
259        Iterator it = optionGroups.iterator();
260        while (it.hasNext())
261          {
262            OptionGroup group = (OptionGroup) it.next();
263            options.addAll(group.options);
264          }
265      }
266    
267      public void printHelp()
268      {
269        this.printHelp(System.out);
270      }
271    
272      synchronized void printHelp(PrintStream out)
273      {
274        requireOptions();
275    
276        if (headerText != null)
277          {
278            formatText(out, headerText);
279            out.println();
280          }
281    
282        Iterator it = optionGroups.iterator();
283        while (it.hasNext())
284          {
285            OptionGroup group = (OptionGroup) it.next();
286            // An option group might be empty, in which case we don't
287            // want to print it..
288            if (! group.options.isEmpty())
289              {
290                group.printHelp(out, longOnly);
291                out.println();
292              }
293          }
294    
295        if (footerText != null)
296          formatText(out, footerText);
297      }
298    
299      /**
300       * This method can be overridden by subclassses to provide some option
301       * validation.  It is called by the parser after all options have been
302       * parsed.  If an option validation problem is encountered, this should
303       * throw an {@link OptionException} whose message should be shown to
304       * the user.
305       * <p>
306       * It is better to do validation here than after {@link #parse(String[])}
307       * returns, because the parser will print a message referring the
308       * user to the <code>--help</code> option.
309       * <p>
310       * The base implementation does nothing.
311       * 
312       * @throws OptionException the error encountered
313       */
314      protected void validate() throws OptionException
315      {
316        // Base implementation does nothing.
317      }
318    
319      private String getArgument(String request) throws OptionException
320      {
321        ++currentIndex;
322        if (currentIndex >= args.length)
323          {
324            String message
325              = MessageFormat.format(Messages.getString("Parser.ArgReqd"), //$NON-NLS-1$
326                                     new Object[] { request });
327            throw new OptionException(request);
328          }
329        return args[currentIndex];
330      }
331    
332      private void handleLongOption(String real, int index) throws OptionException
333      {
334        String option = real.substring(index);
335        String justName = option;
336        int eq = option.indexOf('=');
337        if (eq != -1)
338          justName = option.substring(0, eq);
339        boolean isPlainShort = justName.length() == 1;
340        char shortName = justName.charAt(0);
341        Option found = null;
342        for (int i = options.size() - 1; i >= 0; --i)
343          {
344            Option opt = (Option) options.get(i);
345            if (justName.equals(opt.getLongName()))
346              {
347                found = opt;
348                break;
349              }
350            if ((isPlainShort || opt.isJoined())
351                && opt.getShortName() == shortName)
352              {
353                if (! isPlainShort)
354                  {
355                    // The rest of the option string is the argument.
356                    eq = 0;
357                  }
358                found = opt;
359                break;
360              }
361          }
362        if (found == null)
363          {
364            String msg = MessageFormat.format(Messages.getString("Parser.Unrecognized"), //$NON-NLS-1$
365                                              new Object[] { real });
366            throw new OptionException(msg);
367          }
368        String argument = null;
369        if (found.getTakesArgument())
370          {
371            if (eq == -1)
372              argument = getArgument(real);
373            else
374              argument = option.substring(eq + 1);
375          }
376        else if (eq != - 1)
377          {
378            String msg
379              = MessageFormat.format(Messages.getString("Parser.NoArg"), //$NON-NLS-1$
380                                     new Object[] { real.substring(0, eq + index) });
381            throw new OptionException(msg);
382          }
383        found.parsed(argument);
384      }
385    
386      private void handleShortOptions(String option) throws OptionException
387      {
388        for (int charIndex = 1; charIndex < option.length(); ++charIndex)
389          {
390            char optChar = option.charAt(charIndex);
391            Option found = null;
392            for (int i = options.size() - 1; i >= 0; --i)
393              {
394                Option opt = (Option) options.get(i);
395                if (optChar == opt.getShortName())
396                  {
397                    found = opt;
398                    break;
399                  }
400              }
401            if (found == null)
402              {
403                String msg = MessageFormat.format(Messages.getString("Parser.UnrecDash"), //$NON-NLS-1$
404                                                  new Object[] { "" + optChar }); //$NON-NLS-1$
405                throw new OptionException(msg);
406              }
407            String argument = null;
408            if (found.getTakesArgument())
409              {
410                // If this is a joined short option, and there are more
411                // characters left in this argument, use those as the
412                // argument.
413                if (found.isJoined() && charIndex + 1 < option.length())
414                  {
415                    argument = option.substring(charIndex + 1);
416                    charIndex = option.length();
417                  }
418                else
419                  argument = getArgument("-" + optChar); //$NON-NLS-1$
420              }
421            found.parsed(argument);
422          }
423      }
424    
425      /**
426       * Parse a command line. Any files which are found will be passed to the file
427       * argument callback. This method will exit on error or when --help or
428       * --version is specified.
429       * 
430       * @param inArgs the command-line arguments
431       * @param files the file argument callback
432       */
433      public synchronized void parse(String[] inArgs, FileArgumentCallback files)
434      {
435        requireOptions();
436        try
437          {
438            args = inArgs;
439            for (currentIndex = 0; currentIndex < args.length; ++currentIndex)
440              {
441                if (args[currentIndex].length() == 0
442                    || args[currentIndex].charAt(0) != '-'
443                    || "-".equals(args[currentIndex])) //$NON-NLS-1$
444                  {
445                    files.notifyFile(args[currentIndex]);
446                    continue;
447                  }
448                if ("--".equals(args[currentIndex])) //$NON-NLS-1$
449                  break;
450                if (args[currentIndex].charAt(1) == '-')
451                  handleLongOption(args[currentIndex], 2);
452                else if (longOnly)
453                  handleLongOption(args[currentIndex], 1);
454                else
455                  handleShortOptions(args[currentIndex]);
456              }
457            // Add remaining arguments to leftovers.
458            for (++currentIndex; currentIndex < args.length; ++currentIndex)
459              files.notifyFile(args[currentIndex]);
460            // See if something went wrong.
461            validate();
462          }
463        catch (OptionException err)
464          {
465            System.err.println(programName + ": " + err.getMessage()); //$NON-NLS-1$
466            String fmt;
467            if (longOnly)
468              fmt = Messages.getString("Parser.TryHelpShort"); //$NON-NLS-1$
469            else
470              fmt = Messages.getString("Parser.TryHelpLong"); //$NON-NLS-1$
471            String msg = MessageFormat.format(fmt, new Object[] { programName });
472            System.err.println(programName + ": " + msg); //$NON-NLS-1$
473            System.exit(1);
474          }
475      }
476    
477      /**
478       * Parse a command line. Any files which are found will be returned. This
479       * method will exit on error or when --help or --version is specified.
480       * 
481       * @param inArgs the command-line arguments
482       */
483      public String[] parse(String[] inArgs)
484      {
485        final ArrayList fileResult = new ArrayList();
486        parse(inArgs, new FileArgumentCallback()
487        {
488          public void notifyFile(String fileArgument)
489          {
490            fileResult.add(fileArgument);
491          }
492        });
493        return (String[]) fileResult.toArray(new String[0]);
494      }
495    }