Rework command parser
- Previously CommandParser contained parser which was scannerless type of brute force parsing of command line args. - Contract in that old parser wasn't super clear what is its role with caller as it was given options and registration was parsed in a Shell class. - Add completely new parser package which has better model and which will be much easier to modify for future needs. - Change some interfaces around parsing so that we do as much in this new parsing model instead of pre-parsing something in a Shell class. - We also try to move away from using exceptions as a message delivery which had its own problems. Instead introducing parser messages which gives better info when errors are detected. - Add ParserConfig class which allows to expose settings to change some parser features. Later this will be exposed to user so that some features can be turned on/off for an actual shell needs. - Fixes #646
This commit is contained in:
@@ -229,8 +229,6 @@ public class Shell {
|
||||
this.exitCodeMappings.reset(mappingFunctions);
|
||||
}
|
||||
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
|
||||
Thread commandThread = Thread.currentThread();
|
||||
Object sh = Signals.register("INT", () -> commandThread.interrupt());
|
||||
|
||||
@@ -243,7 +241,7 @@ public class Shell {
|
||||
Object evaluate = null;
|
||||
Exception e = null;
|
||||
try {
|
||||
evaluate = execution.evaluate(commandRegistration.get(), wordsForArgs.toArray(new String[0]));
|
||||
evaluate = execution.evaluate(words.toArray(new String[0]));
|
||||
}
|
||||
catch (UndeclaredThrowableException ute) {
|
||||
if (ute.getCause() instanceof InterruptedException || ute.getCause() instanceof ClosedByInterruptException) {
|
||||
@@ -327,21 +325,6 @@ public class Shell {
|
||||
|| (input.words().iterator().next().matches("\\s*//.*"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of words to be considered for argument resolving. Drops the first N
|
||||
* words used for the command, as well as an optional empty word at the end of the list
|
||||
* (which may be present if user added spaces before submitting the buffer)
|
||||
*/
|
||||
private List<String> wordsForArguments(String command, List<String> words) {
|
||||
int wordsUsedForCommandKey = command.split(" ").length;
|
||||
List<String> args = words.subList(wordsUsedForCommandKey, words.size());
|
||||
int last = args.size() - 1;
|
||||
if (last >= 0 && "".equals(args.get(last))) {
|
||||
args.remove(last);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather completion proposals given some (incomplete) input the user has already typed
|
||||
* in. When and how this method is invoked is implementation specific and decided by the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,6 +38,7 @@ import org.springframework.shell.command.CommandRegistration.TargetInfo;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite;
|
||||
import org.springframework.shell.command.parser.ParserConfig;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -50,11 +51,10 @@ public interface CommandExecution {
|
||||
/**
|
||||
* Evaluate a command with a given arguments.
|
||||
*
|
||||
* @param registration the command registration
|
||||
* @param args the command args
|
||||
* @return evaluated execution
|
||||
*/
|
||||
Object evaluate(CommandRegistration registration, String[] args);
|
||||
Object evaluate(String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandExecution}.
|
||||
@@ -114,17 +114,17 @@ public interface CommandExecution {
|
||||
this.commandCatalog = commandCatalog;
|
||||
}
|
||||
|
||||
public Object evaluate(CommandRegistration registration, String[] args) {
|
||||
public Object evaluate(String[] args) {
|
||||
CommandParser parser = CommandParser.of(conversionService, commandCatalog.getRegistrations(), new ParserConfig());
|
||||
CommandParserResults results = parser.parse(args);
|
||||
CommandRegistration registration = results.registration();
|
||||
|
||||
// fast fail with availability before doing anything else
|
||||
Availability availability = registration.getAvailability();
|
||||
if (availability != null && !availability.isAvailable()) {
|
||||
return new CommandNotCurrentlyAvailable(registration.getCommand(), availability);
|
||||
}
|
||||
|
||||
List<CommandOption> options = registration.getOptions();
|
||||
CommandParser parser = CommandParser.of(conversionService);
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
|
||||
// check help options to short circuit
|
||||
boolean handleHelpOption = false;
|
||||
HelpOptionInfo helpOption = registration.getHelpOption();
|
||||
@@ -157,11 +157,11 @@ public interface CommandExecution {
|
||||
CommandRegistration usedRegistration;
|
||||
if (handleHelpOption) {
|
||||
String command = registration.getCommand();
|
||||
CommandParser helpParser = CommandParser.of(conversionService);
|
||||
CommandParser helpParser = CommandParser.of(conversionService, commandCatalog.getRegistrations(),
|
||||
new ParserConfig());
|
||||
CommandRegistration helpCommandRegistration = commandCatalog.getRegistrations()
|
||||
.get(registration.getHelpOption().getCommand());
|
||||
List<CommandOption> helpOptions = helpCommandRegistration.getOptions();
|
||||
CommandParserResults helpResults = helpParser.parse(helpOptions, new String[] { "--command", command });
|
||||
CommandParserResults helpResults = helpParser.parse(new String[] { "help", "--command", command });
|
||||
results = helpResults;
|
||||
usedRegistration = helpCommandRegistration;
|
||||
}
|
||||
|
||||
@@ -15,21 +15,19 @@
|
||||
*/
|
||||
package org.springframework.shell.command;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.shell.command.parser.Ast;
|
||||
import org.springframework.shell.command.parser.Ast.DefaultAst;
|
||||
import org.springframework.shell.command.parser.CommandModel;
|
||||
import org.springframework.shell.command.parser.Lexer.DefaultLexer;
|
||||
import org.springframework.shell.command.parser.Parser.DefaultParser;
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult;
|
||||
import org.springframework.shell.command.parser.ParserConfig;
|
||||
|
||||
/**
|
||||
* Interface parsing arguments for a {@link CommandRegistration}. A command is
|
||||
@@ -77,6 +75,13 @@ public interface CommandParser {
|
||||
*/
|
||||
interface CommandParserResults {
|
||||
|
||||
/**
|
||||
* Gets the registration.
|
||||
*
|
||||
* @return the registration
|
||||
*/
|
||||
CommandRegistration registration();
|
||||
|
||||
/**
|
||||
* Gets the results.
|
||||
*
|
||||
@@ -101,13 +106,15 @@ public interface CommandParser {
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandParserResults}.
|
||||
*
|
||||
* @param registration the registration
|
||||
* @param results the results
|
||||
* @param positional the list of positional arguments
|
||||
* @param errors the parsing errors
|
||||
* @return a new instance of results
|
||||
*/
|
||||
static CommandParserResults of(List<CommandParserResult> results, List<String> positional, List<CommandParserException> errors) {
|
||||
return new DefaultCommandParserResults(results, positional, errors);
|
||||
static CommandParserResults of(CommandRegistration registration, List<CommandParserResult> results,
|
||||
List<String> positional, List<CommandParserException> errors) {
|
||||
return new DefaultCommandParserResults(registration, results, positional, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,29 +124,22 @@ public interface CommandParser {
|
||||
* May throw various runtime exceptions depending how parser is configure.
|
||||
* For example if required option is missing an exception is thrown.
|
||||
*
|
||||
* @param options the command options
|
||||
* @param args the arguments
|
||||
* @return parsed results
|
||||
*/
|
||||
CommandParserResults parse(List<CommandOption> options, String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default command parser.
|
||||
*
|
||||
* @return instance of a default command parser
|
||||
*/
|
||||
static CommandParser of() {
|
||||
return of(null);
|
||||
}
|
||||
CommandParserResults parse(String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default command parser.
|
||||
*
|
||||
* @param conversionService the conversion service
|
||||
* @param registrations the command registrations
|
||||
* @param config the parser config
|
||||
* @return instance of a default command parser
|
||||
*/
|
||||
static CommandParser of(ConversionService conversionService) {
|
||||
return new DefaultCommandParser(conversionService);
|
||||
static CommandParser of(ConversionService conversionService, Map<String, CommandRegistration> registrations,
|
||||
ParserConfig config) {
|
||||
return new AstCommandParser(registrations, config, conversionService);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,16 +147,24 @@ public interface CommandParser {
|
||||
*/
|
||||
static class DefaultCommandParserResults implements CommandParserResults {
|
||||
|
||||
private CommandRegistration registration;
|
||||
private List<CommandParserResult> results;
|
||||
private List<String> positional;
|
||||
private List<CommandParserException> errors;
|
||||
|
||||
DefaultCommandParserResults(List<CommandParserResult> results, List<String> positional, List<CommandParserException> errors) {
|
||||
DefaultCommandParserResults(CommandRegistration registration, List<CommandParserResult> results,
|
||||
List<String> positional, List<CommandParserException> errors) {
|
||||
this.registration = registration;
|
||||
this.results = results;
|
||||
this.positional = positional;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandRegistration registration() {
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandParserResult> results() {
|
||||
return results;
|
||||
@@ -200,343 +208,45 @@ public interface CommandParser {
|
||||
/**
|
||||
* Default implementation of a {@link CommandParser}.
|
||||
*/
|
||||
static class DefaultCommandParser implements CommandParser {
|
||||
static class AstCommandParser implements CommandParser {
|
||||
|
||||
private final Map<String, CommandRegistration> registrations;
|
||||
private final ParserConfig configuration;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
DefaultCommandParser(ConversionService conversionService) {
|
||||
public AstCommandParser(Map<String, CommandRegistration> registrations, ParserConfig configuration,
|
||||
ConversionService conversionService) {
|
||||
this.registrations = registrations;
|
||||
this.configuration = configuration;
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandParserResults parse(List<CommandOption> options, String[] args) {
|
||||
List<CommandOption> requiredOptions = options.stream()
|
||||
.filter(o -> o.isRequired())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<String> splitValidValues = options.stream()
|
||||
.flatMap(o -> {
|
||||
Stream<String> longs = Stream.of(o.getLongNames()).map(l -> "--" + l);
|
||||
Stream<String> shorts = Stream.of(o.getShortNames()).map(s -> "-"+ Character.toString(s));
|
||||
return Stream.concat(longs, shorts);
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Lexer lexer = new Lexer(args, splitValidValues);
|
||||
List<List<String>> lexerResults = lexer.visit();
|
||||
Parser parser = new Parser();
|
||||
ParserResults parserResults = parser.visit(lexerResults, options);
|
||||
public CommandParserResults parse(String[] args) {
|
||||
CommandModel commandModel = new CommandModel(registrations, configuration);
|
||||
org.springframework.shell.command.parser.Lexer lexer = new DefaultLexer(commandModel, configuration);
|
||||
Ast ast = new DefaultAst();
|
||||
org.springframework.shell.command.parser.Parser parser = new DefaultParser(commandModel, lexer, ast,
|
||||
configuration, conversionService);
|
||||
ParseResult result = parser.parse(Arrays.asList(args));
|
||||
|
||||
List<CommandParserResult> results = new ArrayList<>();
|
||||
List<String> positional = new ArrayList<>();
|
||||
List<CommandParserException> errors = new ArrayList<>();
|
||||
parserResults.results.stream().forEach(pr -> {
|
||||
if (pr.option != null) {
|
||||
results.add(new DefaultCommandParserResult(pr.option, pr.value));
|
||||
requiredOptions.remove(pr.option);
|
||||
}
|
||||
else {
|
||||
for (String arg : pr.args) {
|
||||
if (arg.startsWith("--")) {
|
||||
errors.add(UnrecognisedOptionException.of(String.format("Unrecognised option '%s'", arg),
|
||||
arg));
|
||||
}
|
||||
else {
|
||||
positional.add(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pr.error != null) {
|
||||
errors.add(pr.error);
|
||||
}
|
||||
|
||||
result.optionResults().forEach(or -> {
|
||||
results.add(CommandParserResult.of(or.option(), or.value()));
|
||||
});
|
||||
|
||||
Deque<ParserResult> queue = new ArrayDeque<>(parserResults.results);
|
||||
options.stream()
|
||||
.filter(o -> o.getPosition() > -1)
|
||||
.sorted(Comparator.comparingInt(o -> o.getPosition()))
|
||||
.forEach(o -> {
|
||||
int arityMin = o.getArityMin();
|
||||
int arityMax = o.getArityMax();
|
||||
List<String> oargs = new ArrayList<>();
|
||||
if (arityMin > -1) {
|
||||
for (int i = 0; i < arityMax; i++) {
|
||||
ParserResult pop = null;
|
||||
if (!queue.isEmpty()) {
|
||||
pop = queue.pop();
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
if (pop != null && pop.option == null) {
|
||||
if (!pop.args.isEmpty()) {
|
||||
oargs.addAll(pop.args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// don't do anything if first arg looks like an option as if we are here
|
||||
// then we'd might remove wrong required option
|
||||
if (!oargs.isEmpty() && !oargs.get(0).startsWith("-")) {
|
||||
// as we now have a candicate option, try to see if there is a
|
||||
// conversion we can do and the use it.
|
||||
Object value = convertOptionType(o, oargs);
|
||||
results.add(new DefaultCommandParserResult(o, value));
|
||||
requiredOptions.remove(o);
|
||||
}
|
||||
});
|
||||
|
||||
requiredOptions.stream().forEach(o -> {
|
||||
String ln = o.getLongNames() != null ? Stream.of(o.getLongNames()).collect(Collectors.joining(",")) : "";
|
||||
String sn = o.getShortNames() != null ? Stream.of(o.getShortNames()).map(n -> Character.toString(n))
|
||||
.collect(Collectors.joining(",")) : "";
|
||||
errors.add(MissingOptionException
|
||||
.of(String.format("Missing option, longnames='%s', shortnames='%s'", ln, sn), o));
|
||||
result.messageResults().forEach(mr -> {
|
||||
errors.add(new CommandParserException(mr.getMessage()));
|
||||
});
|
||||
|
||||
return new DefaultCommandParserResults(results, positional, errors);
|
||||
}
|
||||
result.argumentResults().forEach(ar -> {
|
||||
positional.add(ar.value());
|
||||
});
|
||||
|
||||
private Object convertOptionType(CommandOption option, Object value) {
|
||||
if (conversionService != null && option.getType() != null && value != null) {
|
||||
if (conversionService.canConvert(value.getClass(), option.getType().getRawClass())) {
|
||||
value = conversionService.convert(value, option.getType().getRawClass());
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static class ParserResult {
|
||||
private CommandOption option;
|
||||
private List<String> args;
|
||||
private Object value;
|
||||
private CommandParserException error;
|
||||
|
||||
private ParserResult(CommandOption option, List<String> args, Object value, CommandParserException error) {
|
||||
this.option = option;
|
||||
this.args = args;
|
||||
this.value = value;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
static ParserResult of(CommandOption option, List<String> args, Object value,
|
||||
CommandParserException error) {
|
||||
return new ParserResult(option, args, value, error);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParserResults {
|
||||
private List<ParserResult> results;
|
||||
|
||||
private ParserResults(List<ParserResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
static ParserResults of(List<ParserResult> results) {
|
||||
return new ParserResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser works on a results from a lexer. It looks for given options
|
||||
* and builds parsing results.
|
||||
*/
|
||||
private class Parser {
|
||||
ParserResults visit(List<List<String>> lexerResults, List<CommandOption> options) {
|
||||
List<ParserResult> results = lexerResults.stream()
|
||||
.flatMap(lr -> {
|
||||
List<CommandOption> option = matchOptions(options, lr.get(0));
|
||||
if (option.isEmpty()) {
|
||||
return lr.stream().map(a -> ParserResult.of(null, Arrays.asList(a), null, null));
|
||||
}
|
||||
else {
|
||||
return option.stream().flatMap(o -> {
|
||||
List<String> subArgs = lr.subList(1, lr.size());
|
||||
ConvertArgumentsHolder holder = convertArguments(o, subArgs);
|
||||
if (holder.error != null) {
|
||||
return Stream.of(ParserResult.of(o, subArgs, null, holder.error));
|
||||
}
|
||||
Object value = convertOptionType(o, holder.value);
|
||||
Stream<ParserResult> unmapped = holder.unmapped.stream()
|
||||
.map(um -> ParserResult.of(null, Arrays.asList(um), null, null));
|
||||
Stream<ParserResult> res = Stream.of(ParserResult.of(o, subArgs, value, null));
|
||||
return Stream.concat(res, unmapped);
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Check options which didn't get matched and add parser result
|
||||
// for those having default value.
|
||||
List<CommandOption> defaultValueOptionsToCheck = new ArrayList<>(options);
|
||||
results.stream()
|
||||
.forEach(pr -> {
|
||||
if (pr.option != null) {
|
||||
defaultValueOptionsToCheck.remove(pr.option);
|
||||
}
|
||||
});
|
||||
defaultValueOptionsToCheck.stream()
|
||||
.filter(co -> co.getDefaultValue() != null)
|
||||
.forEach(co -> {
|
||||
Object value = co.getDefaultValue();
|
||||
value = convertOptionType(co, value);
|
||||
results.add(ParserResult.of(co, Collections.emptyList(), value, null));
|
||||
});
|
||||
return ParserResults.of(results);
|
||||
}
|
||||
|
||||
private List<CommandOption> matchOptions(List<CommandOption> options, String arg) {
|
||||
List<CommandOption> matched = new ArrayList<>();
|
||||
String trimmed = StringUtils.trimLeadingCharacter(arg, '-');
|
||||
int count = arg.length() - trimmed.length();
|
||||
if (count == 1) {
|
||||
if (trimmed.length() == 1) {
|
||||
Character trimmedChar = trimmed.charAt(0);
|
||||
options.stream()
|
||||
.filter(o -> {
|
||||
for (Character sn : o.getShortNames()) {
|
||||
if (trimmedChar.equals(sn)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(o -> matched.add(o));
|
||||
}
|
||||
else if (trimmed.length() > 1) {
|
||||
trimmed.chars().mapToObj(i -> (char)i)
|
||||
.forEach(c -> {
|
||||
options.stream().forEach(o -> {
|
||||
for (Character sn : o.getShortNames()) {
|
||||
if (c.equals(sn)) {
|
||||
matched.add(o);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (count == 2) {
|
||||
options.stream()
|
||||
.filter(o -> {
|
||||
for (String ln : o.getLongNames()) {
|
||||
if (trimmed.equals(ln)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(o -> matched.add(o));
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private ConvertArgumentsHolder convertArguments(CommandOption option, List<String> arguments) {
|
||||
Object value = null;
|
||||
List<String> unmapped = new ArrayList<>();
|
||||
|
||||
ResolvableType type = option.getType();
|
||||
int arityMin = option.getArityMin();
|
||||
int arityMax = option.getArityMax();
|
||||
|
||||
if (arityMin < 0 && type != null) {
|
||||
if (type.isAssignableFrom(boolean.class)) {
|
||||
arityMin = 1;
|
||||
arityMax = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (arityMax > 1 && arityMin > -1 && arityMax >= arityMin && (arguments.size() < arityMin || arguments.size() > arityMax)) {
|
||||
String ln = option.getLongNames() != null
|
||||
? Stream.of(option.getLongNames()).collect(Collectors.joining(","))
|
||||
: "";
|
||||
String sn = option.getShortNames() != null ? Stream.of(option.getShortNames())
|
||||
.map(n -> Character.toString(n)).collect(Collectors.joining(",")) : "";
|
||||
if (arguments.size() < arityMin) {
|
||||
String msg = String.format("Not enough arguments, longnames='%s', shortnames='%s'", ln, sn);
|
||||
return new ConvertArgumentsHolder(value, unmapped, new NotEnoughArgumentsOptionException(msg, option));
|
||||
}
|
||||
if (arguments.size() > arityMax) {
|
||||
String msg = String.format("Too many arguments, longnames='%s', shortnames='%s'", ln, sn);
|
||||
return new ConvertArgumentsHolder(value, unmapped, new TooManyArgumentsOptionException(msg, option));
|
||||
}
|
||||
}
|
||||
|
||||
if (type != null && type.isAssignableFrom(boolean.class)) {
|
||||
if (arguments.size() == 0) {
|
||||
value = true;
|
||||
}
|
||||
else {
|
||||
value = Boolean.parseBoolean(arguments.get(0));
|
||||
}
|
||||
}
|
||||
else if (type != null && type.isArray()) {
|
||||
value = arguments.stream().collect(Collectors.toList()).toArray();
|
||||
}
|
||||
// if it looks like type is a collection just get as list
|
||||
// as conversion will happen later. we just need to know
|
||||
// if user has Set, List, Collection, etc without worrying
|
||||
// about generics.
|
||||
else if (type != null && type.asCollection() != ResolvableType.NONE) {
|
||||
value = arguments.stream().collect(Collectors.toList());
|
||||
}
|
||||
else {
|
||||
if (!arguments.isEmpty()) {
|
||||
if (arguments.size() == 1) {
|
||||
value = arguments.get(0);
|
||||
}
|
||||
else {
|
||||
if (arityMax > 0) {
|
||||
int limit = Math.min(arguments.size(), arityMax);
|
||||
value = arguments.stream().limit(limit).collect(Collectors.toList());
|
||||
unmapped.addAll(arguments.subList(limit, arguments.size()));
|
||||
}
|
||||
else {
|
||||
value = arguments.get(0);
|
||||
unmapped.addAll(arguments.subList(1, arguments.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ConvertArgumentsHolder(value, unmapped);
|
||||
}
|
||||
|
||||
private class ConvertArgumentsHolder {
|
||||
Object value;
|
||||
final List<String> unmapped = new ArrayList<>();
|
||||
CommandParserException error;
|
||||
|
||||
ConvertArgumentsHolder(Object value, List<String> unmapped) {
|
||||
this(value, unmapped, null);
|
||||
}
|
||||
|
||||
ConvertArgumentsHolder(Object value, List<String> unmapped, CommandParserException error) {
|
||||
this.value = value;
|
||||
if (unmapped != null) {
|
||||
this.unmapped.addAll(unmapped);
|
||||
}
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexers only responsibility is to splice arguments array into
|
||||
* chunks which belongs together what comes for option structure.
|
||||
*/
|
||||
private static class Lexer {
|
||||
private final String[] args;
|
||||
private final Set<String> splitValidValues;
|
||||
Lexer(String[] args, Set<String> splitValues) {
|
||||
this.args = args;
|
||||
this.splitValidValues = splitValues;
|
||||
}
|
||||
List<List<String>> visit() {
|
||||
return Utils.split(args, t -> splitValidValues.contains(t));
|
||||
}
|
||||
return new DefaultCommandParserResults(result.commandRegistration(), results, positional, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,68 +264,4 @@ public interface CommandParser {
|
||||
return new CommandParserException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class OptionException extends CommandParserException {
|
||||
|
||||
private CommandOption option;
|
||||
|
||||
public OptionException(String message, CommandOption option) {
|
||||
super(message);
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
public CommandOption getOption() {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TooManyArgumentsOptionException extends OptionException {
|
||||
|
||||
public TooManyArgumentsOptionException(String message, CommandOption option) {
|
||||
super(message, option);
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotEnoughArgumentsOptionException extends OptionException {
|
||||
|
||||
public NotEnoughArgumentsOptionException(String message, CommandOption option) {
|
||||
super(message, option);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MissingOptionException extends CommandParserException {
|
||||
|
||||
private CommandOption option;
|
||||
|
||||
public MissingOptionException(String message, CommandOption option) {
|
||||
super(message);
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
public static MissingOptionException of(String message, CommandOption option) {
|
||||
return new MissingOptionException(message, option);
|
||||
}
|
||||
|
||||
public CommandOption getOption() {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnrecognisedOptionException extends CommandParserException {
|
||||
|
||||
private String option;
|
||||
|
||||
public UnrecognisedOptionException(String message, String option) {
|
||||
super(message);
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
public static UnrecognisedOptionException of(String message, String option) {
|
||||
return new UnrecognisedOptionException(message, option);
|
||||
}
|
||||
|
||||
public String getOption() {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,10 +20,6 @@ import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandParser.MissingOptionException;
|
||||
import org.springframework.shell.command.CommandParser.NotEnoughArgumentsOptionException;
|
||||
import org.springframework.shell.command.CommandParser.TooManyArgumentsOptionException;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Handles {@link CommandParserExceptionsException}.
|
||||
@@ -37,30 +33,7 @@ public class CommandParserExceptionResolver implements CommandExceptionResolver
|
||||
if (ex instanceof CommandParserExceptionsException cpee) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
cpee.getParserExceptions().stream().forEach(e -> {
|
||||
if (e instanceof MissingOptionException moe) {
|
||||
CommandOption option = moe.getOption();
|
||||
if (option.getLongNames().length > 0) {
|
||||
handleLong(builder, option);
|
||||
}
|
||||
else if (option.getShortNames().length > 0) {
|
||||
handleShort(builder, option);
|
||||
}
|
||||
}
|
||||
else if (e instanceof NotEnoughArgumentsOptionException neaoe) {
|
||||
CommandOption option = neaoe.getOption();
|
||||
if (option.getLongNames().length > 0) {
|
||||
handleLongNotEnough(builder, option);
|
||||
}
|
||||
}
|
||||
else if (e instanceof TooManyArgumentsOptionException tmaoe) {
|
||||
CommandOption option = tmaoe.getOption();
|
||||
if (option.getLongNames().length > 0) {
|
||||
handleLongTooMany(builder, option);
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.append(new AttributedString(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
builder.append(new AttributedString(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
builder.append("\n");
|
||||
});
|
||||
String as = builder.toAttributedString().toAnsi();
|
||||
@@ -68,54 +41,4 @@ public class CommandParserExceptionResolver implements CommandExceptionResolver
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void handleLong(AttributedStringBuilder builder, CommandOption option) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("Missing mandatory option --");
|
||||
buf.append(option.getLongNames()[0]);
|
||||
if (StringUtils.hasText(option.getDescription())) {
|
||||
buf.append(", ");
|
||||
buf.append(option.getDescription());
|
||||
}
|
||||
buf.append(".");
|
||||
builder.append(new AttributedString(buf.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
|
||||
private static void handleShort(AttributedStringBuilder builder, CommandOption option) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("Missing mandatory option -");
|
||||
buf.append(option.getShortNames()[0]);
|
||||
if (StringUtils.hasText(option.getDescription())) {
|
||||
buf.append(", ");
|
||||
buf.append(option.getDescription());
|
||||
}
|
||||
buf.append(".");
|
||||
builder.append(new AttributedString(buf.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
|
||||
private static void handleLongNotEnough(AttributedStringBuilder builder, CommandOption option) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("Not enough arguments --");
|
||||
buf.append(option.getLongNames()[0]);
|
||||
buf.append(" requires at least " + option.getArityMin());
|
||||
if (StringUtils.hasText(option.getDescription())) {
|
||||
buf.append(", ");
|
||||
buf.append(option.getDescription());
|
||||
}
|
||||
buf.append(".");
|
||||
builder.append(new AttributedString(buf.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
|
||||
private static void handleLongTooMany(AttributedStringBuilder builder, CommandOption option) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("Too many arguments --");
|
||||
buf.append(option.getLongNames()[0]);
|
||||
buf.append(" requires at most " + option.getArityMax());
|
||||
if (StringUtils.hasText(option.getDescription())) {
|
||||
buf.append(", ");
|
||||
buf.append(option.getDescription());
|
||||
}
|
||||
buf.append(".");
|
||||
builder.append(new AttributedString(buf.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +232,6 @@ class CommandRegistrationFactoryBean implements FactoryBean<CommandRegistration>
|
||||
else if (ClassUtils.isAssignable(Boolean.class, parameterType)) {
|
||||
optionSpec.arity(OptionArity.ZERO);
|
||||
}
|
||||
else {
|
||||
optionSpec.arity(OptionArity.EXACTLY_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(so.defaultValue())) {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult;
|
||||
|
||||
/**
|
||||
* Base abstract {@link NodeVisitor} which visits all nodes allowing user to
|
||||
* implement callback methods.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractNodeVisitor implements NodeVisitor {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(AbstractNodeVisitor.class);
|
||||
|
||||
@Override
|
||||
public final ParseResult visit(List<NonterminalAstNode> nonterminalNodes,
|
||||
List<TerminalAstNode> terminalNodes) {
|
||||
for (NonterminalAstNode ntn : nonterminalNodes) {
|
||||
log.debug("visit {}", ntn);
|
||||
if (ntn instanceof CommandNode node) {
|
||||
log.debug("onEnterRootCommandNode {}", node);
|
||||
onEnterRootCommandNode(node);
|
||||
visitChildren(node);
|
||||
log.debug("onExitRootCommandNode {}", node);
|
||||
onExitRootCommandNode(node);
|
||||
}
|
||||
}
|
||||
for (TerminalAstNode tn : terminalNodes) {
|
||||
if (tn instanceof DirectiveNode node) {
|
||||
enterDirectiveNode(node);
|
||||
exitDirectiveNode(node);
|
||||
}
|
||||
}
|
||||
return buildResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after all nodes has been visited to build results.
|
||||
*
|
||||
* @return the results from this visit operation
|
||||
*/
|
||||
protected abstract ParseResult buildResult();
|
||||
|
||||
/**
|
||||
* Called when {@link CommandNode} for root is entered. When node is fully
|
||||
* visited, {@link #onExitRootCommandNode(CommandNode)} is called.
|
||||
*
|
||||
* @param node the command node
|
||||
* @see #onExitRootCommandNode(CommandNode)
|
||||
*/
|
||||
protected abstract void onEnterRootCommandNode(CommandNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link CommandNode} for root is exited.
|
||||
*
|
||||
* @param node the command node
|
||||
* @see #onEnterRootCommandNode(CommandNode)
|
||||
*/
|
||||
protected abstract void onExitRootCommandNode(CommandNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link CommandNode} is entered. When node is fully visited,
|
||||
* {@link #onExitCommandNode(CommandNode)} is called.
|
||||
*
|
||||
* @param node the command node
|
||||
* @see #onExitCommandNode(CommandNode)
|
||||
*/
|
||||
protected abstract void onEnterCommandNode(CommandNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link CommandNode} is exited.
|
||||
*
|
||||
* @param node the command node
|
||||
* @see #onEnterCommandNode(CommandNode)
|
||||
*/
|
||||
protected abstract void onExitCommandNode(CommandNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link OptionNode} is entered. When node is fully visited,
|
||||
* {@link #onExitOptionNode(OptionNode)} is called.
|
||||
*
|
||||
* @param node the option node
|
||||
* @see #onExitOptionNode(OptionNode)
|
||||
*/
|
||||
protected abstract void onEnterOptionNode(OptionNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link OptionNode} is exited.
|
||||
*
|
||||
* @param node the option node
|
||||
* @see #onEnterOptionNode(OptionNode)
|
||||
*/
|
||||
protected abstract void onExitOptionNode(OptionNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link CommandArgumentNode} is entered. When node is fully visited,
|
||||
* {@link #onExitCommandArgumentNode(CommandArgumentNode)} is called.
|
||||
*
|
||||
* @param node the command argument node
|
||||
* @see #onExitCommandArgumentNode(CommandArgumentNode)
|
||||
*/
|
||||
protected abstract void onEnterCommandArgumentNode(CommandArgumentNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link CommandArgumentNode} is exited.
|
||||
*
|
||||
* @param node the command argument node
|
||||
* @see #onEnterCommandArgumentNode(CommandArgumentNode)
|
||||
*/
|
||||
protected abstract void onExitCommandArgumentNode(CommandArgumentNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link OptionArgumentNode} is entered. When node is fully visited,
|
||||
* {@link #onExitOptionArgumentNode(OptionArgumentNode)} is called.
|
||||
*
|
||||
* @param node the option argument node
|
||||
* @see #onExitOptionArgumentNode(OptionArgumentNode)
|
||||
*/
|
||||
protected abstract void onEnterOptionArgumentNode(OptionArgumentNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link OptionArgumentNode} is exited.
|
||||
*
|
||||
* @param node the command argument node
|
||||
* @see #onEnterOptionArgumentNode(OptionArgumentNode)
|
||||
*/
|
||||
protected abstract void onExitOptionArgumentNode(OptionArgumentNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link DirectiveNode} is entered. When node is fully visited,
|
||||
* {@link #onExitDirectiveNode(DirectiveNode)} is called.
|
||||
*
|
||||
* @param node the option node
|
||||
* @see #onExitDirectiveNode(DirectiveNode)
|
||||
*/
|
||||
protected abstract void onEnterDirectiveNode(DirectiveNode node);
|
||||
|
||||
/**
|
||||
* Called when {@link DirectiveNode} is exited.
|
||||
*
|
||||
* @param node the option node
|
||||
* @see #onEnterDirectiveNode(DirectiveNode)
|
||||
*/
|
||||
protected abstract void onExitDirectiveNode(DirectiveNode node);
|
||||
|
||||
private void visitChildren(NonterminalAstNode node) {
|
||||
log.debug("visitChildren {}", node);
|
||||
for (AstNode syntaxNode : node.getChildren()) {
|
||||
visitInternal(syntaxNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterCommandNode(CommandNode node) {
|
||||
log.debug("enterCommandNode {}", node);
|
||||
onEnterCommandNode(node);
|
||||
}
|
||||
|
||||
private void exitCommandNode(CommandNode node) {
|
||||
log.debug("exitCommandNode {}", node);
|
||||
onExitCommandNode(node);
|
||||
}
|
||||
|
||||
private void enterOptionNode(OptionNode node) {
|
||||
log.debug("enterOptionNode {}", node);
|
||||
onEnterOptionNode(node);
|
||||
}
|
||||
|
||||
private void exitOptionNode(OptionNode node) {
|
||||
log.debug("exitOptionNode {}", node);
|
||||
onExitOptionNode(node);
|
||||
}
|
||||
|
||||
private void enterCommandArgumentNode(CommandArgumentNode node) {
|
||||
log.debug("enterCommandArgumentNode {}", node);
|
||||
onEnterCommandArgumentNode(node);
|
||||
}
|
||||
|
||||
private void exitCommandArgumentNode(CommandArgumentNode node) {
|
||||
log.debug("exitCommandArgumentNode {}", node);
|
||||
onExitCommandArgumentNode(node);
|
||||
}
|
||||
|
||||
private void enterOptionArgumentNode(OptionArgumentNode node) {
|
||||
log.debug("enterOptionArgumentNode {}", node);
|
||||
onEnterOptionArgumentNode(node);
|
||||
}
|
||||
|
||||
private void exitOptionArgumentNode(OptionArgumentNode node) {
|
||||
log.debug("exitOptionArgumentNode {}", node);
|
||||
onExitOptionArgumentNode(node);
|
||||
}
|
||||
|
||||
private void enterDirectiveNode(DirectiveNode node) {
|
||||
log.debug("enterDirectiveNode {}", node);
|
||||
onEnterDirectiveNode(node);
|
||||
}
|
||||
|
||||
private void exitDirectiveNode(DirectiveNode node) {
|
||||
log.debug("exitDirectiveNode {}", node);
|
||||
onExitDirectiveNode(node);
|
||||
}
|
||||
|
||||
private void visitInternal(AstNode node) {
|
||||
log.debug("visitInternal {}", node);
|
||||
if (node instanceof CommandNode n) {
|
||||
enterCommandNode(n);
|
||||
visitChildren(n);
|
||||
exitCommandNode(n);
|
||||
}
|
||||
else if (node instanceof OptionNode n) {
|
||||
enterOptionNode(n);
|
||||
visitChildren(n);
|
||||
exitOptionNode(n);
|
||||
}
|
||||
else if (node instanceof CommandArgumentNode n) {
|
||||
enterCommandArgumentNode(n);
|
||||
exitCommandArgumentNode(n);
|
||||
}
|
||||
else if (node instanceof OptionArgumentNode n) {
|
||||
enterOptionArgumentNode(n);
|
||||
exitOptionArgumentNode(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface to generate abstract syntax tree from tokens. Generic language
|
||||
* parser usually contains lexing and parsing where this {@code Ast} represents
|
||||
* the latter parsing side.
|
||||
*
|
||||
* Parsing looks tokens and combines those together into nodes and we get
|
||||
* closer to understand commands, its options and arguments whether those
|
||||
* belong to command or option. Parser don't look if for example option
|
||||
* arguments makes sense which happen later when ast tree is visited.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Ast {
|
||||
|
||||
/**
|
||||
* Generate ast result from a tokens. {@link AstResult} contains info about
|
||||
* token to ast tree generation.
|
||||
*
|
||||
* @param tokens the tokens
|
||||
* @return a result containing further syntax info
|
||||
*/
|
||||
AstResult generate(List<Token> tokens);
|
||||
|
||||
/**
|
||||
* Representing result from tokens to ast tree generation.
|
||||
*
|
||||
* @param nonterminalNodes list of nonterminal nodes
|
||||
* @param terminalNodes list of terminal nodes
|
||||
*/
|
||||
public record AstResult(List<NonterminalAstNode> nonterminalNodes, List<TerminalAstNode> terminalNodes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of an {@link Ast}.
|
||||
*/
|
||||
public class DefaultAst implements Ast {
|
||||
|
||||
@Override
|
||||
public AstResult generate(List<Token> tokens) {
|
||||
List<CommandNode> commandNodes = new ArrayList<>();
|
||||
CommandNode commandNode = null;
|
||||
OptionNode optionNode = null;
|
||||
List<DirectiveNode> directiveNodes = new ArrayList<>();
|
||||
|
||||
for (Token token : tokens) {
|
||||
switch (token.getType()) {
|
||||
case DIRECTIVE:
|
||||
String raw = token.getValue();
|
||||
String[] split = raw.split(":", 2);
|
||||
directiveNodes.add(new DirectiveNode(token, split[0], split.length > 1 ? split[1] : null));
|
||||
break;
|
||||
case COMMAND:
|
||||
CommandNode n = new CommandNode(token, token.getValue());
|
||||
if (commandNode == null) {
|
||||
commandNode = n;
|
||||
}
|
||||
else {
|
||||
commandNode.addChildNode(n);
|
||||
commandNode = n;
|
||||
}
|
||||
commandNodes.add(commandNode);
|
||||
break;
|
||||
case OPTION:
|
||||
optionNode = new OptionNode(token, token.getValue());
|
||||
commandNode.addChildNode(optionNode);
|
||||
break;
|
||||
case ARGUMENT:
|
||||
if (optionNode != null) {
|
||||
OptionArgumentNode optionArgumentNode = new OptionArgumentNode(token, optionNode, token.getValue());
|
||||
optionNode.addChildNode(optionArgumentNode);
|
||||
}
|
||||
else {
|
||||
CommandArgumentNode commandArgumentNode = new CommandArgumentNode(token, commandNode);
|
||||
commandNode.addChildNode(commandArgumentNode);
|
||||
}
|
||||
break;
|
||||
case DOUBLEDASH:
|
||||
optionNode = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<NonterminalAstNode> nonterminalNodes = new ArrayList<>();
|
||||
List<TerminalAstNode> terminalNodes = new ArrayList<>();
|
||||
|
||||
if (commandNodes.size() > 0) {
|
||||
nonterminalNodes.add(commandNodes.get(0));
|
||||
}
|
||||
for (DirectiveNode dn : directiveNodes) {
|
||||
terminalNodes.add(dn);
|
||||
}
|
||||
|
||||
return new AstResult(nonterminalNodes, terminalNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Represents a node in an {@code abstract syntax tree} and knows about
|
||||
* {@link Token}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract sealed class AstNode permits NonterminalAstNode, TerminalAstNode {
|
||||
|
||||
private final Token token;
|
||||
|
||||
public AstNode(Token token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public Token getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Node representing a command argument.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class CommandArgumentNode extends TerminalAstNode {
|
||||
|
||||
private final CommandNode parent;
|
||||
|
||||
public CommandArgumentNode(Token token, CommandNode parent) {
|
||||
super(token);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public CommandNode getParentCommandNode() {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
/**
|
||||
* Helper class to make it easier to work with command registrations and how
|
||||
* those are used with parser model.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandModel {
|
||||
|
||||
private final Map<String, CommandInfo> rootCommands = new HashMap<>();
|
||||
private final ParserConfig configuration;
|
||||
|
||||
public CommandModel(Map<String, CommandRegistration> registrations, ParserConfig configuration) {
|
||||
this.configuration = configuration;
|
||||
buildModel(registrations);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
CommandInfo getRootCommand(String command) {
|
||||
return rootCommands.get(command);
|
||||
}
|
||||
|
||||
CommandInfo resolve(List<String> commands) {
|
||||
CommandInfo info = null;
|
||||
boolean onRoot = true;
|
||||
for (String commandx : commands) {
|
||||
String command = configuration.isEnabled(Feature.CASE_SENSITIVE_COMMANDS) ? commandx : commandx.toLowerCase();
|
||||
if (onRoot) {
|
||||
info = rootCommands.get(command);
|
||||
onRoot = false;
|
||||
}
|
||||
else {
|
||||
Optional<CommandInfo> nextInfo = info.getChildren().stream()
|
||||
.filter(i -> i.command.equals(command))
|
||||
.findFirst();
|
||||
if (nextInfo.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
info = nextInfo.get();
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
Map<String, CommandInfo> getRootCommands() {
|
||||
return rootCommands;
|
||||
}
|
||||
|
||||
Map<String, Token> getValidRootTokens() {
|
||||
Map<String, Token> tokens = new HashMap<>();
|
||||
|
||||
rootCommands.entrySet().forEach(e -> {
|
||||
tokens.put(e.getKey(), new Token(e.getKey(), TokenType.COMMAND));
|
||||
// Map<String, Token> validTokens = e.getValue().getValidTokens();
|
||||
// tokens.putAll(validTokens);
|
||||
});
|
||||
|
||||
// rootCommands.entrySet().forEach(e -> {
|
||||
// tokens.put(e.getKey(), new Token(e.getKey(), TokenType.COMMAND));
|
||||
// if (e.getValue().registration != null) {
|
||||
// e.getValue().registration.getAliases().forEach(alias -> {
|
||||
// String[] commands = alias.getCommand().split(" ");
|
||||
// tokens.put(commands[0], new Token(commands[0], TokenType.COMMAND));
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
return tokens;
|
||||
}
|
||||
|
||||
void xxx(String command, CommandRegistration registration) {
|
||||
String[] commands = command.split(" ");
|
||||
for (int i = 0; i < commands.length; i++) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// root1 sub1
|
||||
// root1 sub2
|
||||
|
||||
private CommandInfo getOrCreate(String[] commands, CommandRegistration registration) {
|
||||
CommandInfo ret = null;
|
||||
|
||||
CommandInfo parent = null;
|
||||
int i = -1;
|
||||
for (String command : commands) {
|
||||
i++;
|
||||
String key = configuration.isEnabled(Feature.CASE_SENSITIVE_COMMANDS) ? command : command.toLowerCase();
|
||||
|
||||
if (i == 0) {
|
||||
parent = rootCommands.get(command);
|
||||
if (parent == null) {
|
||||
parent = new CommandInfo(key, i < commands.length - 1 ? null : registration, parent);
|
||||
rootCommands.put(key, parent);
|
||||
}
|
||||
ret = parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
CommandInfo children = parent.getChildren(command);
|
||||
if (children == null) {
|
||||
children = new CommandInfo(key, i < commands.length - 1 ? null : registration, parent);
|
||||
parent.addChildred(command, children);
|
||||
}
|
||||
parent = children;
|
||||
ret = parent;
|
||||
// CommandInfo asdf = new CommandInfo(key, i < commands.length - 1 ? null : registration, parent);
|
||||
// parent.addChildred(command, asdf);
|
||||
// parent = asdf;
|
||||
|
||||
}
|
||||
|
||||
if (ret.registration == null) {
|
||||
ret.registration = registration;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void buildModel(Map<String, CommandRegistration> registrations) {
|
||||
registrations.entrySet().forEach(e -> {
|
||||
String[] commands = e.getKey().split(" ");
|
||||
getOrCreate(commands, e.getValue());
|
||||
|
||||
// String[] commands = e.getKey().split(" ");
|
||||
// CommandInfo parent = null;
|
||||
// for (int i = 0; i < commands.length; i++) {
|
||||
// CommandRegistration registration = i + 1 == commands.length ? e.getValue() : null;
|
||||
// String key = configuration.isCommandsCaseSensitive() ? commands[i] : commands[i].toLowerCase();
|
||||
// if (parent == null) {
|
||||
// CommandInfo info = new CommandInfo(commands[i], registration, parent);
|
||||
// rootCommands.computeIfAbsent(key, command -> info);
|
||||
// parent = info;
|
||||
// }
|
||||
// else {
|
||||
// CommandInfo info = new CommandInfo(key, registration, parent);
|
||||
// parent.children.add(info);
|
||||
// parent = info;
|
||||
// }
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains info about a command, its registration, its parent and childs.
|
||||
*/
|
||||
static class CommandInfo {
|
||||
String command;
|
||||
CommandRegistration registration;
|
||||
CommandInfo parent;
|
||||
// private List<CommandInfo> children = new ArrayList<>();
|
||||
private Map<String, CommandInfo> children = new HashMap<>();
|
||||
|
||||
CommandInfo(String command, CommandRegistration registration, CommandInfo parent) {
|
||||
this.registration = registration;
|
||||
this.parent = parent;
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid tokens for this {@code CommandInfo}.
|
||||
*
|
||||
* @return mapping from raw value to a token
|
||||
*/
|
||||
Map<String, Token> getValidTokens() {
|
||||
Map<String, Token> tokens = new HashMap<>();
|
||||
children.values().forEach(commandInfo -> {
|
||||
tokens.put(commandInfo.command, new Token(command, TokenType.COMMAND));
|
||||
});
|
||||
if (registration != null) {
|
||||
registration.getOptions().forEach(commandOption -> {
|
||||
for (String longName : commandOption.getLongNames()) {
|
||||
tokens.put("--" + longName, new Token(longName, TokenType.OPTION));
|
||||
}
|
||||
});
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
public Collection<CommandInfo> getChildren() {
|
||||
return children.values();
|
||||
}
|
||||
|
||||
public void addChildred(String command, CommandInfo children) {
|
||||
this.children.put(command, children);
|
||||
}
|
||||
|
||||
CommandInfo getChildren(String command) {
|
||||
return children.get(command);
|
||||
// return children.stream()
|
||||
// .filter(c -> c.command.equals(command))
|
||||
// .findFirst()
|
||||
// .orElse(null)
|
||||
// ;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Node representing a command.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class CommandNode extends NonterminalAstNode {
|
||||
|
||||
private final String command;
|
||||
|
||||
public CommandNode(Token token, String command) {
|
||||
super(token);
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CommandNode [command=" + command + ", children=" + getChildren() + ", token=" + getToken() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Node representing
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class DirectiveNode extends TerminalAstNode {
|
||||
|
||||
private final String name;
|
||||
private final String value;
|
||||
|
||||
public DirectiveNode(Token token, String name, String value) {
|
||||
super(token);
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Encapsulating {@code Directive} with its fields, {@code name} and
|
||||
* {@code value}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public record DirectiveResult(String name, @Nullable String value) {
|
||||
|
||||
public static DirectiveResult of(String name, @Nullable String value) {
|
||||
return new DirectiveResult(name, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.command.parser.CommandModel.CommandInfo;
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
/**
|
||||
* Interface to tokenize arguments into tokens. Generic language parser usually
|
||||
* contains lexing and parsing where this {@code Lexer} represents the former
|
||||
* lexing side.
|
||||
*
|
||||
* Lexing takes a first step to analyse basic construct of elements out from
|
||||
* given arguments. We get rough idea what each argument represents but don't
|
||||
* look deeper if any of it is correct which happens later when tokens go
|
||||
* through parsing operation.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Lexer {
|
||||
|
||||
/**
|
||||
* Tokenize given command line arguments into a list of tokens.
|
||||
*
|
||||
* @param arguments the command line arguments
|
||||
* @return lexer result having tokens and operation messages
|
||||
*/
|
||||
LexerResult tokenize(List<String> arguments);
|
||||
|
||||
/**
|
||||
* Representing result from {@link Lexer} tokenisation.
|
||||
*
|
||||
* @param tokens list of tokens in this result
|
||||
* @param messageResults list of error results in this result
|
||||
*/
|
||||
public record LexerResult(List<Token> tokens, List<MessageResult> messageResults) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link Lexer}.
|
||||
*/
|
||||
public class DefaultLexer implements Lexer {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(DefaultLexer.class);
|
||||
private final CommandModel commandModel;
|
||||
private final ParserConfig config;
|
||||
|
||||
public DefaultLexer(CommandModel commandModel, ParserConfig config) {
|
||||
this.commandModel = commandModel;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private record ArgumentsSplit(List<String> before, List<String> after) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits arguments from a point first valid command is found, where
|
||||
* {@code before} is everything before commands and {@code after} what's
|
||||
* remaining.
|
||||
*/
|
||||
private ArgumentsSplit splitArguments(List<String> arguments, Map<String, Token> validTokens) {
|
||||
int i = -1;
|
||||
boolean foundSplit = false;
|
||||
for (String argument : arguments) {
|
||||
if (!config.isEnabled(Feature.CASE_SENSITIVE_COMMANDS)) {
|
||||
argument = argument.toLowerCase();
|
||||
}
|
||||
i++;
|
||||
if (validTokens.containsKey(argument)) {
|
||||
foundSplit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i < 0) {
|
||||
return new ArgumentsSplit(Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
else if (i == 0) {
|
||||
if (foundSplit) {
|
||||
return new ArgumentsSplit(Collections.emptyList(), arguments);
|
||||
}
|
||||
return new ArgumentsSplit(arguments, Collections.emptyList());
|
||||
}
|
||||
return new ArgumentsSplit(arguments.subList(0, i), arguments.subList(i, arguments.size()));
|
||||
}
|
||||
|
||||
private List<String> extractDirectives(List<String> arguments) {
|
||||
List<String> ret = new ArrayList<>();
|
||||
Pattern pattern = Pattern.compile("\\[(.*?)\\]");
|
||||
String raw = arguments.stream().collect(Collectors.joining());
|
||||
Matcher matcher = pattern.matcher(raw);
|
||||
while (matcher.find()) {
|
||||
String group = matcher.group(1);
|
||||
ret.add(group);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LexerResult tokenize(List<String> arguments) {
|
||||
log.debug("Tokenizing arguments {}", arguments);
|
||||
List<MessageResult> errorResults = new ArrayList<>();
|
||||
List<Token> tokenList = new ArrayList<Token>();
|
||||
|
||||
preValidate(errorResults, arguments);
|
||||
|
||||
// starting from root level
|
||||
Map<String, Token> validTokens = commandModel.getValidRootTokens();
|
||||
|
||||
// we process arguments in two steps, ones before commands and commands
|
||||
ArgumentsSplit split = splitArguments(arguments, validTokens);
|
||||
|
||||
// consume everything before command section starts
|
||||
// currently there can only be directives and we need
|
||||
// to differentiate if we silenty ignore those vs.
|
||||
// whether directive support is enabled or not
|
||||
List<String> beforeArguments = split.before();
|
||||
|
||||
int i1 = split.before().size() - 1;
|
||||
|
||||
if (config.isEnabled(Feature.ALLOW_DIRECTIVES)) {
|
||||
List<String> rawDirectives = extractDirectives(beforeArguments);
|
||||
for (String raw : rawDirectives) {
|
||||
tokenList.add(Token.of(raw, TokenType.DIRECTIVE, 0));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!config.isEnabled(Feature.IGNORE_DIRECTIVES) && beforeArguments.size() > 0) {
|
||||
errorResults.add(MessageResult.of(ParserMessage.ILLEGAL_CONTENT_BEFORE_COMMANDS, 0, beforeArguments));
|
||||
}
|
||||
}
|
||||
|
||||
// consume remaining arguments which should contain
|
||||
// only ones starting from a first command
|
||||
boolean foundDoubleDash = false;
|
||||
List<String> afterArguments = split.after();
|
||||
CommandInfo currentCommand = null;
|
||||
|
||||
int i2 = i1;
|
||||
for (String argument : afterArguments) {
|
||||
// if (!configuration.isEnabled(Feature.CASE_SENSITIVE_COMMANDS)) {
|
||||
// argument = argument.toLowerCase();
|
||||
// }
|
||||
i2++;
|
||||
|
||||
// We've found bash style "--" meaning further option processing is
|
||||
// stopped and remaining arguments are simply command arguments
|
||||
if (foundDoubleDash) {
|
||||
tokenList.add(Token.of(argument, TokenType.ARGUMENT, i2));
|
||||
continue;
|
||||
}
|
||||
if (!foundDoubleDash && "--".equals(argument)) {
|
||||
tokenList.add(Token.of(argument, TokenType.DOUBLEDASH, i2));
|
||||
foundDoubleDash = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
String argumentToCheck = argument;
|
||||
if (!config.isEnabled(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
|| !config.isEnabled(Feature.CASE_SENSITIVE_OPTIONS)) {
|
||||
argumentToCheck = argument.toLowerCase();
|
||||
}
|
||||
|
||||
if (validTokens.containsKey(argumentToCheck)) {
|
||||
Token token = validTokens.get(argumentToCheck);
|
||||
switch (token.getType()) {
|
||||
case COMMAND:
|
||||
currentCommand = currentCommand == null ? commandModel.getRootCommands().get(argumentToCheck)
|
||||
: currentCommand.getChildren(argument);
|
||||
tokenList.add(Token.of(argument, TokenType.COMMAND, i2));
|
||||
validTokens = currentCommand.getValidTokens();
|
||||
break;
|
||||
case OPTION:
|
||||
tokenList.add(Token.of(argument, TokenType.OPTION, i2));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (isLastTokenOfType(tokenList, TokenType.OPTION)) {
|
||||
if (argument.startsWith("-")) {
|
||||
tokenList.add(Token.of(argument, TokenType.OPTION, i2));
|
||||
}
|
||||
else {
|
||||
tokenList.add(Token.of(argument, TokenType.ARGUMENT, i2));
|
||||
}
|
||||
}
|
||||
else if (isLastTokenOfType(tokenList, TokenType.COMMAND)) {
|
||||
if (argument.startsWith("-")) {
|
||||
tokenList.add(Token.of(argument, TokenType.OPTION, i2));
|
||||
}
|
||||
else {
|
||||
tokenList.add(Token.of(argument, TokenType.ARGUMENT, i2));
|
||||
}
|
||||
}
|
||||
else if (isLastTokenOfType(tokenList, TokenType.ARGUMENT)) {
|
||||
tokenList.add(Token.of(argument, TokenType.ARGUMENT, i2));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.debug("Generated token list {}", tokenList);
|
||||
return new LexerResult(tokenList, errorResults);
|
||||
}
|
||||
|
||||
private void preValidate(List<MessageResult> errorResults, List<String> arguments) {
|
||||
if (arguments.size() > 0) {
|
||||
String arg = arguments.get(0);
|
||||
if ("--".equals(arg)) {
|
||||
errorResults.add(MessageResult.of(ParserMessage.ILLEGAL_CONTENT_BEFORE_COMMANDS, 0, arg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLastTokenOfType(List<Token> tokenList, TokenType type) {
|
||||
if (tokenList.size() > 0) {
|
||||
if (tokenList.get(tokenList.size() - 1).getType() == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Encapsulating {@link ParserMessage} with its postion and {@code inserts}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public record MessageResult(ParserMessage parserMessage, int position, Object[] inserts) {
|
||||
|
||||
public static MessageResult of(ParserMessage parserMessage, int position, Object... inserts) {
|
||||
return new MessageResult(parserMessage, position, inserts);
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return parserMessage.formatMessage(position, inserts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult;
|
||||
|
||||
/**
|
||||
* Interface to vising nodes.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface NodeVisitor {
|
||||
|
||||
/**
|
||||
* Visit lists of non terminal and terminal nodes.
|
||||
*
|
||||
* @param nonterminalNodes non terminal nodes
|
||||
* @param terminalNodes terminal nodes
|
||||
* @return parser result
|
||||
*/
|
||||
ParseResult visit(List<NonterminalAstNode> nonterminalNodes, List<TerminalAstNode> terminalNodes);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@code Nonterminal} node means that it can have children and doesn't
|
||||
* necessarily terminal {@code ast tree branch}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract sealed class NonterminalAstNode extends AstNode permits CommandNode, OptionNode {
|
||||
|
||||
private final List<AstNode> children = new ArrayList<>();
|
||||
|
||||
public NonterminalAstNode(Token token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
public List<AstNode> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
void addChildNode(AstNode node) {
|
||||
this.children.add(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Node representing an option argument.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class OptionArgumentNode extends TerminalAstNode {
|
||||
|
||||
private final OptionNode parentOptionNode;
|
||||
private final String value;
|
||||
|
||||
public OptionArgumentNode(Token token, OptionNode parentOptionNode, String value) {
|
||||
super(token);
|
||||
this.parentOptionNode = parentOptionNode;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public OptionNode getParentOptionNode() {
|
||||
return parentOptionNode;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* Node representing an option.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class OptionNode extends NonterminalAstNode {
|
||||
|
||||
private final String name;
|
||||
|
||||
public OptionNode(Token token, String name) {
|
||||
super(token);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OptionNode [name=" + name + ", children=" + getChildren() + ", token=" + getToken() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.command.CommandOption;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.parser.Ast.AstResult;
|
||||
import org.springframework.shell.command.parser.CommandModel.CommandInfo;
|
||||
import org.springframework.shell.command.parser.Lexer.LexerResult;
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult.ArgumentResult;
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult.OptionResult;
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
/**
|
||||
* Interface to parse command line arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Parser {
|
||||
|
||||
/**
|
||||
* Parse given arguments into a {@link ParseResult}.
|
||||
*
|
||||
* @param arguments the command line arguments
|
||||
* @return a parsed results
|
||||
*/
|
||||
ParseResult parse(List<String> arguments);
|
||||
|
||||
|
||||
/**
|
||||
* Results from a {@link Parser} containing needed information like resolved
|
||||
* {@link CommandRegistration}, list of {@link CommandOption} instances, errors
|
||||
* and directive.
|
||||
*
|
||||
* @param commandRegistration command registration
|
||||
* @param optionResults option results
|
||||
* @param argumentResults argument results
|
||||
* @param messageResults message results
|
||||
* @param directiveResults directive result
|
||||
*/
|
||||
public record ParseResult(CommandRegistration commandRegistration, List<OptionResult> optionResults,
|
||||
List<ArgumentResult> argumentResults, List<MessageResult> messageResults,
|
||||
List<DirectiveResult> directiveResults) {
|
||||
|
||||
public record OptionResult(CommandOption option, Object value) {
|
||||
|
||||
public static OptionResult of(CommandOption option, Object value) {
|
||||
return new OptionResult(option, value);
|
||||
}
|
||||
}
|
||||
|
||||
public record ArgumentResult(String value, int position) {
|
||||
|
||||
public static ArgumentResult of(String value, int position) {
|
||||
return new ArgumentResult(value, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link Parser}. Uses {@link Lexer} and
|
||||
* {@link Ast}.
|
||||
*/
|
||||
public class DefaultParser implements Parser {
|
||||
|
||||
private final ParserConfig config;
|
||||
private final CommandModel commandModel;
|
||||
private final Lexer lexer;
|
||||
private final Ast ast;
|
||||
private ConversionService conversionService;
|
||||
|
||||
public DefaultParser(CommandModel commandModel, Lexer lexer, Ast ast) {
|
||||
this(commandModel, lexer, ast, new ParserConfig());
|
||||
}
|
||||
|
||||
public DefaultParser(CommandModel commandModel, Lexer lexer, Ast ast, ParserConfig config) {
|
||||
this(commandModel, lexer, ast, config, null);
|
||||
}
|
||||
|
||||
public DefaultParser(CommandModel commandModel, Lexer lexer, Ast ast, ParserConfig config,
|
||||
ConversionService conversionService) {
|
||||
this.commandModel = commandModel;
|
||||
this.lexer = lexer;
|
||||
this.ast = ast;
|
||||
this.config = config;
|
||||
this.conversionService = conversionService != null ? conversionService : new DefaultConversionService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseResult parse(List<String> arguments) {
|
||||
// 1. tokenize arguments
|
||||
LexerResult lexerResult = lexer.tokenize(arguments);
|
||||
List<Token> tokens = lexerResult.tokens();
|
||||
|
||||
// 2. generate syntax tree results from tokens
|
||||
// result from it is then feed into node visitor
|
||||
AstResult astResult = ast.generate(tokens);
|
||||
|
||||
// 3. visit nodes
|
||||
// whoever uses this parser can then do further
|
||||
// things with final parsing results
|
||||
NodeVisitor visitor = new DefaultNodeVisitor(commandModel, conversionService, config);
|
||||
ParseResult parseResult = visitor.visit(astResult.nonterminalNodes(), astResult.terminalNodes());
|
||||
parseResult.messageResults().addAll(lexerResult.messageResults());
|
||||
return parseResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link NodeVisitor}.
|
||||
*/
|
||||
class DefaultNodeVisitor extends AbstractNodeVisitor {
|
||||
|
||||
private final CommandModel commandModel;
|
||||
private final ConversionService conversionService;
|
||||
private final ParserConfig config;
|
||||
private final List<MessageResult> commonMessageResults = new ArrayList<>();
|
||||
private List<String> resolvedCommmand = new ArrayList<>();
|
||||
private List<OptionResult> optionResults = new ArrayList<>();
|
||||
private List<String> currentOptionArgument = new ArrayList<>();
|
||||
private List<DirectiveResult> directiveResults = new ArrayList<>();
|
||||
private List<OptionNode> invalidOptionNodes = new ArrayList<>();
|
||||
private List<ArgumentResult> argumentResults = new ArrayList<>();
|
||||
private int commandArgumentPos = 0;
|
||||
|
||||
DefaultNodeVisitor(CommandModel commandModel, ConversionService conversionService, ParserConfig config) {
|
||||
this.commandModel = commandModel;
|
||||
this.conversionService = conversionService;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ParseResult buildResult() {
|
||||
CommandInfo info = commandModel.resolve(resolvedCommmand);
|
||||
CommandRegistration registration = info != null ? info.registration : null;
|
||||
|
||||
List<MessageResult> messageResults = new ArrayList<>();
|
||||
if (registration != null) {
|
||||
messageResults.addAll(commonMessageResults);
|
||||
messageResults.addAll(validateOptionNotMissing(registration));
|
||||
messageResults.addAll(validateOptionIsValid(registration));
|
||||
|
||||
|
||||
// add options with default values
|
||||
Set<CommandOption> resolvedOptions1 = optionResults.stream()
|
||||
.map(or -> or.option())
|
||||
.collect(Collectors.toSet());
|
||||
registration.getOptions().stream()
|
||||
.filter(o -> o.getDefaultValue() != null)
|
||||
.filter(o -> !resolvedOptions1.contains(o))
|
||||
.forEach(o -> {
|
||||
resolvedOptions1.add(o);
|
||||
Object value = convertOptionType(o, o.getDefaultValue());
|
||||
optionResults.add(OptionResult.of(o, value));
|
||||
});
|
||||
|
||||
List<CommandOption> optionsForArguments = registration.getOptions().stream()
|
||||
.filter(o -> !resolvedOptions1.contains(o))
|
||||
.filter(o -> o.getPosition() > -1)
|
||||
.filter(o -> o.getArityMin() > -1)
|
||||
.sorted(Comparator.comparingInt(o -> o.getPosition()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<String> argumentValues = argumentResults.stream()
|
||||
.sorted(Comparator.comparingInt(ar -> ar.position()))
|
||||
.map(ar -> ar.value())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int i = 0;
|
||||
for (CommandOption o : optionsForArguments) {
|
||||
int j = i + o.getArityMax();
|
||||
j = Math.min(argumentValues.size(), j);
|
||||
|
||||
List<String> asdf = argumentValues.subList(i, j);
|
||||
if (asdf.isEmpty()) {
|
||||
optionResults.add(OptionResult.of(o, null));
|
||||
}
|
||||
else {
|
||||
Object value = convertOptionType(o, asdf);
|
||||
optionResults.add(OptionResult.of(o, value));
|
||||
}
|
||||
|
||||
if (j == argumentValues.size()) {
|
||||
break;
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new ParseResult(registration, optionResults, argumentResults, messageResults, directiveResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnterDirectiveNode(DirectiveNode node) {
|
||||
directiveResults.add(DirectiveResult.of(node.getName(), node.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExitDirectiveNode(DirectiveNode node) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnterRootCommandNode(CommandNode node) {
|
||||
resolvedCommmand.add(node.getCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExitRootCommandNode(CommandNode node) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnterCommandNode(CommandNode node) {
|
||||
resolvedCommmand.add(node.getCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExitCommandNode(CommandNode node) {
|
||||
}
|
||||
|
||||
private List<CommandOption> currentOptions = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onEnterOptionNode(OptionNode node) {
|
||||
commandArgumentPos = 0;
|
||||
currentOptions.clear();
|
||||
currentOptionArgument.clear();
|
||||
CommandInfo info = commandModel.resolve(resolvedCommmand);
|
||||
|
||||
String name = node.getName();
|
||||
if (name.startsWith("--")) {
|
||||
info.registration.getOptions().forEach(option -> {
|
||||
Set<String> longNames = Arrays.asList(option.getLongNames()).stream()
|
||||
.map(n -> "--" + n)
|
||||
.collect(Collectors.toSet());
|
||||
String nameToMatch = config.isEnabled(Feature.CASE_SENSITIVE_OPTIONS) ? name : name.toLowerCase();
|
||||
boolean match = longNames.contains(nameToMatch);
|
||||
if (match) {
|
||||
currentOptions.add(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (name.startsWith("-")) {
|
||||
if (name.length() == 2) {
|
||||
info.registration.getOptions().forEach(option -> {
|
||||
Set<String> shortNames = Arrays.asList(option.getShortNames()).stream()
|
||||
.map(n -> "-" + Character.toString(n))
|
||||
.collect(Collectors.toSet());
|
||||
boolean match = shortNames.contains(name);
|
||||
if (match) {
|
||||
currentOptions.add(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (name.length() > 2) {
|
||||
info.registration.getOptions().forEach(option -> {
|
||||
Set<String> shortNames = Arrays.asList(option.getShortNames()).stream()
|
||||
.map(n -> "-" + Character.toString(n))
|
||||
.collect(Collectors.toSet());
|
||||
for (int i = 1; i < name.length(); i++) {
|
||||
boolean match = shortNames.contains("-" + name.charAt(i));
|
||||
if (match) {
|
||||
currentOptions.add(option);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExitOptionNode(OptionNode node) {
|
||||
if (!currentOptions.isEmpty()) {
|
||||
for (CommandOption currentOption : currentOptions) {
|
||||
int max = currentOption.getArityMax() > 0 ? currentOption.getArityMax() : Integer.MAX_VALUE;
|
||||
max = Math.min(max, currentOptionArgument.size());
|
||||
List<String> toUse = currentOptionArgument.subList(0, max);
|
||||
|
||||
if (currentOption.getArityMin() > -1 && currentOptionArgument.size() < currentOption.getArityMin()) {
|
||||
String arg = currentOption.getLongNames()[0];
|
||||
commonMessageResults.add(MessageResult.of(ParserMessage.NOT_ENOUGH_OPTION_ARGUMENTS, 0, arg,
|
||||
currentOptionArgument.size()));
|
||||
}
|
||||
else if (currentOption.getArityMax() > -1 && currentOptionArgument.size() > currentOption.getArityMax()) {
|
||||
String arg = currentOption.getLongNames()[0];
|
||||
commonMessageResults.add(MessageResult.of(ParserMessage.TOO_MANY_OPTION_ARGUMENTS, 0, arg,
|
||||
currentOption.getArityMax()));
|
||||
}
|
||||
|
||||
Object value = null;
|
||||
if (toUse.size() == 1) {
|
||||
value = toUse.get(0);
|
||||
}
|
||||
else if (toUse.size() > 1) {
|
||||
value = new ArrayList<>(toUse);
|
||||
}
|
||||
|
||||
try {
|
||||
value = convertOptionType(currentOption, value);
|
||||
} catch (Exception e) {
|
||||
commonMessageResults.add(MessageResult.of(ParserMessage.ILLEGAL_OPTION_VALUE, 0, value, e.getMessage()));
|
||||
}
|
||||
optionResults.add(new OptionResult(currentOption, value));
|
||||
}
|
||||
}
|
||||
else {
|
||||
invalidOptionNodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnterCommandArgumentNode(CommandArgumentNode node) {
|
||||
argumentResults.add(ArgumentResult.of(node.getToken().getValue(), commandArgumentPos++));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExitCommandArgumentNode(CommandArgumentNode node) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnterOptionArgumentNode(OptionArgumentNode node) {
|
||||
currentOptionArgument.add(node.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExitOptionArgumentNode(OptionArgumentNode node) {
|
||||
}
|
||||
|
||||
private Object convertOptionType(CommandOption option, Object value) {
|
||||
ResolvableType type = option.getType();
|
||||
if (value == null && type != null && type.isAssignableFrom(boolean.class)) {
|
||||
return true;
|
||||
}
|
||||
if (conversionService != null && option.getType() != null && value != null) {
|
||||
if (conversionService.canConvert(value.getClass(), option.getType().getRawClass())) {
|
||||
value = conversionService.convert(value, option.getType().getRawClass());
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private List<MessageResult> validateOptionNotMissing(CommandRegistration registration) {
|
||||
HashSet<CommandOption> requiredOptions = registration.getOptions().stream()
|
||||
.filter(o -> o.isRequired())
|
||||
.collect(Collectors.toCollection(() -> new HashSet<>()));
|
||||
|
||||
optionResults.stream().map(or -> or.option()).forEach(o -> {
|
||||
requiredOptions.remove(o);
|
||||
});
|
||||
|
||||
return requiredOptions.stream()
|
||||
.map(o -> {
|
||||
String ln = o.getLongNames() != null
|
||||
? Stream.of(o.getLongNames()).collect(Collectors.joining(","))
|
||||
: "";
|
||||
String sn = o.getShortNames() != null ? Stream.of(o.getShortNames()).map(n -> Character.toString(n))
|
||||
.collect(Collectors.joining(",")) : "";
|
||||
return MessageResult.of(ParserMessage.MANDATORY_OPTION_MISSING, 0, ln, sn);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<MessageResult> validateOptionIsValid(CommandRegistration registration) {
|
||||
return invalidOptionNodes.stream()
|
||||
.map(on -> {
|
||||
return MessageResult.of(ParserMessage.UNRECOGNISED_OPTION, 0, on.getName());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
public class ParserConfig {
|
||||
|
||||
private long features;
|
||||
|
||||
public ParserConfig() {
|
||||
this.features = Feature.collectDefaults();
|
||||
}
|
||||
|
||||
boolean isEnabled(Feature feature) {
|
||||
return (features & feature.getMask()) != 0;
|
||||
}
|
||||
|
||||
ParserConfig configure(Feature feature, boolean state) {
|
||||
return state ? enable(feature) : disable(feature);
|
||||
}
|
||||
|
||||
ParserConfig enable(Feature feature) {
|
||||
features |= feature.getMask();
|
||||
return this;
|
||||
}
|
||||
|
||||
ParserConfig disable(Feature feature) {
|
||||
features &= ~feature.getMask();
|
||||
return this;
|
||||
}
|
||||
|
||||
public static enum Feature {
|
||||
|
||||
/**
|
||||
* Defines if directives support is enabled, disabled on default.
|
||||
*/
|
||||
ALLOW_DIRECTIVES(false),
|
||||
|
||||
/**
|
||||
* Used in a case where directive support is disabled and parser should ignore
|
||||
* ones found instead of reporting error, disabled on default.
|
||||
*/
|
||||
IGNORE_DIRECTIVES(false),
|
||||
|
||||
/**
|
||||
* Defines if commands are parsed using case-sensitivity, enabled on default.
|
||||
*/
|
||||
CASE_SENSITIVE_COMMANDS(true),
|
||||
|
||||
/**
|
||||
* Defines if options are parsed using case-sensitivity, enabled on default.
|
||||
*/
|
||||
CASE_SENSITIVE_OPTIONS(true)
|
||||
;
|
||||
|
||||
private final boolean defaultState;
|
||||
private final long mask;
|
||||
|
||||
private Feature(boolean defaultState) {
|
||||
this.mask = (1 << ordinal());
|
||||
this.defaultState = defaultState;
|
||||
}
|
||||
|
||||
public static long collectDefaults() {
|
||||
long flags = 0;
|
||||
for (Feature f : values()) {
|
||||
if (f.enabledByDefault()) {
|
||||
flags |= f.getMask();
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
public boolean enabledByDefault() {
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
public boolean enabledIn(int flags) {
|
||||
return (flags & mask) != 0;
|
||||
}
|
||||
|
||||
public long getMask() {
|
||||
return mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
/**
|
||||
* Contains all the messages that can be produced during parsing. Each message
|
||||
* has a kind (WARNING, ERROR) and a code number. Code is used to identify
|
||||
* particular message and makes it easier to test what messages are produced.
|
||||
* Code numbers are split so that ones within {@code 1xxx} are from lexer
|
||||
* and {@code 2xxx} from parser.
|
||||
*
|
||||
* Messages with {@code ERROR} should be treated as terminating messages because
|
||||
* those are most likely hard errors based on manual validation or exception
|
||||
* thrown within lexing or parsing.
|
||||
*
|
||||
* Messages with {@code WARNING} can be ignored but can be used to provide
|
||||
* info to user. For example parsing may detect some ambiguities with a command
|
||||
* and option model related to what user tries to use as an input. This
|
||||
* because there are limits how clever a parser can be as command model
|
||||
* is beyond its control.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public enum ParserMessage {
|
||||
|
||||
ILLEGAL_CONTENT_BEFORE_COMMANDS(Type.ERROR, 1000, "Illegal content before commands ''{0}''"),
|
||||
MANDATORY_OPTION_MISSING(Type.ERROR, 2000, "Missing mandatory option, longnames=''{0}'', shortnames=''{1}''"),
|
||||
UNRECOGNISED_OPTION(Type.ERROR, 2001, "Unrecognised option ''{0}''"),
|
||||
ILLEGAL_OPTION_VALUE(Type.ERROR, 2002, "Illegal option value ''{0}'', reason ''{1}''"),
|
||||
NOT_ENOUGH_OPTION_ARGUMENTS(Type.ERROR, 2003, "Not enough arguments for option ''{0}'', requires at least ''{1}''"),
|
||||
TOO_MANY_OPTION_ARGUMENTS(Type.ERROR, 2004, "Too many arguments for option ''{0}'', requires at most ''{1}''")
|
||||
;
|
||||
|
||||
private Type type;
|
||||
private int code;
|
||||
private String message;
|
||||
|
||||
ParserMessage(Type type, int code, String message) {
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String formatMessage(int position, Object... inserts) {
|
||||
StringBuilder msg = new StringBuilder();
|
||||
msg.append(code);
|
||||
switch (type) {
|
||||
case WARNING:
|
||||
msg.append("W");
|
||||
break;
|
||||
case ERROR:
|
||||
msg.append("E");
|
||||
break;
|
||||
}
|
||||
msg.append(":");
|
||||
if (position != -1) {
|
||||
msg.append("(pos ").append(position).append("): ");
|
||||
}
|
||||
msg.append(MessageFormat.format(message, inserts));
|
||||
return msg.toString();
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
WARNING,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
/**
|
||||
* {@code Terminal} node means that it is end of {@code ast tree branch}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract sealed class TerminalAstNode
|
||||
extends AstNode permits DirectiveNode, OptionArgumentNode, CommandArgumentNode {
|
||||
|
||||
public TerminalAstNode(Token token) {
|
||||
super(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
public class Token {
|
||||
|
||||
private final static int IMPLICIT_POSITION = -1;
|
||||
private final String value;
|
||||
private final TokenType type;
|
||||
private final int position;
|
||||
|
||||
public Token(String value, TokenType type) {
|
||||
this(value, type, IMPLICIT_POSITION);
|
||||
}
|
||||
|
||||
public Token(String value, TokenType type, int position) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public static Token of(String value, TokenType type) {
|
||||
return new Token(value, type);
|
||||
}
|
||||
|
||||
public static Token of(String value, TokenType type, int position) {
|
||||
return new Token(value, type, position);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public TokenType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Token [type=" + type + ", position=" + position + ", value=" + value + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
public enum TokenType {
|
||||
|
||||
ARGUMENT,
|
||||
COMMAND,
|
||||
OPTION,
|
||||
DOUBLEDASH,
|
||||
DIRECTIVE
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
@@ -37,14 +38,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
public class CommandExecutionTests extends AbstractCommandTests {
|
||||
|
||||
private CommandExecution execution;
|
||||
private CommandCatalog commandCatalog;
|
||||
|
||||
@BeforeEach
|
||||
public void setupCommandExecutionTests() {
|
||||
commandCatalog = CommandCatalog.of();
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(new ArgumentHeaderMethodArgumentResolver(conversionService, null));
|
||||
resolvers.add(new CommandContextMethodArgumentResolver());
|
||||
execution = CommandExecution.of(resolvers, null, null, conversionService);
|
||||
execution = CommandExecution.of(resolvers, null, null, conversionService, commandCatalog);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -60,7 +63,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
commandCatalog.register(r1);
|
||||
Object result = execution.evaluate(new String[] { "command1", "--arg1", "myarg1value" });
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
}
|
||||
|
||||
@@ -77,7 +81,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
commandCatalog.register(r1);
|
||||
Object result = execution.evaluate(new String[] { "command1", "--arg1", "myarg1value" });
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
assertThat(pojo1.method3Count).isEqualTo(1);
|
||||
}
|
||||
@@ -95,7 +100,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method1")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "--arg1", "myarg1value" });
|
||||
assertThat(pojo1.method1Count).isEqualTo(1);
|
||||
assertThat(pojo1.method1Ctx).isNotNull();
|
||||
}
|
||||
@@ -109,13 +115,13 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "--arg1" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isNull();
|
||||
}
|
||||
@@ -135,7 +141,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "myarg1value" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value");
|
||||
}
|
||||
@@ -152,7 +159,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
commandCatalog.register(r1);
|
||||
Object result = execution.evaluate(new String[] { "command1", "--arg1", "myarg1value" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value");
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
@@ -173,7 +181,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "myarg1value1", "myarg1value2" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1");
|
||||
}
|
||||
@@ -193,7 +202,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "myarg1value1", "myarg1value2" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1,myarg1value2");
|
||||
}
|
||||
@@ -213,7 +223,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method9")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "myarg1value1", "myarg1value2" });
|
||||
assertThat(pojo1.method9Count).isEqualTo(1);
|
||||
assertThat(pojo1.method9Arg1).isEqualTo(new String[] { "myarg1value1", "myarg1value2" });
|
||||
}
|
||||
@@ -233,7 +244,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"1", "2"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "1", "2" });
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[] { 1, 2 });
|
||||
}
|
||||
@@ -260,7 +272,9 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"--arg1", "myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(
|
||||
new String[] { "command1", "--arg1", "myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value" });
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
@@ -290,7 +304,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"--arg1", "1", "--arg2", "2", "--arg3", "3"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "--arg1", "1", "--arg2", "2", "--arg3", "3" });
|
||||
assertThat(pojo1.method7Count).isEqualTo(1);
|
||||
assertThat(pojo1.method7Arg1).isEqualTo(1);
|
||||
assertThat(pojo1.method7Arg2).isEqualTo(2);
|
||||
@@ -325,7 +340,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"myarg1value", "myarg2value", "myarg3value"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "myarg1value", "myarg2value", "myarg3value" });
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
@@ -333,10 +349,11 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@Disabled("concepts change")
|
||||
@ValueSource(strings = {
|
||||
"myarg1value --arg2 myarg2value --arg3 myarg3value",
|
||||
"--arg1 myarg1value myarg2value --arg3 myarg3value",
|
||||
"--arg1 myarg1value --arg2 myarg2value myarg3value"
|
||||
"command1 myarg1value --arg2 myarg2value --arg3 myarg3value",
|
||||
"command1 --arg1 myarg1value myarg2value --arg3 myarg3value",
|
||||
"command1 --arg1 myarg1value --arg2 myarg2value myarg3value"
|
||||
})
|
||||
public void testMethodMultiplePositionalStringArgsMixed(String arg) {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
@@ -365,8 +382,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.and()
|
||||
.build();
|
||||
String[] args = arg.split(" ");
|
||||
// execution.evaluate(r1, new String[]{"myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value"});
|
||||
execution.evaluate(r1, args);
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(args);
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
@@ -397,7 +414,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method5")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"-abc"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "-abc" });
|
||||
assertThat(pojo1.method5ArgA).isTrue();
|
||||
assertThat(pojo1.method5ArgB).isTrue();
|
||||
assertThat(pojo1.method5ArgC).isTrue();
|
||||
@@ -427,7 +445,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method5")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"-ac", "-b", "false"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "-ac", "-b", "false" });
|
||||
assertThat(pojo1.method5ArgA).isTrue();
|
||||
assertThat(pojo1.method5ArgB).isFalse();
|
||||
assertThat(pojo1.method5ArgC).isTrue();
|
||||
@@ -446,7 +465,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "0.1"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "--arg1", "0.1" });
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[]{0.1f});
|
||||
}
|
||||
@@ -464,7 +484,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "0.1", "0.2"});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1", "--arg1", "0.1", "0.2" });
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[]{0.1f, 0.2f});
|
||||
}
|
||||
@@ -480,7 +501,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isNull();
|
||||
}
|
||||
@@ -499,7 +521,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{});
|
||||
commandCatalog.register(r1);
|
||||
execution.evaluate(new String[] { "command1" });
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("defaultValue1");
|
||||
}
|
||||
@@ -517,8 +540,9 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.and()
|
||||
.build();
|
||||
|
||||
commandCatalog.register(r1);
|
||||
assertThatThrownBy(() -> {
|
||||
execution.evaluate(r1, new String[]{});
|
||||
execution.evaluate(new String[] { "command1" });
|
||||
}).isInstanceOf(CommandParserExceptionsException.class);
|
||||
}
|
||||
|
||||
@@ -536,7 +560,8 @@ public class CommandExecutionTests extends AbstractCommandTests {
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
commandCatalog.register(r1);
|
||||
Object result = execution.evaluate(new String[] { "command1", "--arg1", "myarg1value" });
|
||||
assertThat(result).isInstanceOf(CommandNotCurrentlyAvailable.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2022-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,9 +22,6 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserException;
|
||||
import org.springframework.shell.command.CommandParser.MissingOptionException;
|
||||
import org.springframework.shell.command.CommandParser.NotEnoughArgumentsOptionException;
|
||||
import org.springframework.shell.command.CommandParser.TooManyArgumentsOptionException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -33,118 +30,14 @@ class CommandParserExceptionResolverTests {
|
||||
private final CommandParserExceptionResolver resolver = new CommandParserExceptionResolver();
|
||||
|
||||
@Test
|
||||
void resolvesMissingLongOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("Desc arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(missingOption(registration.getOptions().get(0)));
|
||||
void resolvesCommandParserException() {
|
||||
CommandHandlingResult resolve = resolver.resolve(genericParserException());
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("--arg1", "Desc arg1");
|
||||
assertThat(resolve.message()).contains("hi");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesMissingLongOptionWhenAlsoShort() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.shortNames('x')
|
||||
.description("Desc arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(missingOption(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("--arg1", "Desc arg1");
|
||||
assertThat(resolve.message()).doesNotContain("-x", "Desc x");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesMissingShortOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.shortNames('x')
|
||||
.description("Desc x")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(missingOption(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("-x", "Desc x");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesTooManyLongOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("Desc arg1")
|
||||
.arity(2, 3)
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(tooManyArguments(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("--arg1 requires at most", "Desc arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesNotEnoughLongOption() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("required-value")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("Desc arg1")
|
||||
.arity(2, 3)
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
CommandHandlingResult resolve = resolver.resolve(notEnoughArguments(registration.getOptions().get(0)));
|
||||
assertThat(resolve).isNotNull();
|
||||
assertThat(resolve.message()).contains("--arg1 requires at least", "Desc arg1");
|
||||
}
|
||||
|
||||
static CommandParserExceptionsException missingOption(CommandOption option) {
|
||||
MissingOptionException e = new MissingOptionException("msg", option);
|
||||
List<CommandParserException> parserExceptions = Arrays.asList(e);
|
||||
return new CommandParserExceptionsException("msg", parserExceptions);
|
||||
}
|
||||
|
||||
static CommandParserExceptionsException tooManyArguments(CommandOption option) {
|
||||
TooManyArgumentsOptionException e = new TooManyArgumentsOptionException("msg", option);
|
||||
List<CommandParserException> parserExceptions = Arrays.asList(e);
|
||||
return new CommandParserExceptionsException("msg", parserExceptions);
|
||||
}
|
||||
|
||||
static CommandParserExceptionsException notEnoughArguments(CommandOption option) {
|
||||
NotEnoughArgumentsOptionException e = new NotEnoughArgumentsOptionException("msg", option);
|
||||
static CommandParserExceptionsException genericParserException() {
|
||||
CommandParserException e = new CommandParserException("hi");
|
||||
List<CommandParserException> parserExceptions = Arrays.asList(e);
|
||||
return new CommandParserExceptionsException("msg", parserExceptions);
|
||||
}
|
||||
|
||||
@@ -15,600 +15,40 @@
|
||||
*/
|
||||
package org.springframework.shell.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
import org.springframework.shell.command.CommandParser.MissingOptionException;
|
||||
import org.springframework.shell.command.CommandParser.NotEnoughArgumentsOptionException;
|
||||
import org.springframework.shell.command.CommandParser.TooManyArgumentsOptionException;
|
||||
import org.springframework.shell.command.CommandParser.UnrecognisedOptionException;
|
||||
import org.springframework.shell.command.parser.ParserConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CommandParserTests extends AbstractCommandTests {
|
||||
|
||||
private CommandParser parser;
|
||||
@Test
|
||||
public void testMethodExecution1() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.description("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
public void setupCommandParserTests() {
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
parser = CommandParser.of(conversionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyOptionsAndArgs() {
|
||||
CommandParserResults results = parser.parse(Collections.emptyList(), new String[0]);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongName() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("command1", r1);
|
||||
CommandParser parser = CommandParser.of(conversionService, registrations, new ParserConfig());
|
||||
CommandParserResults results = parser.parse(new String[] { "command1", "--arg1", "myarg1value" });
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortName() {
|
||||
CommandOption option1 = shortOption('a');
|
||||
CommandOption option2 = shortOption('b');
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"-a", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArgs() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo", "--arg2", "bar"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArgsWithMultiValues() {
|
||||
CommandOption option1 = longOption("arg1", null, false, null, 1, 2);
|
||||
CommandOption option2 = longOption("arg2", null, false, null, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo1", "foo2", "--arg2", "bar1", "bar2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(Arrays.asList("foo1", "foo2"));
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(Arrays.asList("bar1", "bar2"));
|
||||
assertThat(results.positional()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithoutArg() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = shortOption('v', type);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"-v"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithArg() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = shortOption('v', type);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"-v", "false"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingRequiredOption() {
|
||||
CommandOption option1 = longOption("arg1", true);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.errors()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpaceInArgWithOneArg() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "foo bar"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpaceInArgWithMultipleArgs() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo bar", "--arg2", "hi"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo bar");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("hi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgs() {
|
||||
String[] args = new String[]{"arg1", "arg2"};
|
||||
CommandParserResults results = parser.parse(Collections.emptyList(), args);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
assertThat(results.positional()).containsExactly("arg1", "arg2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgBeforeOption() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"foo", "--arg1", "value"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value");
|
||||
assertThat(results.positional()).containsExactly("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgAfterOption() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value");
|
||||
assertThat(results.positional()).containsExactly("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgWithoutOption() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
// no type so we get raw list
|
||||
assertThat(results.results().get(0).value()).isEqualTo(Arrays.asList("value", "foo"));
|
||||
assertThat(results.positional()).containsExactly("value", "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgWithoutOptionHavingType() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value,foo");
|
||||
assertThat(results.positional()).containsExactly("value", "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMappedFromArgToString() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value,foo");
|
||||
assertThat(results.positional()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombined() {
|
||||
CommandOption optionA = shortOption('a');
|
||||
CommandOption optionB = shortOption('b');
|
||||
CommandOption optionC = shortOption('c');
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isNull();
|
||||
assertThat(results.results().get(1).value()).isNull();
|
||||
assertThat(results.results().get(2).value()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanType() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanTypeArgFalse() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc", "false"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(false);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(false);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanTypeSomeArgFalse() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-ac", "-b", "false"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongOptionsWithArray() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(int[].class));
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(new int[] { 1, 2 });
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongOptionsWithStringArray() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String[].class));
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(new String[] { "1", "2" });
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongOptionsWithPlainList() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(List.class));
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(Arrays.asList("1", "2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongOptionsWithTypedList() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forClassWithGenerics(List.class, String.class));
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(Arrays.asList("1", "2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArityErrors() {
|
||||
CommandOption option1 = CommandOption.of(
|
||||
new String[] { "arg1" },
|
||||
null,
|
||||
null,
|
||||
ResolvableType.forType(int[].class),
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
2,
|
||||
3,
|
||||
null,
|
||||
null);
|
||||
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
|
||||
String[] args1 = new String[]{"--arg1", "1", "2", "3", "4"};
|
||||
CommandParserResults results1 = parser.parse(options, args1);
|
||||
assertThat(results1.errors()).hasSize(1);
|
||||
assertThat(results1.errors().get(0)).isInstanceOf(TooManyArgumentsOptionException.class);
|
||||
assertThat(results1.results()).hasSize(1);
|
||||
assertThat(results1.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results1.results().get(0).value()).isNull();
|
||||
|
||||
String[] args2 = new String[]{"--arg1", "1"};
|
||||
CommandParserResults results2 = parser.parse(options, args2);
|
||||
assertThat(results2.errors()).hasSize(1);
|
||||
assertThat(results2.errors().get(0)).isInstanceOf(NotEnoughArgumentsOptionException.class);
|
||||
assertThat(results2.results()).hasSize(1);
|
||||
assertThat(results2.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results2.results().get(0).value()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapToIntArray() {
|
||||
CommandOption option1 = CommandOption.of(
|
||||
new String[] { "arg1" },
|
||||
null,
|
||||
null,
|
||||
ResolvableType.forType(int[].class),
|
||||
false,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
null,
|
||||
null);
|
||||
|
||||
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.errors()).hasSize(0);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(new int[] { 1, 2 });
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs1() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", ResolvableType.forType(String.class), false, 1, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("1");
|
||||
assertThat(results.results().get(1).value()).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs11() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", 1, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(Arrays.asList("1"));
|
||||
// no type so we get raw list
|
||||
assertThat(results.results().get(1).value()).isEqualTo(Arrays.asList("2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs2() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", ResolvableType.forType(String.class), false, 1, 1, 2);
|
||||
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("1");
|
||||
assertThat(results.results().get(1).value()).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithDefault() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = CommandOption.of(new String[] { "arg1" }, new Character[0], "description", type, false,
|
||||
"true", null, null, null, null, null);
|
||||
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerWithDefault() {
|
||||
ResolvableType type = ResolvableType.forType(Integer.class);
|
||||
CommandOption option1 = CommandOption.of(new String[] { "arg1" }, new Character[0], "description", type, false,
|
||||
"1", null, null, null, null, null);
|
||||
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerWithGivenValue() {
|
||||
ResolvableType type = ResolvableType.forType(Integer.class);
|
||||
CommandOption option1 = CommandOption.of(new String[] { "arg1" }, new Character[0], "description", type, false,
|
||||
null, null, null, null, null, null);
|
||||
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[] { "--arg1", "1" };
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotDefinedLongOptionWithoutOptions() {
|
||||
// gh-602
|
||||
List<CommandOption> options = Arrays.asList();
|
||||
String[] args = new String[]{"--arg1", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
assertThat(results.errors()).hasSize(1);
|
||||
assertThat(results.errors()).satisfiesExactly(
|
||||
e -> {
|
||||
assertThat(e).isInstanceOf(UnrecognisedOptionException.class);
|
||||
}
|
||||
);
|
||||
assertThat(results.positional()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotDefinedLongOptionWithOptionalOption() {
|
||||
// gh-602
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "bar", "--arg2", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.errors()).hasSize(1);
|
||||
assertThat(results.errors()).satisfiesExactly(
|
||||
e -> {
|
||||
assertThat(e).isInstanceOf(UnrecognisedOptionException.class);
|
||||
}
|
||||
);
|
||||
assertThat(results.positional()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotDefinedLongOptionWithRequiredOption() {
|
||||
// gh-602
|
||||
CommandOption option1 = longOption("arg1", true);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg2", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
assertThat(results.errors()).hasSize(2);
|
||||
assertThat(results.errors()).satisfiesExactly(
|
||||
e -> {
|
||||
assertThat(e).isInstanceOf(UnrecognisedOptionException.class);
|
||||
},
|
||||
e -> {
|
||||
assertThat(e).isInstanceOf(MissingOptionException.class);
|
||||
}
|
||||
);
|
||||
assertThat(results.positional()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDashOptionValueDoNotError() {
|
||||
// gh-651
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "-1"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.errors()).hasSize(0);
|
||||
assertThat(results.positional()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPositionDoesNotAffectRequiredErrorWithOtherErrors() {
|
||||
// gh-601
|
||||
CommandOption o1 = CommandOption.of(
|
||||
new String[] { "arg1" },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
null);
|
||||
|
||||
List<CommandOption> options = Arrays.asList(o1);
|
||||
String[] args = new String[]{"--arg2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
assertThat(results.errors()).hasSize(2);
|
||||
assertThat(results.positional()).hasSize(0);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name) {
|
||||
return longOption(name, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, boolean required) {
|
||||
return longOption(name, null, required, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type) {
|
||||
return longOption(name, type, false, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, int position, int arityMin, int arityMax) {
|
||||
return longOption(name, null, false, position, arityMin, arityMax);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type, boolean required, Integer position) {
|
||||
return longOption(name, type, required, position, null, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type, boolean required, Integer position, Integer arityMin, Integer arityMax) {
|
||||
return CommandOption.of(new String[] { name }, new Character[0], "desc", type, required, null, position,
|
||||
arityMin, arityMax, null, null);
|
||||
}
|
||||
|
||||
private static CommandOption shortOption(char name) {
|
||||
return shortOption(name, null);
|
||||
}
|
||||
|
||||
private static CommandOption shortOption(char name, ResolvableType type) {
|
||||
return CommandOption.of(new String[0], new Character[] { name }, "desc", type);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("myarg1value");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.parser.Ast.AstResult;
|
||||
import org.springframework.shell.command.parser.Lexer.LexerResult;
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult;
|
||||
|
||||
abstract class AbstractParsingTests {
|
||||
|
||||
static final CommandRegistration ROOT1 = CommandRegistration.builder()
|
||||
.command("root1")
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT1_UP = CommandRegistration.builder()
|
||||
.command("ROOT1")
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT2 = CommandRegistration.builder()
|
||||
.command("root2")
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT2_SUB1 = CommandRegistration.builder()
|
||||
.command("root2", "sub1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT2_SUB2 = CommandRegistration.builder()
|
||||
.command("root2", "sub2")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT2_SUB1_SUB2 = CommandRegistration.builder()
|
||||
.command("root2", "sub1", "sub2")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT2_SUB1_SUB3 = CommandRegistration.builder()
|
||||
.command("root2", "sub1", "sub3")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT2_SUB1_SUB4 = CommandRegistration.builder()
|
||||
.command("root2", "sub1", "sub4")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT3 = CommandRegistration.builder()
|
||||
.command("root3")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT3_SHORT_OPTION_A = CommandRegistration.builder()
|
||||
.command("root3")
|
||||
.withOption()
|
||||
.shortNames('a')
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT4 = CommandRegistration.builder()
|
||||
.command("root4")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT5 = CommandRegistration.builder()
|
||||
.command("root5")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT6_OPTION_INT = CommandRegistration.builder()
|
||||
.command("root6")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(int.class)
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT6_OPTION_INTARRAY = CommandRegistration.builder()
|
||||
.command("root6")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(int[].class)
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
static final CommandRegistration ROOT6_OPTION_DEFAULT_VALUE = CommandRegistration.builder()
|
||||
.command("root6")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.defaultValue("defaultvalue")
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
registrations.clear();
|
||||
}
|
||||
|
||||
void register(CommandRegistration registration) {
|
||||
registrations.put(registration.getCommand(), registration);
|
||||
}
|
||||
|
||||
CommandModel commandModel() {
|
||||
return new CommandModel(registrations, new ParserConfig());
|
||||
}
|
||||
|
||||
CommandModel commandModel(ParserConfig configuration) {
|
||||
return new CommandModel(registrations, configuration);
|
||||
}
|
||||
|
||||
Token token(String value, TokenType type, int position) {
|
||||
return new Token(value, type, position);
|
||||
}
|
||||
|
||||
Lexer lexer() {
|
||||
return new Lexer.DefaultLexer(commandModel(), new ParserConfig());
|
||||
}
|
||||
|
||||
Lexer lexer(ParserConfig config) {
|
||||
return new Lexer.DefaultLexer(commandModel(config), config);
|
||||
}
|
||||
|
||||
Lexer lexer(CommandModel commandModel) {
|
||||
return new Lexer.DefaultLexer(commandModel, new ParserConfig());
|
||||
}
|
||||
|
||||
Lexer lexer(CommandModel commandModel, ParserConfig configuration) {
|
||||
return new Lexer.DefaultLexer(commandModel, configuration);
|
||||
}
|
||||
|
||||
List<Token> tokenize(String... arguments) {
|
||||
return tokenize(lexer(), arguments);
|
||||
}
|
||||
|
||||
List<Token> tokenize(Lexer lexer, String... arguments) {
|
||||
return lexer.tokenize(Arrays.asList(arguments)).tokens();
|
||||
}
|
||||
|
||||
LexerResult tokenizeAsResult(String... arguments) {
|
||||
return tokenizeAsResult(lexer(), arguments);
|
||||
}
|
||||
|
||||
LexerResult tokenizeAsResult(Lexer lexer, String... arguments) {
|
||||
return lexer.tokenize(Arrays.asList(arguments));
|
||||
}
|
||||
|
||||
Ast ast() {
|
||||
return new Ast.DefaultAst();
|
||||
}
|
||||
|
||||
AstResult ast(List<Token> tokens) {
|
||||
Ast ast = ast();
|
||||
return ast.generate(tokens);
|
||||
}
|
||||
|
||||
AstResult ast(Token... tokens) {
|
||||
Ast ast = ast();
|
||||
return ast.generate(Arrays.asList(tokens));
|
||||
}
|
||||
|
||||
ParseResult parse(String... arguments) {
|
||||
CommandModel commandModel = commandModel();
|
||||
Lexer lexer = lexer(commandModel);
|
||||
Ast ast = ast();
|
||||
Parser parser = new Parser.DefaultParser(commandModel, lexer, ast);
|
||||
return parser.parse(Arrays.asList(arguments));
|
||||
}
|
||||
|
||||
ParseResult parse(Lexer lexer, String... arguments) {
|
||||
CommandModel commandModel = commandModel();
|
||||
Ast ast = ast();
|
||||
Parser parser = new Parser.DefaultParser(commandModel, lexer, ast);
|
||||
return parser.parse(Arrays.asList(arguments));
|
||||
}
|
||||
|
||||
ParseResult parse(ParserConfig config, String... arguments) {
|
||||
CommandModel commandModel = commandModel(config);
|
||||
Ast ast = ast();
|
||||
Parser parser = new Parser.DefaultParser(commandModel, lexer(config), ast, config);
|
||||
return parser.parse(Arrays.asList(arguments));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.parser.Ast.AstResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for default implementation in an {@link Ast}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
class AstTests extends AbstractParsingTests {
|
||||
|
||||
@Test
|
||||
void createsCommandNode() {
|
||||
register(ROOT1);
|
||||
Token root1 = token("root1", TokenType.COMMAND, 0);
|
||||
AstResult result = ast(root1);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.nonterminalNodes()).satisfiesExactly(
|
||||
node -> {
|
||||
assertThat(node).isInstanceOf(CommandNode.class);
|
||||
CommandNode cn = (CommandNode)node;
|
||||
assertThat(cn.getCommand()).isEqualTo("root1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsMultipleCommandNodes() {
|
||||
register(ROOT2_SUB1_SUB2);
|
||||
Token root1 = token("root1", TokenType.COMMAND, 0);
|
||||
Token sub1 = token("sub1", TokenType.COMMAND, 1);
|
||||
Token sub2 = token("sub2", TokenType.COMMAND, 2);
|
||||
Token arg1 = token("--arg1", TokenType.OPTION, 3);
|
||||
Token value1 = token("xxx", TokenType.ARGUMENT, 4);
|
||||
AstResult result = ast(root1, sub1, sub2, arg1, value1);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.nonterminalNodes()).satisfiesExactly(
|
||||
n1 -> {
|
||||
assertThat(n1).isInstanceOf(CommandNode.class);
|
||||
CommandNode cn1 = (CommandNode)n1;
|
||||
assertThat(cn1.getCommand()).isEqualTo("root1");
|
||||
assertThat(cn1.getChildren()).satisfiesExactly(
|
||||
n2 -> {
|
||||
assertThat(n2).isInstanceOf(CommandNode.class);
|
||||
CommandNode cn2 = (CommandNode)n2;
|
||||
assertThat(cn2.getCommand()).isEqualTo("sub1");
|
||||
assertThat(cn2.getChildren()).satisfiesExactly(
|
||||
n3 -> {
|
||||
assertThat(n3).isInstanceOf(CommandNode.class);
|
||||
CommandNode cn3 = (CommandNode)n3;
|
||||
assertThat(cn3.getCommand()).isEqualTo("sub2");
|
||||
assertThat(cn3.getChildren()).satisfiesExactly(
|
||||
n4 -> {
|
||||
assertThat(n4).isInstanceOf(OptionNode.class);
|
||||
OptionNode on4 = (OptionNode)n4;
|
||||
assertThat(on4.getChildren()).satisfiesExactly(
|
||||
n5 -> {
|
||||
assertThat(n5).isInstanceOf(OptionArgumentNode.class);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsOptionNodeNoOptionArg() {
|
||||
register(ROOT3);
|
||||
Token root3 = token("root3", TokenType.COMMAND, 0);
|
||||
Token arg1 = token("--arg1", TokenType.OPTION, 1);
|
||||
AstResult result = ast(root3, arg1);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.nonterminalNodes()).hasSize(1);
|
||||
assertThat(result.nonterminalNodes().get(0)).isInstanceOf(CommandNode.class);
|
||||
assertThat(result.nonterminalNodes().get(0)).satisfies(n -> {
|
||||
CommandNode cn = (CommandNode)n;
|
||||
assertThat(cn.getCommand()).isEqualTo("root3");
|
||||
assertThat(cn.getChildren()).hasSize(1);
|
||||
assertThat(cn.getChildren())
|
||||
.filteredOn(on -> on instanceof OptionNode)
|
||||
.extracting(on -> {
|
||||
return ((OptionNode)on).getName();
|
||||
})
|
||||
.containsExactly("--arg1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsOptionNodeWithOptionArg() {
|
||||
register(ROOT3);
|
||||
Token root3 = token("root3", TokenType.COMMAND, 0);
|
||||
Token arg1 = token("--arg1", TokenType.OPTION, 1);
|
||||
Token value1 = token("value1", TokenType.ARGUMENT, 2);
|
||||
AstResult result = ast(root3, arg1, value1);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.nonterminalNodes()).hasSize(1);
|
||||
assertThat(result.nonterminalNodes().get(0)).isInstanceOf(CommandNode.class);
|
||||
assertThat(result.nonterminalNodes().get(0)).satisfies(n -> {
|
||||
CommandNode cn = (CommandNode)n;
|
||||
assertThat(cn.getCommand()).isEqualTo("root3");
|
||||
assertThat(cn.getChildren()).hasSize(1);
|
||||
assertThat(cn.getChildren())
|
||||
.filteredOn(on -> on instanceof OptionNode)
|
||||
.extracting(on -> {
|
||||
return ((OptionNode)on).getName();
|
||||
})
|
||||
.containsExactly("--arg1");
|
||||
OptionNode on = (OptionNode) cn.getChildren().get(0);
|
||||
assertThat(on.getChildren()).hasSize(1);
|
||||
OptionArgumentNode oan = (OptionArgumentNode) on.getChildren().get(0);
|
||||
assertThat(oan.getValue()).isEqualTo("value1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createOptionNodesWhenNoOptionArguments() {
|
||||
register(ROOT3);
|
||||
Token root3 = token("root3", TokenType.COMMAND, 0);
|
||||
Token arg1 = token("--arg1", TokenType.OPTION, 1);
|
||||
Token arg2 = token("--arg2", TokenType.OPTION, 2);
|
||||
AstResult result = ast(root3, arg1, arg2);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.nonterminalNodes()).hasSize(1);
|
||||
assertThat(result.nonterminalNodes().get(0)).isInstanceOf(CommandNode.class);
|
||||
assertThat(result.nonterminalNodes().get(0)).satisfies(n -> {
|
||||
CommandNode cn = (CommandNode)n;
|
||||
assertThat(cn.getCommand()).isEqualTo("root3");
|
||||
assertThat(cn.getChildren()).hasSize(2);
|
||||
assertThat(cn.getChildren())
|
||||
.filteredOn(on -> on instanceof OptionNode)
|
||||
.extracting(on -> {
|
||||
return ((OptionNode)on).getName();
|
||||
})
|
||||
.containsExactly("--arg1", "--arg2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void directiveNoValueWithCommand() {
|
||||
register(ROOT1);
|
||||
Token directive = token("fake", TokenType.DIRECTIVE, 0);
|
||||
Token root1 = token("root1", TokenType.COMMAND, 1);
|
||||
AstResult result = ast(directive, root1);
|
||||
|
||||
assertThat(result.terminalNodes()).hasSize(1);
|
||||
assertThat(result.terminalNodes().get(0)).isInstanceOf(DirectiveNode.class);
|
||||
DirectiveNode dn = (DirectiveNode) result.terminalNodes().get(0);
|
||||
assertThat(dn.getName()).isEqualTo("fake");
|
||||
assertThat(dn.getValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void directiveWithValueWithCommand() {
|
||||
register(ROOT1);
|
||||
Token directive = token("fake:value", TokenType.DIRECTIVE, 0);
|
||||
Token root1 = token("root1", TokenType.COMMAND, 1);
|
||||
AstResult result = ast(directive, root1);
|
||||
|
||||
assertThat(result.terminalNodes()).hasSize(1);
|
||||
assertThat(result.terminalNodes().get(0)).isInstanceOf(DirectiveNode.class);
|
||||
DirectiveNode dn = (DirectiveNode) result.terminalNodes().get(0);
|
||||
assertThat(dn.getName()).isEqualTo("fake");
|
||||
assertThat(dn.getValue()).isEqualTo("value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleDirectivesWithValueWithCommand() {
|
||||
register(ROOT1);
|
||||
Token directive1 = token("fake1:value1", TokenType.DIRECTIVE, 0);
|
||||
Token directive2 = token("fake2:value2", TokenType.DIRECTIVE, 1);
|
||||
Token root1 = token("root1", TokenType.COMMAND, 2);
|
||||
AstResult result = ast(directive1, directive2, root1);
|
||||
|
||||
assertThat(result.terminalNodes()).hasSize(2);
|
||||
assertThat(result.terminalNodes().get(0)).isInstanceOf(DirectiveNode.class);
|
||||
DirectiveNode dn1 = (DirectiveNode) result.terminalNodes().get(0);
|
||||
assertThat(dn1.getName()).isEqualTo("fake1");
|
||||
assertThat(dn1.getValue()).isEqualTo("value1");
|
||||
DirectiveNode dn2 = (DirectiveNode) result.terminalNodes().get(1);
|
||||
assertThat(dn2.getName()).isEqualTo("fake2");
|
||||
assertThat(dn2.getValue()).isEqualTo("value2");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CommandModel}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
class CommandModelTests extends AbstractParsingTests {
|
||||
|
||||
@Test
|
||||
void oneRootCommand() {
|
||||
register(ROOT1);
|
||||
CommandModel model = commandModel();
|
||||
|
||||
Map<String, Token> tokens = model.getValidRootTokens();
|
||||
assertThat(tokens).hasSize(1);
|
||||
assertThat(tokens.get("root1")).satisfies(token -> {
|
||||
assertThat(token).isNotNull();
|
||||
assertThat(token.getValue()).isEqualTo("root1");
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneRootCommandCaseInsensitive() {
|
||||
register(ROOT1_UP);
|
||||
ParserConfig configuration = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS);
|
||||
CommandModel model = commandModel(configuration);
|
||||
|
||||
Map<String, Token> tokens = model.getValidRootTokens();
|
||||
assertThat(tokens).hasSize(1);
|
||||
assertThat(tokens.get("root1")).satisfies(token -> {
|
||||
assertThat(token).isNotNull();
|
||||
assertThat(token.getValue()).isEqualTo("root1");
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void onePlainSubCommand() {
|
||||
register(ROOT2_SUB1);
|
||||
CommandModel model = commandModel();
|
||||
|
||||
Map<String, Token> tokens = model.getValidRootTokens();
|
||||
|
||||
assertThat(tokens).hasSize(1);
|
||||
assertThat(tokens.get("root2")).satisfies(token -> {
|
||||
assertThat(token).isNotNull();
|
||||
assertThat(token.getValue()).isEqualTo("root2");
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
|
||||
assertThat(model.getRootCommand("root2")).satisfies(root1 -> {
|
||||
assertThat(root1).isNotNull();
|
||||
assertThat(root1.getChildren()).hasSize(1);
|
||||
assertThat(root1.registration).isNull();
|
||||
assertThat(root1.getChildren()).satisfiesExactly(
|
||||
sub1 -> {
|
||||
assertThat(sub1).isNotNull();
|
||||
assertThat(sub1.getChildren()).isEmpty();
|
||||
assertThat(sub1.registration).isNotNull();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void onePlainSubCommand2() {
|
||||
register(ROOT2_SUB1);
|
||||
register(ROOT2_SUB2);
|
||||
CommandModel model = commandModel();
|
||||
|
||||
Map<String, Token> tokens = model.getValidRootTokens();
|
||||
|
||||
assertThat(tokens).hasSize(1);
|
||||
assertThat(tokens.get("root2")).satisfies(token -> {
|
||||
assertThat(token).isNotNull();
|
||||
assertThat(token.getValue()).isEqualTo("root2");
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
|
||||
assertThat(model.getRootCommand("root2")).satisfies(root1 -> {
|
||||
assertThat(root1.command).isEqualTo("root2");
|
||||
assertThat(root1.getChildren()).hasSize(2);
|
||||
assertThat(root1.registration).isNull();
|
||||
assertThat(root1.getValidTokens()).satisfies(map -> {
|
||||
assertThat(map).hasSize(2);
|
||||
assertThat(map).hasEntrySatisfying("sub1", token -> {
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
assertThat(map).hasEntrySatisfying("sub2", token -> {
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
});
|
||||
assertThat(root1.getChildren()).satisfiesExactlyInAnyOrder(
|
||||
sub1 -> {
|
||||
assertThat(sub1.command).isEqualTo("sub1");
|
||||
assertThat(sub1).isNotNull();
|
||||
assertThat(sub1.getChildren()).isEmpty();
|
||||
assertThat(sub1.registration).isNotNull();
|
||||
},
|
||||
sub2 -> {
|
||||
assertThat(sub2.command).isEqualTo("sub2");
|
||||
assertThat(sub2).isNotNull();
|
||||
assertThat(sub2.getChildren()).isEmpty();
|
||||
assertThat(sub2.registration).isNotNull();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void onePlainSubCommand3() {
|
||||
register(ROOT2_SUB1_SUB2);
|
||||
register(ROOT2_SUB1_SUB3);
|
||||
register(ROOT2_SUB1_SUB4);
|
||||
CommandModel model = commandModel();
|
||||
|
||||
Map<String, Token> tokens = model.getValidRootTokens();
|
||||
|
||||
assertThat(tokens).hasSize(1);
|
||||
assertThat(tokens.get("root2")).satisfies(token -> {
|
||||
assertThat(token).isNotNull();
|
||||
assertThat(token.getValue()).isEqualTo("root2");
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
|
||||
assertThat(model.getRootCommand("root2")).satisfies(root1 -> {
|
||||
assertThat(root1.command).isEqualTo("root2");
|
||||
assertThat(root1.getChildren()).hasSize(1);
|
||||
assertThat(root1.registration).isNull();
|
||||
assertThat(root1.getValidTokens()).satisfies(map -> {
|
||||
assertThat(map).hasSize(1);
|
||||
assertThat(map).hasEntrySatisfying("sub1", token -> {
|
||||
assertThat(token.getType()).isEqualTo(TokenType.COMMAND);
|
||||
});
|
||||
});
|
||||
assertThat(root1.getChildren()).satisfiesExactlyInAnyOrder(
|
||||
sub1 -> {
|
||||
assertThat(sub1.command).isEqualTo("sub1");
|
||||
assertThat(sub1.getChildren()).satisfiesExactlyInAnyOrder(
|
||||
sub2 -> {
|
||||
assertThat(sub2.getChildren()).isEmpty();
|
||||
},
|
||||
sub3 -> {
|
||||
assertThat(sub3.getChildren()).isEmpty();
|
||||
},
|
||||
sub4 -> {
|
||||
assertThat(sub4.getChildren()).isEmpty();
|
||||
}
|
||||
);
|
||||
// assertThat(sub1).isNotNull();
|
||||
// assertThat(sub1.getChildren()).isEmpty();
|
||||
// assertThat(sub1.registration).isNotNull();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveCommand() {
|
||||
register(ROOT2_SUB1);
|
||||
CommandModel model = commandModel();
|
||||
|
||||
assertThat(model.resolve(Arrays.asList("root2", "sub1"))).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveParentCommand() {
|
||||
register(ROOT2);
|
||||
register(ROOT2_SUB1_SUB2);
|
||||
CommandModel model = commandModel();
|
||||
|
||||
assertThat(model.resolve(Arrays.asList("root2"))).satisfies(
|
||||
info -> {
|
||||
assertThat(info.registration).isNotNull();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.parser.Lexer.LexerResult;
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for default implementation in a {@link Lexer}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
class LexerTests extends AbstractParsingTests {
|
||||
|
||||
@Nested
|
||||
class NoOptions {
|
||||
|
||||
@Test
|
||||
void rootCommandNoOptions() {
|
||||
register(ROOT1);
|
||||
List<Token> tokens = tokenize("root1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root1");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class CaseSensitivity {
|
||||
|
||||
@Test
|
||||
void commandRegUpperCommandLower() {
|
||||
register(ROOT1_UP);
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS);
|
||||
List<Token> tokens = tokenize(lexer(config), "root1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandRegLowerCommandUpper() {
|
||||
register(ROOT1);
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS);
|
||||
List<Token> tokens = tokenize(lexer(config), "Root1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("Root1");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// @Test
|
||||
// void commandWithCaseInsensitiveInArguments() {
|
||||
// register(ROOT1);
|
||||
// ParserConfig configuration = new ParserConfig()
|
||||
// .setOptionsCaseSensitive(false);
|
||||
// List<Token> tokens = tokenize(lexer(configuration), "ROOT1");
|
||||
|
||||
// assertThat(tokens).satisfiesExactly(
|
||||
// token -> {
|
||||
// ParserAssertions.assertThat(token)
|
||||
// .isType(TokenType.COMMAND)
|
||||
// .hasPosition(0)
|
||||
// .hasValue("ROOT1");
|
||||
// });
|
||||
// }
|
||||
|
||||
@Test
|
||||
void rootCommandWithChildGetsRootForRoot() {
|
||||
register(ROOT1);
|
||||
register(ROOT2_SUB1);
|
||||
List<Token> tokens = tokenize("root1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void subCommandLevel1WithoutRoot() {
|
||||
register(ROOT2_SUB1);
|
||||
List<Token> tokens = tokenize("root2", "sub1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root2");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(1)
|
||||
.hasValue("sub1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void subCommandLevel2WithoutRoot() {
|
||||
register(ROOT2_SUB1_SUB2);
|
||||
List<Token> tokens = tokenize("root2", "sub1", "sub2");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root2");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(1)
|
||||
.hasValue("sub1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(2)
|
||||
.hasValue("sub2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void subCommandLevel2WithoutRootWithOption() {
|
||||
register(ROOT2_SUB1_SUB2);
|
||||
List<Token> tokens = tokenize("root2", "sub1", "sub2", "--arg1", "xxx");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root2");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(1)
|
||||
.hasValue("sub1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(2)
|
||||
.hasValue("sub2");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(3)
|
||||
.hasValue("--arg1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.ARGUMENT)
|
||||
.hasPosition(4)
|
||||
.hasValue("xxx");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void definedOptionShouldBeOptionAfterCommand() {
|
||||
register(ROOT3);
|
||||
List<Token> tokens = tokenize("root3", "--arg1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root3");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(1)
|
||||
.hasValue("--arg1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void notDefinedOptionShouldBeOptionAfterCommand() {
|
||||
register(ROOT3);
|
||||
List<Token> tokens = tokenize("root3", "--arg2");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root3");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(1)
|
||||
.hasValue("--arg2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void notDefinedOptionShouldBeOptionAfterDefinedOption() {
|
||||
register(ROOT3);
|
||||
List<Token> tokens = tokenize("root3", "--arg1", "--arg2");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root3");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(1)
|
||||
.hasValue("--arg1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(2)
|
||||
.hasValue("--arg2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsWithoutValuesFromRoot() {
|
||||
register(ROOT5);
|
||||
List<Token> tokens = tokenize("root5", "--arg1", "--arg2");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root5");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(1)
|
||||
.hasValue("--arg1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(2)
|
||||
.hasValue("--arg2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsWithValuesFromRoot() {
|
||||
register(ROOT5);
|
||||
List<Token> tokens = tokenize("root5", "--arg1", "value1", "--arg2", "value2");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(0)
|
||||
.hasValue("root5");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(1)
|
||||
.hasValue("--arg1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.ARGUMENT)
|
||||
.hasPosition(2)
|
||||
.hasValue("value1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.OPTION)
|
||||
.hasPosition(3)
|
||||
.hasValue("--arg2");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.ARGUMENT)
|
||||
.hasPosition(4)
|
||||
.hasValue("value2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionValueFromRoot() {
|
||||
register(ROOT3);
|
||||
List<Token> tokens = tokenize("root3", "--arg1", "value1");
|
||||
|
||||
assertThat(tokens).extracting(Token::getType).containsExactly(TokenType.COMMAND, TokenType.OPTION,
|
||||
TokenType.ARGUMENT);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DoubleDash {
|
||||
|
||||
@Test
|
||||
void doubleDashAddsCommandArguments() {
|
||||
register(ROOT3);
|
||||
List<Token> tokens = tokenize("root3", "--", "arg1");
|
||||
|
||||
assertThat(tokens).extracting(Token::getType).containsExactly(TokenType.COMMAND, TokenType.DOUBLEDASH,
|
||||
TokenType.ARGUMENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandArgsWithoutOption() {
|
||||
register(ROOT3);
|
||||
List<Token> tokens = tokenize("root3", "value1", "value2");
|
||||
|
||||
assertThat(tokens).extracting(Token::getType).containsExactly(TokenType.COMMAND, TokenType.ARGUMENT,
|
||||
TokenType.ARGUMENT);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Directives {
|
||||
|
||||
@Test
|
||||
void directiveWithCommand() {
|
||||
register(ROOT1);
|
||||
ParserConfig config = new ParserConfig().enable(Feature.ALLOW_DIRECTIVES);
|
||||
List<Token> tokens = tokenize(lexer(config), "[fake]", "root1");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.DIRECTIVE)
|
||||
.hasPosition(0)
|
||||
.hasValue("fake");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.COMMAND)
|
||||
.hasPosition(1)
|
||||
.hasValue("root1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasOneDirective() {
|
||||
register(ROOT1);
|
||||
ParserConfig config = new ParserConfig().enable(Feature.ALLOW_DIRECTIVES);
|
||||
List<Token> tokens = tokenize(lexer(config), "[fake]");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.DIRECTIVE)
|
||||
.hasPosition(0)
|
||||
.hasValue("fake");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasMultipleDirectives() {
|
||||
register(ROOT1);
|
||||
ParserConfig config = new ParserConfig().enable(Feature.ALLOW_DIRECTIVES);
|
||||
List<Token> tokens = tokenize(lexer(config), "[fake1][fake2]");
|
||||
|
||||
assertThat(tokens).satisfiesExactly(
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.DIRECTIVE)
|
||||
.hasPosition(0)
|
||||
.hasValue("fake1");
|
||||
},
|
||||
token -> {
|
||||
ParserAssertions.assertThat(token)
|
||||
.isType(TokenType.DIRECTIVE)
|
||||
.hasPosition(0)
|
||||
.hasValue("fake2");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorIfDirectivesDisabledAndIgnoreDisabled() {
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.ALLOW_DIRECTIVES)
|
||||
.disable(Feature.IGNORE_DIRECTIVES);
|
||||
LexerResult result = tokenizeAsResult(lexer(config),"[fake]");
|
||||
assertThat(result.messageResults()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noErrorIfDirectivesDisabledAndIgnoreEnabled() {
|
||||
ParserConfig config = new ParserConfig()
|
||||
.enable(Feature.IGNORE_DIRECTIVES);
|
||||
LexerResult result = tokenizeAsResult(lexer(config), "[fake]");
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoreDirectiveIfDisabled() {
|
||||
register(ROOT1);
|
||||
List<Token> tokens = tokenize("[fake]", "root1");
|
||||
|
||||
assertThat(tokens).extracting(Token::getType).containsExactly(TokenType.COMMAND);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ParserMessages {
|
||||
|
||||
@Test
|
||||
void hasErrorMessageWithoutArguments() {
|
||||
LexerResult result = tokenizeAsResult("--");
|
||||
assertThat(result.messageResults()).satisfiesExactly(
|
||||
message -> {
|
||||
assertThat(message.parserMessage().getCode()).isEqualTo(1000);
|
||||
assertThat(message.position()).isEqualTo(0);
|
||||
},
|
||||
message -> {
|
||||
assertThat(message.parserMessage().getCode()).isEqualTo(1000);
|
||||
assertThat(message.position()).isEqualTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
public class ParserAssertions {
|
||||
|
||||
public static TokenAssert assertThat(Token actual) {
|
||||
return new TokenAssert(actual);
|
||||
}
|
||||
|
||||
public static ParserMessageAssert assertThat(ParserMessage message) {
|
||||
return new ParserMessageAssert(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ParserConfigTests {
|
||||
|
||||
@Test
|
||||
void hasDefaultEnabled() {
|
||||
ParserConfig config = new ParserConfig();
|
||||
assertThat(config.isEnabled(Feature.ALLOW_DIRECTIVES)).isFalse();
|
||||
assertThat(config.isEnabled(Feature.IGNORE_DIRECTIVES)).isFalse();
|
||||
assertThat(config.isEnabled(Feature.CASE_SENSITIVE_COMMANDS)).isTrue();
|
||||
assertThat(config.isEnabled(Feature.CASE_SENSITIVE_OPTIONS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureStates() {
|
||||
ParserConfig config = new ParserConfig();
|
||||
config.configure(Feature.ALLOW_DIRECTIVES, true);
|
||||
assertThat(config.isEnabled(Feature.ALLOW_DIRECTIVES)).isTrue();
|
||||
config.disable(Feature.ALLOW_DIRECTIVES);
|
||||
assertThat(config.isEnabled(Feature.ALLOW_DIRECTIVES)).isFalse();
|
||||
config.enable(Feature.ALLOW_DIRECTIVES);
|
||||
assertThat(config.isEnabled(Feature.ALLOW_DIRECTIVES)).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
|
||||
import org.springframework.shell.command.parser.ParserMessage.Type;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ParserMessageAssert extends AbstractAssert<ParserMessageAssert, ParserMessage> {
|
||||
|
||||
public ParserMessageAssert(ParserMessage actual) {
|
||||
super(actual, ParserMessageAssert.class);
|
||||
}
|
||||
|
||||
public ParserMessageAssert hasCode(int code) {
|
||||
isNotNull();
|
||||
assertThat(actual.getCode()).isEqualTo(code);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParserMessageAssert hasType(Type type) {
|
||||
isNotNull();
|
||||
assertThat(actual.getType()).isEqualTo(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.parser.Parser.ParseResult;
|
||||
import org.springframework.shell.command.parser.ParserConfig.Feature;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for default implementations in/with a {@link Parser}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
class ParserTests extends AbstractParsingTests {
|
||||
|
||||
@Test
|
||||
void shouldFindRegistration() {
|
||||
register(ROOT1);
|
||||
ParseResult result = parse("root1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CommandArguments {
|
||||
|
||||
@Test
|
||||
void commandArgumentsGetsAddedWhenNoOptions() {
|
||||
register(ROOT1);
|
||||
ParseResult result = parse("root1", "arg1", "arg2");
|
||||
|
||||
assertThat(result.argumentResults()).satisfiesExactly(
|
||||
ar -> {
|
||||
assertThat(ar.value()).isEqualTo("arg1");
|
||||
assertThat(ar.position()).isEqualTo(0);
|
||||
},
|
||||
ar -> {
|
||||
assertThat(ar.value()).isEqualTo("arg2");
|
||||
assertThat(ar.position()).isEqualTo(1);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandArgumentsGetsAddedAfterDoubleDash() {
|
||||
register(ROOT3);
|
||||
ParseResult result = parse("root3", "--arg1", "value1", "--", "arg1", "arg2");
|
||||
|
||||
assertThat(result.argumentResults()).satisfiesExactly(
|
||||
ar -> {
|
||||
assertThat(ar.value()).isEqualTo("arg1");
|
||||
assertThat(ar.position()).isEqualTo(0);
|
||||
},
|
||||
ar -> {
|
||||
assertThat(ar.value()).isEqualTo("arg2");
|
||||
assertThat(ar.position()).isEqualTo(1);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TypeConversions {
|
||||
|
||||
@Test
|
||||
void optionValueShouldBeInteger() {
|
||||
register(ROOT6_OPTION_INT);
|
||||
ParseResult result = parse("root6", "--arg1", "1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.optionResults().get(0).value()).isEqualTo(1);
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionValueShouldBeIntegerArray() {
|
||||
register(ROOT6_OPTION_INTARRAY);
|
||||
ParseResult result = parse("root6", "--arg1", "1", "2");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.optionResults().get(0).value()).isEqualTo(new int[] { 1, 2 });
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionValueFailsFromStringToInteger() {
|
||||
register(ROOT6_OPTION_INT);
|
||||
ParseResult result = parse("root6", "--arg1", "x");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.optionResults().get(0).value()).isEqualTo("x");
|
||||
assertThat(result.messageResults()).isNotEmpty();
|
||||
assertThat(result.messageResults()).satisfiesExactly(ms -> {
|
||||
// "2002E:(pos 0): Illegal option value 'x', reason 'Failed to convert from type [java.lang.String] to type [int] for value 'x''"
|
||||
assertThat(ms.getMessage()).contains("Failed to convert");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ParserMessages {
|
||||
|
||||
@Test
|
||||
void shouldHaveErrorResult() {
|
||||
register(ROOT4);
|
||||
ParseResult result = parse("root4");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.messageResults()).satisfiesExactly(message -> {
|
||||
ParserAssertions.assertThat(message.parserMessage()).hasCode(2000).hasType(ParserMessage.Type.ERROR);
|
||||
});
|
||||
// "100E:(pos 0): Missing option, longnames='arg1', shortnames=''"
|
||||
assertThat(result.messageResults().get(0).getMessage()).contains("Missing mandatory option", "arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHaveErrorResult2() {
|
||||
register(ROOT4);
|
||||
// ParseResult result = parse("root4", "--arg1", "value1", "--arg2", "value2");
|
||||
ParseResult result = parse("root4", "--arg1", "--arg2");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.messageResults()).satisfiesExactly(message -> {
|
||||
ParserAssertions.assertThat(message.parserMessage()).hasCode(2001).hasType(ParserMessage.Type.ERROR);
|
||||
});
|
||||
// "101E:(pos 0): Unrecognised option '--arg2'"
|
||||
// assertThat(result.messageResults().get(0).getMessage()).contains("xxx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void lexerMessageShouldGetPropagated() {
|
||||
ParseResult parse = parse("--");
|
||||
assertThat(parse.messageResults()).extracting(ms -> ms.parserMessage().getCode()).containsExactly(1000, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Directives {
|
||||
|
||||
@Test
|
||||
void directiveNoValueWithCommand() {
|
||||
register(ROOT3);
|
||||
ParserConfig config = new ParserConfig().enable(Feature.ALLOW_DIRECTIVES);
|
||||
|
||||
ParseResult result = parse(lexer(config), "[fake]", "root3");
|
||||
assertThat(result.directiveResults()).satisfiesExactly(d -> {
|
||||
assertThat(d.name()).isEqualTo("fake");
|
||||
assertThat(d.value()).isNull();
|
||||
});
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void directiveWithValueWithCommand() {
|
||||
register(ROOT3);
|
||||
ParserConfig config = new ParserConfig().enable(Feature.ALLOW_DIRECTIVES);
|
||||
|
||||
ParseResult result = parse(lexer(config), "[fake:value]", "root3");
|
||||
assertThat(result.directiveResults()).satisfiesExactly(d -> {
|
||||
assertThat(d.name()).isEqualTo("fake");
|
||||
assertThat(d.value()).isEqualTo("value");
|
||||
});
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleDirectivesWithCommand() {
|
||||
register(ROOT3);
|
||||
ParserConfig config = new ParserConfig().enable(Feature.ALLOW_DIRECTIVES);
|
||||
|
||||
ParseResult result = parse(lexer(config), "[foo][bar:value]", "root3");
|
||||
assertThat(result.directiveResults()).satisfiesExactly(
|
||||
d -> {
|
||||
assertThat(d.name()).isEqualTo("foo");
|
||||
assertThat(d.value()).isNull();
|
||||
},
|
||||
d -> {
|
||||
assertThat(d.name()).isEqualTo("bar");
|
||||
assertThat(d.value()).isEqualTo("value");
|
||||
});
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class LongOptions {
|
||||
|
||||
@Test
|
||||
void shouldFindLongOption() {
|
||||
register(ROOT3);
|
||||
ParseResult result = parse("root3", "--arg1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindLongOptionTwoCommands() {
|
||||
register(ROOT2_SUB1);
|
||||
ParseResult result = parse("root2", "sub1", "--arg1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindLongOptionThreeCommands() {
|
||||
register(ROOT2_SUB1_SUB2);
|
||||
ParseResult result = parse("root2", "sub1", "sub2", "--arg1", "xxx");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindLongOptionArgument() {
|
||||
register(ROOT3);
|
||||
ParseResult result = parse("root3", "--arg1", "value1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.optionResults()).satisfiesExactly(
|
||||
r -> {
|
||||
assertThat(r.value()).isEqualTo("value1");
|
||||
}
|
||||
);
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ShortOptions {
|
||||
|
||||
@Test
|
||||
void shouldFindShortOption() {
|
||||
register(ROOT3_SHORT_OPTION_A);
|
||||
ParseResult result = parse("root3", "-a");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CaseSensitivity {
|
||||
|
||||
@Test
|
||||
void shouldFindRegistrationRegUpperCommandLower() {
|
||||
register(ROOT1_UP);
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS);
|
||||
ParseResult result = parse(config, "root1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindRegistrationRegUpperCommandUpper() {
|
||||
register(ROOT1_UP);
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS);
|
||||
ParseResult result = parse(config, "Root1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindRegistrationRegLowerCommandUpper() {
|
||||
register(ROOT1);
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS);
|
||||
ParseResult result = parse(config, "Root1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindLongOptionRegLowerCommandUpper() {
|
||||
register(ROOT3);
|
||||
ParserConfig config = new ParserConfig()
|
||||
.disable(Feature.CASE_SENSITIVE_COMMANDS)
|
||||
.disable(Feature.CASE_SENSITIVE_OPTIONS)
|
||||
;
|
||||
ParseResult result = parse(config, "root3", "--Arg1", "value1");
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.commandRegistration()).isNotNull();
|
||||
assertThat(result.optionResults()).isNotEmpty();
|
||||
assertThat(result.optionResults()).satisfiesExactly(
|
||||
r -> {
|
||||
assertThat(r.value()).isEqualTo("value1");
|
||||
}
|
||||
);
|
||||
assertThat(result.messageResults()).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DefaultValues {
|
||||
|
||||
@Test
|
||||
void shouldAddOptionIfItHasDefaultValue() {
|
||||
register(ROOT6_OPTION_DEFAULT_VALUE);
|
||||
ParseResult result = parse("root6");
|
||||
assertThat(result.optionResults()).satisfiesExactly(
|
||||
r -> {
|
||||
assertThat(r.value()).isEqualTo("defaultvalue");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.command.parser;
|
||||
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TokenAssert extends AbstractAssert<TokenAssert, Token> {
|
||||
|
||||
public TokenAssert(Token actual) {
|
||||
super(actual, TokenAssert.class);
|
||||
}
|
||||
|
||||
public TokenAssert isType(TokenType type) {
|
||||
isNotNull();
|
||||
assertThat(actual.getType()).isEqualTo(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenAssert hasPosition(int position) {
|
||||
isNotNull();
|
||||
assertThat(actual.getPosition()).isEqualTo(position);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenAssert hasValue(String value) {
|
||||
isNotNull();
|
||||
assertThat(actual.getValue()).isEqualTo(value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import java.util.Set;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
@@ -107,6 +109,102 @@ public class OptionTypeCommands {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(command = BaseE2ECommands.ANNO, group = BaseE2ECommands.GROUP)
|
||||
public static class Annotation extends BaseE2ECommands {
|
||||
|
||||
@Command(command = "option-type-string")
|
||||
public String optionTypeStringAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
String arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@Command(command = "option-type-boolean")
|
||||
public String optionTypeBooleanAnnotation(
|
||||
@ShellOption()
|
||||
@Option(longNames = "arg1")
|
||||
boolean arg1,
|
||||
@ShellOption(defaultValue = "true")
|
||||
@Option(longNames = "arg2", defaultValue = "true")
|
||||
boolean arg2,
|
||||
@ShellOption(defaultValue = "false")
|
||||
@Option(longNames = "arg3", defaultValue = "false")
|
||||
boolean arg3,
|
||||
@ShellOption()
|
||||
@Option(longNames = "arg4")
|
||||
Boolean arg4,
|
||||
@ShellOption(defaultValue = "true")
|
||||
@Option(longNames = "arg5", defaultValue = "true")
|
||||
Boolean arg5,
|
||||
@ShellOption(defaultValue = "false")
|
||||
@Option(longNames = "arg6", defaultValue = "false")
|
||||
Boolean arg6
|
||||
) {
|
||||
return String.format("Hello arg1=%s arg2=%s arg3=%s arg4=%s arg5=%s arg6=%s", arg1, arg2, arg3, arg4, arg5,
|
||||
arg6);
|
||||
}
|
||||
|
||||
@Command(command = "option-type-integer")
|
||||
public String optionTypeIntegerAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
int arg1,
|
||||
@Option(longNames = "arg2")
|
||||
Integer arg2
|
||||
) {
|
||||
return String.format("Hello '%s' '%s'", arg1, arg2);
|
||||
}
|
||||
|
||||
@Command(command = "option-type-enum")
|
||||
public String optionTypeEnumAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
OptionTypeEnum arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@Command(command = "option-type-string-array")
|
||||
public String optionTypeStringArrayAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
String[] arg1
|
||||
) {
|
||||
return "Hello " + stringOfStrings(arg1);
|
||||
}
|
||||
|
||||
@Command(command = "option-type-int-array")
|
||||
public String optionTypeIntArrayAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
int[] arg1
|
||||
) {
|
||||
return "Hello " + stringOfInts(arg1);
|
||||
}
|
||||
|
||||
@Command(command = "option-type-string-list")
|
||||
public String optionTypeStringListAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
List<String> arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@Command(command = "option-type-string-set")
|
||||
public String optionTypeStringSetAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
Set<String> arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@Command(command = "option-type-string-collection")
|
||||
public String optionTypeStringCollectionAnnotation(
|
||||
@Option(longNames = "arg1")
|
||||
Collection<String> arg1
|
||||
) {
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
public static class Registration extends BaseE2ECommands {
|
||||
|
||||
|
||||
@@ -17,24 +17,27 @@ package org.springframework.shell.samples.e2e;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
|
||||
import org.springframework.shell.command.annotation.EnableCommand;
|
||||
import org.springframework.shell.samples.AbstractSampleTests;
|
||||
import org.springframework.shell.samples.e2e.OptionTypeCommands.Annotation;
|
||||
import org.springframework.shell.samples.e2e.OptionTypeCommands.LegacyAnnotation;
|
||||
import org.springframework.shell.samples.e2e.OptionTypeCommands.Registration;
|
||||
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
@ContextConfiguration(classes = { LegacyAnnotation.class, Registration.class })
|
||||
@EnableCommand(Annotation.class)
|
||||
class OptionTypeCommandsTests extends AbstractSampleTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-string --arg1 hi", annox = false)
|
||||
@E2ESource(command = "option-type-string --arg1 hi")
|
||||
void optionTypeString(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello hi");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-boolean", annox = false, reg = false)
|
||||
@E2ESource(command = "option-type-boolean", reg = false)
|
||||
void optionTypeBooleanWithAnno(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello arg1=false arg2=true arg3=false arg4=false arg5=true arg6=false");
|
||||
@@ -48,49 +51,49 @@ class OptionTypeCommandsTests extends AbstractSampleTests {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-integer --arg1 1 --arg2 2", annox = false)
|
||||
@E2ESource(command = "option-type-integer --arg1 1 --arg2 2")
|
||||
void optionTypeInteger(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello '1' '2'");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-enum --arg1 ONE", annox = false)
|
||||
@E2ESource(command = "option-type-enum --arg1 ONE")
|
||||
void optionTypeEnum(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello ONE");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-string-array --arg1 one two", annox = false)
|
||||
@E2ESource(command = "option-type-string-array --arg1 one two")
|
||||
void optionTypeStringArray(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello [one,two]");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-int-array --arg1 1 2", annox = false)
|
||||
@E2ESource(command = "option-type-int-array --arg1 1 2")
|
||||
void optionTypeIntArray(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello [1,2]");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-string-list --arg1 one two", annox = false)
|
||||
@E2ESource(command = "option-type-string-list --arg1 one two")
|
||||
void optionTypeStringList(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello [one, two]");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-string-set --arg1 one two", annox = false)
|
||||
@E2ESource(command = "option-type-string-set --arg1 one two")
|
||||
void optionTypeStringSet(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello [one, two]");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@E2ESource(command = "option-type-string-collection --arg1 one two", annox = false)
|
||||
@E2ESource(command = "option-type-string-collection --arg1 one two")
|
||||
void optionTypeStringCollection(String command, boolean interactive) {
|
||||
BaseShellSession<?> session = createSession(command, interactive);
|
||||
assertScreenContainsText(session, "Hello [one, two]");
|
||||
|
||||
@@ -162,9 +162,6 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
else if (ClassUtils.isAssignable(Boolean.class, parameterType)) {
|
||||
optionSpec.arity(OptionArity.ZERO);
|
||||
}
|
||||
else {
|
||||
optionSpec.arity(OptionArity.EXACTLY_ONE);
|
||||
}
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(so.defaultValue(), ShellOption.NONE)
|
||||
&& !ObjectUtils.nullSafeEquals(so.defaultValue(), ShellOption.NULL)) {
|
||||
|
||||
Reference in New Issue
Block a user