source: josm/trunk/src/org/openstreetmap/josm/tools/OptionParser.java@ 14640

Last change on this file since 14640 was 14640, checked in by simon04, 5 years ago

fix #17169 - Missing MessageFormat arguments in OptionParser

File size: 12.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.HashMap;
7import java.util.LinkedList;
8import java.util.List;
9import java.util.Map.Entry;
10import java.util.Objects;
11import java.util.function.Consumer;
12import java.util.stream.Collectors;
13
14/**
15 * A replacement of getopt.
16 * <p>
17 * Allows parsing command line options
18 *
19 * @author Michael Zangl
20 * @since 14415
21 */
22public class OptionParser {
23
24 private HashMap<String, AvailableOption> availableOptions = new HashMap<>();
25 private final String program;
26
27 /**
28 * Create a new option parser.
29 * @param program The program name.
30 */
31 public OptionParser(String program) {
32 Objects.requireNonNull(program, "program name must be provided");
33 this.program = program;
34 }
35
36 /**
37 * Adds an alias for the long option --optionName to the short version -name
38 * @param optionName The long option
39 * @param shortName The short version
40 * @return this {@link OptionParser}
41 */
42 public OptionParser addShortAlias(String optionName, String shortName) {
43 if (!shortName.matches("\\w")) {
44 throw new IllegalArgumentException("Short name '" + shortName + "' must be one character");
45 }
46 if (availableOptions.containsKey("-" + shortName)) {
47 throw new IllegalArgumentException("Short name '" + shortName + "' is already used");
48 }
49 AvailableOption longDefinition = availableOptions.get("--" + optionName);
50 if (longDefinition == null) {
51 throw new IllegalArgumentException("No long definition for " + optionName
52 + " was defined. Define the long definition first before creating " + "a short definition for it.");
53 }
54 availableOptions.put("-" + shortName, longDefinition);
55 return this;
56 }
57
58 /**
59 * Adds an option that may be used as a flag, e.g. --debug
60 * @param optionName The parameter name
61 * @param handler The handler that is called when the flag is encountered.
62 * @return this {@link OptionParser}
63 */
64 public OptionParser addFlagParameter(String optionName, Runnable handler) {
65 checkOptionName(optionName);
66 availableOptions.put("--" + optionName, parameter -> handler.run());
67 return this;
68 }
69
70 private void checkOptionName(String optionName) {
71 if (!optionName.matches("\\w([\\w-]*\\w)?")) {
72 throw new IllegalArgumentException("Illegal option name: '" + optionName + "'");
73 }
74 if (availableOptions.containsKey("--" + optionName)) {
75 throw new IllegalArgumentException("The option '--" + optionName + "' is already registered");
76 }
77 }
78
79 /**
80 * Add a parameter that expects a string attribute. E.g.: --config=/path/to/file
81 * @param optionName The name of the parameter.
82 * @param count The number of times the parameter may occur.
83 * @param handler A function that gets the current object and the parameter.
84 * It should throw an {@link OptionParseException} if the parameter cannot be handled / is invalid.
85 * @return this {@link OptionParser}
86 */
87 public OptionParser addArgumentParameter(String optionName, OptionCount count, Consumer<String> handler) {
88 checkOptionName(optionName);
89 availableOptions.put("--" + optionName, new AvailableOption() {
90 @Override
91 public boolean requiresParameter() {
92 return true;
93 }
94
95 @Override
96 public OptionCount getRequiredCount() {
97 return count;
98 }
99
100 @Override
101 public void runFor(String parameter) {
102 Objects.requireNonNull(parameter, "parameter");
103 handler.accept(parameter);
104 }
105 });
106 return this;
107 }
108
109 /**
110 * Same as {@link #parseOptions(List)}, but exits if option parsing fails.
111 * @param arguments The options
112 * @return The remaining program arguments that are no options.
113 */
114 public List<String> parseOptionsOrExit(List<String> arguments) {
115 try {
116 return parseOptions(arguments);
117 } catch (OptionParseException e) {
118 System.err.println(e.getLocalizedMessage());
119 System.exit(1);
120 // unreachable, but makes compilers happy
121 throw e;
122 }
123 }
124
125 /**
126 * Parses the options.
127 * <p>
128 * It first checks if all required options are present, if all options are known and validates the option count.
129 * <p>
130 * Then, all option handlers are called in the order in which the options are encountered.
131 * @param arguments Program arguments
132 * @return The remaining program arguments that are no options.
133 * @throws OptionParseException The error to display if option parsing failed.
134 */
135 public List<String> parseOptions(List<String> arguments) {
136 LinkedList<String> toHandle = new LinkedList<>(arguments);
137 List<String> remainingArguments = new LinkedList<>();
138 boolean argumentOnlyMode = false;
139 List<FoundOption> options = new LinkedList<>();
140
141 while (!toHandle.isEmpty()) {
142 String next = toHandle.removeFirst();
143 if (argumentOnlyMode || !next.matches("-.+")) {
144 // argument found, add it to arguments list
145 remainingArguments.add(next);
146 } else if ("--".equals(next)) {
147 // we are done, the remaining should be used as arguments.
148 argumentOnlyMode = true;
149 } else {
150 if (next.matches("-\\w\\w+")) {
151 // special case: concatenated short options like -hv
152 // We handle them as if the user wrote -h -v by just scheduling the remainder for the next loop.
153 toHandle.addFirst("-" + next.substring(2));
154 next = next.substring(0, 2);
155 }
156
157 String[] split = next.split("=", 2);
158 String optionName = split[0];
159 AvailableOption definition = findParameter(optionName);
160 String parameter = null;
161 if (definition.requiresParameter()) {
162 if (split.length > 1) {
163 parameter = split[1];
164 } else {
165 if (toHandle.isEmpty() || "--".equals(toHandle.getFirst())) {
166 throw new OptionParseException(tr("{0}: option ''{1}'' requires an argument", program, optionName));
167 }
168 parameter = toHandle.removeFirst();
169 }
170 } else if (split.length > 1) {
171 throw new OptionParseException(
172 tr("{0}: option ''{1}'' does not allow an argument", program, optionName));
173 }
174 options.add(new FoundOption(optionName, definition, parameter));
175 }
176 }
177
178 // Count how often they are used
179 availableOptions.values().stream().distinct().forEach(def -> {
180 long count = options.stream().filter(p -> def.equals(p.option)).count();
181 String optionName = availableOptions.entrySet().stream()
182 .filter(entry -> def.equals(entry.getValue()))
183 .map(Entry::getKey)
184 .findFirst()
185 .orElse("?");
186 if (count < def.getRequiredCount().min) {
187 // min may be 0 or 1 at the moment
188 throw new OptionParseException(tr("{0}: option ''{1}'' is required", program, optionName));
189 } else if (count > def.getRequiredCount().max) {
190 // max may be 1 or MAX_INT at the moment
191 throw new OptionParseException(tr("{0}: option ''{1}'' may not appear multiple times", program, optionName));
192 }
193 });
194
195 // Actually apply the parameters.
196 for (FoundOption option : options) {
197 try {
198 option.option.runFor(option.parameter);
199 } catch (OptionParseException e) {
200 StringBuilder message = new StringBuilder();
201 // Just add a nicer error message
202 if (option.parameter == null) {
203 message.append(tr("{0}: Error while handling option ''{1}''", program, option.optionName));
204 } else {
205 message.append(tr("{0}: Invalid value {2} for option ''{1}''", program, option.optionName,
206 option.parameter));
207 }
208 if (!e.getLocalizedMessage().isEmpty()) {
209 message.append(": ").append(e.getLocalizedMessage().isEmpty());
210 }
211 throw new OptionParseException(message.toString(), e);
212 }
213 }
214 return remainingArguments;
215 }
216
217 private AvailableOption findParameter(String optionName) {
218 AvailableOption exactMatch = availableOptions.get(optionName);
219 if (exactMatch != null) {
220 return exactMatch;
221 } else if (optionName.startsWith("--")) {
222 List<AvailableOption> alternatives = availableOptions.entrySet().stream()
223 .filter(entry -> entry.getKey().startsWith(optionName)).map(Entry::getValue).distinct()
224 .collect(Collectors.toList());
225
226 if (alternatives.size() == 1) {
227 return alternatives.get(0);
228 } else if (alternatives.size() > 1) {
229 throw new OptionParseException(tr("{0}: option ''{1}'' is ambiguous", program, optionName));
230 }
231 }
232 throw new OptionParseException(tr("{0}: unrecognized option ''{1}''", program, optionName));
233 }
234
235 /**
236 * How often an option may / must be specified on the command line.
237 * @author Michael Zangl
238 */
239 public enum OptionCount {
240 /**
241 * The option may be specified once
242 */
243 OPTIONAL(0, 1),
244 /**
245 * The option is required exactly once
246 */
247 REQUIRED(1, 1),
248 /**
249 * The option may be specified multiple times
250 */
251 MULTIPLE(0, Integer.MAX_VALUE);
252
253 private int min;
254 private int max;
255
256 OptionCount(int min, int max) {
257 this.min = min;
258 this.max = max;
259
260 }
261 }
262
263 protected interface AvailableOption {
264
265 /**
266 * Determines if this option requires a parameter.
267 * @return {@code true} if this option requires a parameter ({@code false} by default)
268 */
269 default boolean requiresParameter() {
270 return false;
271 }
272
273 /**
274 * Determines how often this option may / must be specified on the command line.
275 * @return how often this option may / must be specified on the command line
276 */
277 default OptionCount getRequiredCount() {
278 return OptionCount.OPTIONAL;
279 }
280
281 /**
282 * Called once if the parameter is encountered, afer basic validation.
283 * @param parameter The parameter if {@link #requiresParameter()} is true, <code>null</code> otherwise.
284 */
285 void runFor(String parameter);
286 }
287
288 private static class FoundOption {
289 private final String optionName;
290 private final AvailableOption option;
291 private final String parameter;
292
293 FoundOption(String optionName, AvailableOption option, String parameter) {
294 this.optionName = optionName;
295 this.option = option;
296 this.parameter = parameter;
297 }
298 }
299
300 /**
301 * Exception thrown when an option cannot be parsed.
302 * @author Michael Zangl
303 */
304 public static class OptionParseException extends RuntimeException {
305 // Don't rely on JAVA handling this correctly.
306 private final String localizedMessage;
307
308 /**
309 * Create an empty error with no description
310 */
311 public OptionParseException() {
312 super();
313 localizedMessage = "";
314 }
315
316 /**
317 * Create an error with a localized description
318 * @param localizedMessage The message to display to the user.
319 */
320 public OptionParseException(String localizedMessage) {
321 super(localizedMessage);
322 this.localizedMessage = localizedMessage;
323 }
324
325 /**
326 * Create an error with a localized description and a root cause
327 * @param localizedMessage The message to display to the user.
328 * @param t The error that caused this message to be displayed.
329 */
330 public OptionParseException(String localizedMessage, Throwable t) {
331 super(localizedMessage, t);
332 this.localizedMessage = localizedMessage;
333 }
334
335 @Override
336 public String getLocalizedMessage() {
337 return localizedMessage;
338 }
339 }
340}
Note: See TracBrowser for help on using the repository browser.