• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aspectran / aspectran / #4051

08 Feb 2025 09:08AM CUT coverage: 35.297% (-0.01%) from 35.31%
#4051

push

github

topframe
Update

14247 of 40363 relevant lines covered (35.3%)

0.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.43
/shell/src/main/java/com/aspectran/shell/command/option/DefaultOptionParser.java
1
/*
2
 * Copyright (c) 2008-2025 The Aspectran Project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package com.aspectran.shell.command.option;
17

18
import com.aspectran.utils.annotation.jsr305.NonNull;
19

20
import java.util.ArrayList;
21
import java.util.Enumeration;
22
import java.util.List;
23
import java.util.Properties;
24

25
/**
26
 * The default command option parser.
27
 */
28
public class DefaultOptionParser implements OptionParser {
29

30
    /** The parsed options instance. */
31
    private ParsedOptions parsedOptions;
32

33
    /** The current options. */
34
    private Options options;
35

36
    /**
37
     * Flag indicating how unrecognized tokens are handled. {@code true} to stop
38
     * the parsing and add the remaining tokens to the args list.
39
     * {@code false} to throw an exception.
40
     */
41
    private boolean skipParsingAtNonOption;
42

43
    /** The token currently processed. */
44
    private String currentToken;
45

46
    /** The last option parsed. */
47
    private Option currentOption;
48

49
    /** The required options and groups expected to be found when parsing the command line. */
50
    private List<Object> expectedOpts;
51

52
    /** Flag indicating if partial matching of long options is supported. */
53
    private final boolean allowPartialMatching;
54

55
    /**
56
     * Creates a new DefaultParser instance with partial matching enabled.
57
     * <p>By "partial matching" we mean that given the following code:
58
     * <pre>
59
     *     {@code
60
     *     Options options = new Options();
61
     *     options.addOption(new Option("d", "debug", false, "Turn on debug."));
62
     *     options.addOption(new Option("e", "extract", false, "Turn on extract."));
63
     *     options.addOption(new Option("o", "option", true, "Turn on option with argument."));
64
     *     }
65
     * </pre></p>
66
     * with "partial matching" turned on, {@code -de} only matches the
67
     * {@code "debug"} option. However, with "partial matching" disabled,
68
     * {@code -de} would enable both {@code debug} as well as
69
     * {@code extract} options.
70
     */
71
    public DefaultOptionParser() {
72
        this(false);
1✔
73
    }
1✔
74

75
    /**
76
     * Create a new DefaultParser instance with the specified partial matching policy.
77
     * <p>
78
     * By "partial matching" we mean that given the following code:
79
     * <pre>
80
     *     {@code
81
     *          Options options = new Options();
82
     *      options.addOption(new Option("d", "debug", false, "Turn on debug."));
83
     *      options.addOption(new Option("e", "extract", false, "Turn on extract."));
84
     *      options.addOption(new Option("o", "option", true, "Turn on option with argument."));
85
     *      }
86
     * </pre>
87
     * with "partial matching" turned on, {@code -de} only matches the
88
     * {@code "debug"} option. However, with "partial matching" disabled,
89
     * {@code -de} would enable both {@code debug} as well as
90
     * {@code extract} options.
91
     * @param allowPartialMatching if partial matching of long options shall be enabled
92
     */
93
    public DefaultOptionParser(boolean allowPartialMatching) {
1✔
94
        this.allowPartialMatching = allowPartialMatching;
1✔
95
    }
1✔
96

97
    public ParsedOptions parse(Options options, String[] args) throws OptionParserException {
98
        return parse(options, args, null);
1✔
99
    }
100

101
    /**
102
     * Parse the arguments according to the specified options and properties.
103
     * @param options the specified Options
104
     * @param args the command line arguments
105
     * @param properties command line option name-value pairs
106
     * @return the list of atomic option and value tokens
107
     * @throws OptionParserException if there are any problems encountered
108
     *      while parsing the command line tokens
109
     */
110
    public ParsedOptions parse(Options options, String[] args, Properties properties)
111
            throws OptionParserException {
112
        return parse(options, args, properties, false);
1✔
113
    }
114

115
    public ParsedOptions parse(Options options, String[] args, boolean skipParsingAtNonOption)
116
            throws OptionParserException {
117
        return parse(options, args, null, skipParsingAtNonOption);
1✔
118
    }
119

120
    /**
121
     * Parse the arguments according to the specified options and properties.
122
     * @param options the specified Options
123
     * @param args the command line arguments
124
     * @param properties command line option name-value pairs
125
     * @param skipParsingAtNonOption if {@code true} an unrecognized argument stops
126
     *     the parsing and the remaining arguments are added to the
127
     *     {@link ParsedOptions}s args list. If {@code false} an unrecognized
128
     *     argument triggers a ParseException.
129
     * @return the list of atomic option and value tokens
130
     * @throws OptionParserException if there are any problems encountered
131
     *      while parsing the command line tokens
132
     */
133
    public ParsedOptions parse(@NonNull Options options, String[] args, Properties properties,
134
                               boolean skipParsingAtNonOption)
135
            throws OptionParserException {
136
        this.options = options;
1✔
137
        this.skipParsingAtNonOption = skipParsingAtNonOption;
1✔
138
        this.currentOption = null;
1✔
139
        this.expectedOpts = new ArrayList<>(options.getRequiredOptions());
1✔
140

141
        // clear the data from the groups
142
        for (OptionGroup group : options.getOptionGroups()) {
1✔
143
            group.setSelected(null);
1✔
144
        }
1✔
145

146
        this.parsedOptions = new ParsedOptions();
1✔
147

148
        if (args != null) {
1✔
149
            for (String argument : args) {
1✔
150
                handleToken(argument);
1✔
151
            }
152
        }
153

154
        // check the arguments of the last option
155
        checkRequiredOptionValues();
1✔
156

157
        // add the default options
158
        handleProperties(properties);
1✔
159

160
        checkRequiredOptions();
1✔
161

162
        return this.parsedOptions;
1✔
163
    }
164

165
    /**
166
     * Sets the values of Options using the values in {@code properties}.
167
     * @param properties the value properties to be processed
168
     * @throws OptionParserException if option parsing fails
169
     */
170
    private void handleProperties(Properties properties) throws OptionParserException {
171
        if (properties == null) {
1✔
172
            return;
1✔
173
        }
174

175
        for (Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();) {
×
176
            String name = e.nextElement().toString();
×
177
            Option opt = options.getOption(name);
×
178
            if (opt == null) {
×
179
                throw new UnrecognizedOptionException("Default option wasn't defined", name);
×
180
            }
181

182
            // if the option is part of a group, check if another option of the group has been selected
183
            OptionGroup group = options.getOptionGroup(opt);
×
184
            boolean selected = (group != null && group.getSelected() != null);
×
185
            if (!parsedOptions.hasOption(name) && !selected) {
×
186
                // get the value from the properties
187
                String value = properties.getProperty(name);
×
188
                if (opt.hasValue()) {
×
189
                    if (opt.getValues() == null || opt.getValues().length == 0) {
×
190
                        opt.addValue(value);
×
191
                    }
192
                } else if (!("yes".equalsIgnoreCase(value)
×
193
                        || "true".equalsIgnoreCase(value)
×
194
                        || "1".equalsIgnoreCase(value))) {
×
195
                    // if the value is not yes, true or 1 then don't add the option to the ParsedOptions
196
                    continue;
×
197
                }
198
                handleOption(opt);
×
199
                currentOption = null;
×
200
            }
201
        }
×
202
    }
×
203

204
    /**
205
     * Handle any command line token.
206
     * @param token the command line token to handle
207
     * @throws OptionParserException if option parsing fails
208
     */
209
    private void handleToken(String token) throws OptionParserException {
210
        currentToken = token;
1✔
211
        if (!"--".equals(token)) {
1✔
212
            if (currentOption != null && currentOption.acceptsValue() &&
1✔
213
                    !currentOption.isWithEqualSign() && isArgument(token)) {
1✔
214
                String t = OptionUtils.stripLeadingAndTrailingQuotes(token);
1✔
215
                currentOption.addValue(t);
1✔
216
            } else if (token.startsWith("--")) {
1✔
217
                String t = OptionUtils.stripLeadingHyphens(token);
1✔
218
                handleLongOption(t);
1✔
219
            } else if (token.startsWith("-") && token.length() > 1) {
1✔
220
                String t = OptionUtils.stripLeadingHyphens(token);
1✔
221
                handleShortAndLongOption(t);
1✔
222
            } else {
1✔
223
                handleUnknownToken(token);
1✔
224
            }
225
        }
226
        if (currentOption != null && !currentOption.acceptsValue()) {
1✔
227
            currentOption = null;
×
228
        }
229
    }
1✔
230

231
    /**
232
     * Handles the following tokens:
233
     * <pre>
234
     * --L
235
     * --L=V
236
     * --L V
237
     * --l
238
     * </pre>
239
     * @param token the command line token to handle
240
     * @throws OptionParserException if option parsing fails
241
     */
242
    private void handleLongOption(@NonNull String token) throws OptionParserException {
243
        if (token.indexOf('=') == -1) {
1✔
244
            handleLongOptionWithoutEqual(token);
1✔
245
        } else {
246
            handleLongOptionWithEqual(token);
1✔
247
        }
248
    }
1✔
249

250
    /**
251
     * Handles the following tokens:
252
     * <pre>
253
     * --L
254
     * -L
255
     * --l
256
     * -l
257
     * </pre>
258
     * @param token the command line token to handle
259
     * @throws OptionParserException if option parsing fails
260
     */
261
    private void handleLongOptionWithoutEqual(String token) throws OptionParserException {
262
        List<String> matchingOpts = getMatchingLongOptions(token);
1✔
263
        if (matchingOpts.isEmpty()) {
1✔
264
            handleUnknownToken(currentToken);
×
265
        } else if (matchingOpts.size() > 1 && !options.hasLongOption(token)) {
1✔
266
            throw new AmbiguousOptionException(token, matchingOpts);
×
267
        } else {
268
            String key = (options.hasLongOption(token) ? token : matchingOpts.get(0));
1✔
269
            handleOption(options.getOption(key));
1✔
270
        }
271
    }
1✔
272

273
    /**
274
     * Handles the following tokens:
275
     * <pre>
276
     * --L=V
277
     * -L=V
278
     * --l=V
279
     * -l=V
280
     * </pre>
281
     * @param token the command line token to handle
282
     * @throws OptionParserException if option parsing fails
283
     */
284
    private void handleLongOptionWithEqual(@NonNull String token) throws OptionParserException {
285
        int pos = token.indexOf('=');
1✔
286
        String name = token.substring(0, pos);
1✔
287
        String value = token.substring(pos + 1);
1✔
288
        List<String> matchingOpts = getMatchingLongOptions(name);
1✔
289
        if (matchingOpts.isEmpty()) {
1✔
290
            handleUnknownToken(currentToken);
×
291
        } else if (matchingOpts.size() > 1 && !options.hasLongOption(name)) {
1✔
292
            throw new AmbiguousOptionException(name, matchingOpts);
×
293
        } else {
294
            String key = (options.hasLongOption(name) ? name : matchingOpts.get(0));
1✔
295
            Option option = options.getOption(key);
1✔
296
            if (option.acceptsValue()) {
1✔
297
                handleOption(option);
1✔
298
                currentOption.addValue(value);
1✔
299
                currentOption = null;
1✔
300
            } else {
301
                handleUnknownToken(currentToken);
×
302
            }
303
        }
304
    }
1✔
305

306
    /**
307
     * Handles the following tokens:
308
     * <pre>
309
     * -S
310
     * -SV
311
     * -S V
312
     * -S=V
313
     * -S1S2
314
     * -S1S2 V
315
     * -SV1=V2
316
     *
317
     * -L
318
     * -LV
319
     * -L V
320
     * -L=V
321
     * -l
322
     * </pre>
323
     * @param token the command line token to handle
324
     * @throws OptionParserException if option parsing fails
325
     */
326
    private void handleShortAndLongOption(@NonNull String token) throws OptionParserException {
327
        if (token.length() == 1) {
1✔
328
            // -S
329
            if (options.hasShortOption(token)) {
1✔
330
                handleOption(options.getOption(token));
1✔
331
            } else {
332
                handleUnknownToken(currentToken);
×
333
            }
334
            return;
1✔
335
        }
336
        int pos = token.indexOf('=');
1✔
337
        if (pos == -1) {
1✔
338
            // no equal sign found (-xxx)
339
            if (options.hasShortOption(token)) {
1✔
340
                handleOption(options.getOption(token));
1✔
341
            } else if (!getMatchingLongOptions(token).isEmpty()) {
1✔
342
                // -L or -l
343
                handleLongOptionWithoutEqual(token);
1✔
344
            } else {
345
                // look for a long prefix (-Xmx512m)
346
                String name = getLongPrefix(token);
×
347
                if (name != null) {
×
348
                    Option option = options.getOption(name);
×
349
                    if (!option.isWithEqualSign() && option.acceptsValue()) {
×
350
                        handleOption(options.getOption(name));
×
351
                        currentOption.addValue(token.substring(name.length()));
×
352
                        currentOption = null;
×
353
                        return;
×
354
                    }
355
                }
356
                handleUnknownToken(currentToken);
×
357
            }
×
358
        } else {
359
            // equal sign found (-xxx=yyy)
360
            String name = token.substring(0, pos);
1✔
361
            String value = token.substring(pos + 1);
1✔
362
            // -S=V
363
            Option option = options.getOption(name);
1✔
364
            if (option != null && option.acceptsValue()) {
1✔
365
                handleOption(option);
1✔
366
                currentOption.addValue(value);
1✔
367
                currentOption = null;
1✔
368
            } else {
369
                // -L=V or -l=V
370
                handleLongOptionWithEqual(token);
×
371
            }
372
        }
373
    }
1✔
374

375
    /**
376
     * Handles an unknown token. If the token starts with a dash an
377
     * UnrecognizedOptionException is thrown. Otherwise, the token is added
378
     * to the arguments of the command line. If the skipParsingAtNonOption flag
379
     * is set, this stops the parsing and the remaining tokens are added
380
     * as-is in the arguments of the command line.
381
     * @param token the command line token to handle
382
     * @throws OptionParserException if option parsing fails
383
     */
384
    private void handleUnknownToken(@NonNull String token) throws OptionParserException {
385
        if (token.startsWith("-") && token.length() > 1 && !skipParsingAtNonOption) {
1✔
386
            throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
×
387
        }
388
        parsedOptions.addArg(token);
1✔
389
    }
1✔
390

391
    private void handleOption(Option option) throws OptionParserException {
392
        // check the previous option before handling the next one
393
        checkRequiredOptionValues();
1✔
394
        try {
395
            option = option.clone();
1✔
396
        } catch (CloneNotSupportedException e) {
×
397
            throw new OptionParserException("A CloneNotSupportedException was thrown: " + e.getMessage() + "; " +
×
398
                    "Class " + option.getClass() + " must implement the Cloneable interface");
×
399
        }
1✔
400
        updateRequiredOptions(option);
1✔
401
        parsedOptions.addOption(option);
1✔
402
        if (option.hasValue()) {
1✔
403
            currentOption = option;
1✔
404
        } else {
405
            currentOption = null;
1✔
406
        }
407
    }
1✔
408

409
    /**
410
     * Removes the option or its group from the list of expected elements.
411
     */
412
    private void updateRequiredOptions(@NonNull Option option) throws AlreadySelectedException {
413
        if (option.isRequired()) {
1✔
414
            expectedOpts.remove(option.getKey());
1✔
415
        }
416
        // if the option is in an OptionGroup make that option the selected option of the group
417
        if (options.getOptionGroup(option) != null) {
1✔
418
            OptionGroup group = options.getOptionGroup(option);
1✔
419
            if (group.isRequired()) {
1✔
420
                expectedOpts.remove(group);
×
421
            }
422
            group.setSelected(option);
1✔
423
        }
424
    }
1✔
425

426
    /**
427
     * Throws a {@link MissingOptionException} if all required options are not present.
428
     * @throws MissingOptionException if any of the required Options are not present
429
     */
430
    private void checkRequiredOptions() throws MissingOptionException {
431
        // if there are required options that have not been processed
432
        if (!expectedOpts.isEmpty()) {
1✔
433
            throw new MissingOptionException(expectedOpts);
×
434
        }
435
    }
1✔
436

437
    /**
438
     * Throw a {@link MissingOptionValueException} if the current option
439
     * didn't receive the number of values expected.
440
     */
441
    private void checkRequiredOptionValues() throws OptionParserException {
442
        if (currentOption != null && currentOption.requiresValue()) {
1✔
443
            throw new MissingOptionValueException(currentOption);
×
444
        }
445
    }
1✔
446

447
    /**
448
     * Returns true is the token is a valid argument.
449
     * @param token the command line token to handle
450
     * @return true if the token is a valid argument
451
     */
452
    private boolean isArgument(String token) {
453
        return (!isOption(token) || isNegativeNumber(token));
1✔
454
    }
455

456
    /**
457
     * Check if the token is a negative number.
458
     * @param token the command line token to handle
459
     * @return true if the token is a negative number
460
     */
461
    private boolean isNegativeNumber(String token) {
462
        try {
463
            Double.parseDouble(token);
×
464
            return true;
×
465
        } catch (NumberFormatException e) {
1✔
466
            return false;
1✔
467
        }
468
    }
469

470
    /**
471
     * Tells if the token looks like an option.
472
     * @param token the command line token to handle
473
     * @return true if the token looks like an option
474
     */
475
    private boolean isOption(String token) {
476
        return (isLongOption(token) || isShortOption(token));
1✔
477
    }
478

479
    /**
480
     * Tells if the token looks like a short option.
481
     * @param token the command line token to handle
482
     * @return true if the token like a short option
483
     */
484
    private boolean isShortOption(@NonNull String token) {
485
        // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
486
        if (!token.startsWith("-") || token.length() == 1) {
1✔
487
            return false;
1✔
488
        }
489
        // remove leading "-" and "=value"
490
        int pos = token.indexOf("=");
1✔
491
        String name = (pos == -1 ? token.substring(1) : token.substring(1, pos));
1✔
492
        if (options.hasShortOption(name)) {
1✔
493
            return true;
1✔
494
        }
495
        // check for several concatenated short options
496
        return (!name.isEmpty() && options.hasShortOption(String.valueOf(name.charAt(0))));
×
497
    }
498

499
    /**
500
     * Tells if the token looks like a long option.
501
     * @param token the command line token to handle
502
     * @return true if the token like a long option
503
     */
504
    private boolean isLongOption(@NonNull String token) {
505
        if (!token.startsWith("-") || token.length() == 1) {
1✔
506
            return false;
1✔
507
        }
508
        int pos = token.indexOf("=");
1✔
509
        String t = (pos == -1 ? token : token.substring(0, pos));
1✔
510
        if (!getMatchingLongOptions(t).isEmpty()) {
1✔
511
            // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
512
            return true;
×
513
        }
514
        if (getLongPrefix(token) != null && !token.startsWith("--")) {
1✔
515
            // -LV
516
            return true;
×
517
        }
518
        return false;
1✔
519
    }
520

521
    /**
522
     * Returns a list of matching option strings for the given token, depending
523
     * on the selected partial matching policy.
524
     * @param token the token (may contain leading dashes)
525
     * @return the list of matching option strings or an empty list if no
526
     *      matching option could be found
527
     */
528
    private List<String> getMatchingLongOptions(String token) {
529
        if (allowPartialMatching) {
1✔
530
            return options.getMatchingOptions(token);
×
531
        } else {
532
            List<String> matches = new ArrayList<>(1);
1✔
533
            if (options.hasLongOption(token)) {
1✔
534
                Option option = options.getOption(token);
1✔
535
                matches.add(option.getLongName());
1✔
536
            }
537
            return matches;
1✔
538
        }
539
    }
540

541
    /**
542
     * Search for a prefix that is the long name of an option (-Xmx512m).
543
     * @param token the command line token to handle
544
     */
545
    private String getLongPrefix(@NonNull String token) {
546
        String name = null;
1✔
547
        for (int i = token.length() - 2; i > 1; i--) {
1✔
548
            String prefix = token.substring(0, i);
×
549
            if (options.hasLongOption(prefix)) {
×
550
                name = prefix;
×
551
                break;
×
552
            }
553
        }
554
        return name;
1✔
555
    }
556

557
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc