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

aspectran / aspectran / #3589

17 Jun 2024 11:37PM CUT coverage: 34.069% (-0.01%) from 34.081%
#3589

push

github

topframe
Bump jline.version from 3.26.1 to 3.26.2

Bumps `jline.version` from 3.26.1 to 3.26.2.

Updates `org.jline:jline-reader` from 3.26.1 to 3.26.2
- [Release notes](https://github.com/jline/jline3/releases)
- [Changelog](https://github.com/jline/jline3/blob/master/changelog.md)
- [Commits](https://github.com/jline/jline3/compare/jline-parent-3.26.1...jline-parent-3.26.2)

Updates `org.jline:jline-console` from 3.26.1 to 3.26.2
- [Release notes](https://github.com/jline/jline3/releases)
- [Changelog](https://github.com/jline/jline3/blob/master/changelog.md)
- [Commits](https://github.com/jline/jline3/compare/jline-parent-3.26.1...jline-parent-3.26.2)

Updates `org.jline:jline-terminal` from 3.26.1 to 3.26.2
- [Release notes](https://github.com/jline/jline3/releases)
- [Changelog](https://github.com/jline/jline3/blob/master/changelog.md)
- [Commits](https://github.com/jline/jline3/compare/jline-parent-3.26.1...jline-parent-3.26.2)

Updates `org.jline:jline-terminal-jni` from 3.26.1 to 3.26.2
- [Release notes](https://github.com/jline/jline3/releases)
- [Changelog](https://github.com/jline/jline3/blob/master/changelog.md)
- [Commits](https://github.com/jline/jline3/compare/jline-parent-3.26.1...jline-parent-3.26.2)

---
updated-dependencies:
- dependency-name: org.jline:jline-reader
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jline:jline-console
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jline:jline-terminal
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jline:jline-terminal-jni
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

13343 of 39165 relevant lines covered (34.07%)

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

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

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

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

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

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

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

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

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

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

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

161
        checkRequiredOptions();
1✔
162

163
        return this.parsedOptions;
1✔
164
    }
165

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

558
}
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