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