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 }