001 /*--------------------------------------------------------------------------+ 002 $Id: CommandLine.java 26283 2010-02-18 11:18:57Z juergens $ 003 | | 004 | Copyright 2005-2010 Technische Universitaet Muenchen | 005 | | 006 | Licensed under the Apache License, Version 2.0 (the "License"); | 007 | you may not use this file except in compliance with the License. | 008 | You may obtain a copy of the License at | 009 | | 010 | http://www.apache.org/licenses/LICENSE-2.0 | 011 | | 012 | Unless required by applicable law or agreed to in writing, software | 013 | distributed under the License is distributed on an "AS IS" BASIS, | 014 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 015 | See the License for the specific language governing permissions and | 016 | limitations under the License. | 017 +--------------------------------------------------------------------------*/ 018 package edu.tum.cs.commons.options; 019 020 import java.io.PrintWriter; 021 import java.util.ArrayList; 022 import java.util.Collections; 023 import java.util.List; 024 025 import edu.tum.cs.commons.reflect.TypeConversionException; 026 import edu.tum.cs.commons.string.StringUtils; 027 028 /** 029 * A class providing command line parsing and usage messages using GNU syntax. 030 * <p> 031 * The GNU syntax is implemented as follows. There are short (single character) 032 * and long (multi character) options, just as provided by the AOption 033 * annotation. Short options are introduced using a single minus (e.g. '-h') 034 * while long options are introduced using a double minus (e.g. '--help'). The 035 * parameter for an option is either the next argument, or--in case of long 036 * options--possibly separated by an equals sign (e.g. '--file=test.txt'). Short 037 * options may be chained (e.g. '-xvf abc' instead of '-x -v -f abc'). For 038 * chained short options, only the last option may take a parameter. 039 * 040 * @author Benjamin Hummel 041 * @author $Author: juergens $ 042 * 043 * @version $Rev: 26283 $ 044 * @levd.rating GREEN Hash: 11D10A0EF71752B4224881C9DF088659 045 */ 046 public class CommandLine { 047 048 /** Registry containing the options to be used by this instance */ 049 private final OptionRegistry registry; 050 051 /** 052 * Constructor. 053 * 054 * @param registry 055 * Registry containing the options to be used by this instance. 056 */ 057 public CommandLine(OptionRegistry registry) { 058 this.registry = registry; 059 } 060 061 /** 062 * Parses the given command line parameters and applies the options found. 063 * The arguments not treated as options or parameters are returned (often 064 * they are treated as file arguments). If the syntax does not conform to 065 * the options in the registry, an {@link OptionException} is thrown. 066 * 067 * @param args 068 * the command line arguments to be parsed. 069 * @return the remaining arguments. 070 * @throws OptionException 071 * in case of syntax errors or invalid parameters. 072 */ 073 public String[] parse(String[] args) throws OptionException { 074 return parse(new CommandLineTokenStream(args)); 075 } 076 077 /** 078 * Parses the command line parameters implicitly given by the token stream 079 * and applies the options found. The arguments not treated as options or 080 * parameters are returned (often they are treated as file arguments). If 081 * the syntax does not conform to the options in the registry an 082 * IllegalArgumentException is thrown. 083 * 084 * @param ts 085 * Token stream containing the arguments. 086 * @return Remaining arguments. 087 * @throws OptionException 088 * in case of syntax errors or invalid parameters. 089 */ 090 public String[] parse(CommandLineTokenStream ts) throws OptionException { 091 List<String> fileArgs = new ArrayList<String>(); 092 093 while (ts.hasNext()) { 094 if (ts.nextIsLongOption()) { 095 String name = ts.nextLongOption(); 096 OptionApplicator applicator = registry.getLongOption(name); 097 applyOption(applicator, formatLongOption(name), ts); 098 } else if (ts.nextIsShortOption()) { 099 char name = ts.nextShortOption(); 100 OptionApplicator applicator = registry.getShortOption(name); 101 applyOption(applicator, formatShortOption(name), ts); 102 } else if (ts.nextIsFileArgument()) { 103 fileArgs.add(ts.next()); 104 } else { 105 throw new OptionException("Unexpected command line argument: " 106 + ts.next()); 107 } 108 } 109 110 String[] result = new String[fileArgs.size()]; 111 return fileArgs.toArray(result); 112 } 113 114 /** 115 * Applies an option and tests for various errors. 116 * 117 * @param applicator 118 * the applicator for the option. 119 * @param optionName 120 * the name of the option. 121 * @param ts 122 * the token stream used to get additional parameters. 123 */ 124 private void applyOption(OptionApplicator applicator, String optionName, 125 CommandLineTokenStream ts) throws OptionException { 126 if (applicator == null) { 127 throw new OptionException("Unknown option: " + optionName); 128 } 129 if (applicator.requiresParameter()) { 130 if (!ts.nextIsParameter()) { 131 throw new OptionException("Missing argument for option: " 132 + optionName); 133 } 134 135 do { 136 String parameter = ts.next(); 137 try { 138 applicator.applyOption(parameter); 139 } catch (TypeConversionException e) { 140 throw new OptionException("Parameter " + parameter 141 + " for option " + optionName 142 + " is not of required type!"); 143 } 144 } while (applicator.isGreedy() && ts.hasNext() 145 && !(ts.nextIsLongOption() || ts.nextIsShortOption())); 146 } else { 147 applicator.applyOption(); 148 } 149 } 150 151 /** 152 * Print the list of all supported options using reasonable default values 153 * for widths. 154 * 155 * @param pw 156 * the writer used for output. 157 */ 158 public void printUsage(PrintWriter pw) { 159 printUsage(pw, 20, 80); 160 } 161 162 /** 163 * Print the list of all supported options. 164 * 165 * @param pw 166 * the writer to print to. 167 * @param firstCol 168 * the width of the first column containing the option name 169 * (without the trailing space). 170 * @param width 171 * the maximal width of a line (aka terminal width). 172 */ 173 public void printUsage(PrintWriter pw, int firstCol, int width) { 174 List<AOption> sortedOptions = new ArrayList<AOption>(registry 175 .getAllOptions()); 176 Collections.sort(sortedOptions, new AOptionComparator()); 177 178 for (AOption option : sortedOptions) { 179 printOption(option, pw, firstCol, width); 180 } 181 pw.flush(); 182 } 183 184 /** 185 * Print a single option. 186 * 187 * @param option 188 * the option to be printed. 189 * @param pw 190 * the writer to print to. 191 * @param firstCol 192 * the width of the first column containing the option name 193 * (without the trailing space). 194 * @param width 195 * the maximal width of a line (aka terminal width). 196 */ 197 private void printOption(AOption option, PrintWriter pw, int firstCol, 198 int width) { 199 String names = formatNames(option); 200 pw.print(names); 201 202 // start new line (if name too long for firstCol) or indent correctly 203 int pos = names.length(); 204 if (pos > firstCol) { 205 pos = width + 1; 206 } else { 207 pw.print(StringUtils.fillString(firstCol - pos, ' ')); 208 } 209 210 // Format description using lines no longer than width 211 String indent = StringUtils.fillString(firstCol, ' '); 212 String[] words = option.description().split("\\s+"); 213 for (String word : words) { 214 if (pos + 1 + word.length() > width) { 215 pw.println(); 216 pw.print(indent); 217 pos = firstCol; 218 } 219 pw.print(' '); 220 pw.print(word); 221 pos += 1 + word.length(); 222 } 223 pw.println(); 224 } 225 226 /** 227 * Format the names of an option for output. 228 * 229 * @param option 230 * the options to format. 231 * @return the formatted string. 232 */ 233 private String formatNames(AOption option) { 234 String names = " "; 235 if (option.shortName() == 0) { 236 names += StringUtils.fillString( 237 2 + formatShortOption('x').length(), ' '); 238 } else { 239 names += formatShortOption(option.shortName()); 240 if (option.longName().length() > 0) { 241 names += ", "; 242 } 243 } 244 if (option.longName().length() > 0) { 245 names += formatLongOption(option.longName()); 246 } 247 return names; 248 } 249 250 /** 251 * Returns the user visible name for the given long option. 252 * 253 * @param name 254 * the name of the option to format. 255 * @return the user visible name for the given long option. 256 */ 257 private String formatLongOption(String name) { 258 return "--" + name; 259 } 260 261 /** 262 * Returns the user visible name for the given short option. 263 * 264 * @param name 265 * the name of the option to format. 266 * @return the user visible name for the given short option. 267 */ 268 private String formatShortOption(char name) { 269 return "-" + name; 270 } 271 }