Add short options into valid tokens

- Add short options as valid tokens in a `CommandModel` so that Lexer
  create tokens with accurate info and doesn't then cascade this issue
  in Ast and Parser.
- Previously with a command `command -a aaa -b bbb` tokenisation resulted
  `COMMAND OPTION ARGUMENT ARGUMENT ARGUMENT` with multiple short options
  while it should have been `COMMAND OPTION ARGUMENT OPTION ARGUMENT`.
- Relates #757
This commit is contained in:
Janne Valkealahti
2023-05-31 17:28:21 +01:00
parent 8cba636fdb
commit c125c46010
5 changed files with 276 additions and 0 deletions

View File

@@ -196,6 +196,9 @@ public class CommandModel {
for (String longName : commandOption.getLongNames()) {
tokens.put("--" + longName, new Token(longName, TokenType.OPTION));
}
for (Character shortName : commandOption.getShortNames()) {
tokens.put("-" + shortName, new Token(shortName.toString(), TokenType.OPTION));
}
});
}
return tokens;

View File

@@ -110,6 +110,19 @@ abstract class AbstractParsingTests {
.and()
.build();
static final CommandRegistration ROOT3_OPTION_ARG1_ARG2 = CommandRegistration.builder()
.command("root3")
.withOption()
.longNames("arg1")
.and()
.withOption()
.longNames("arg2")
.and()
.withTarget()
.consumer(ctx -> {})
.and()
.build();
static final CommandRegistration ROOT3_SHORT_OPTION_A = CommandRegistration.builder()
.command("root3")
.withOption()
@@ -120,6 +133,34 @@ abstract class AbstractParsingTests {
.and()
.build();
static final CommandRegistration ROOT3_SHORT_OPTION_A_B = CommandRegistration.builder()
.command("root3")
.withOption()
.shortNames('a')
.and()
.withOption()
.shortNames('b')
.and()
.withTarget()
.consumer(ctx -> {})
.and()
.build();
static final CommandRegistration ROOT3_SHORT_OPTION_A_B_REQUIRED = CommandRegistration.builder()
.command("root3")
.withOption()
.shortNames('a')
.required()
.and()
.withOption()
.required()
.shortNames('b')
.and()
.withTarget()
.consumer(ctx -> {})
.and()
.build();
static final CommandRegistration ROOT4 = CommandRegistration.builder()
.command("root4")
.withOption()

View File

@@ -137,6 +137,102 @@ class AstTests extends AbstractParsingTests {
});
}
@Test
void createsOptionNodesWithTwoOptionArg() {
register(ROOT3);
Token root3 = token("root3", TokenType.COMMAND, 0);
Token arg1 = token("--arg1", TokenType.OPTION, 1);
Token value1 = token("value1", TokenType.ARGUMENT, 2);
Token arg2 = token("--arg2", TokenType.OPTION, 3);
Token value2 = token("value2", TokenType.ARGUMENT, 4);
AstResult result = ast(root3, arg1, value1, arg2, value2);
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");
OptionNode on1 = (OptionNode) cn.getChildren().get(0);
assertThat(on1.getChildren()).hasSize(1);
OptionArgumentNode oan1 = (OptionArgumentNode) on1.getChildren().get(0);
assertThat(oan1.getValue()).isEqualTo("value1");
OptionNode on2 = (OptionNode) cn.getChildren().get(1);
assertThat(on2.getChildren()).hasSize(1);
OptionArgumentNode oan2 = (OptionArgumentNode) on2.getChildren().get(0);
assertThat(oan2.getValue()).isEqualTo("value2");
});
}
@Test
void createsOptionNodeWithShortOptionArg() {
register(ROOT3_SHORT_OPTION_A);
Token root3 = token("root3", TokenType.COMMAND, 0);
Token arg1 = token("-a", 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("-a");
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 createsOptionNodesWithTwoShortOptionArg() {
register(ROOT3_SHORT_OPTION_A_B);
Token root3 = token("root3", TokenType.COMMAND, 0);
Token arg1 = token("-a", TokenType.OPTION, 1);
Token value1 = token("value1", TokenType.ARGUMENT, 2);
Token arg2 = token("-b", TokenType.OPTION, 3);
Token value2 = token("value2", TokenType.ARGUMENT, 4);
AstResult result = ast(root3, arg1, value1, arg2, value2);
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("-a", "-b");
OptionNode on1 = (OptionNode) cn.getChildren().get(0);
assertThat(on1.getChildren()).hasSize(1);
OptionArgumentNode oan1 = (OptionArgumentNode) on1.getChildren().get(0);
assertThat(oan1.getValue()).isEqualTo("value1");
OptionNode on2 = (OptionNode) cn.getChildren().get(1);
assertThat(on2.getChildren()).hasSize(1);
OptionArgumentNode oan2 = (OptionArgumentNode) on2.getChildren().get(0);
assertThat(oan2.getValue()).isEqualTo("value2");
});
}
@Test
void createOptionNodesWhenNoOptionArguments() {
register(ROOT3);

View File

@@ -336,6 +336,70 @@ class LexerTests extends AbstractParsingTests {
});
}
@Test
void shortOptionWithValuesFromRoot() {
register(ROOT3_SHORT_OPTION_A);
List<Token> tokens = tokenize("root3", "-a", "value1");
assertThat(tokens).satisfiesExactly(
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.COMMAND)
.hasPosition(0)
.hasValue("root3");
},
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.OPTION)
.hasPosition(1)
.hasValue("-a");
},
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.ARGUMENT)
.hasPosition(2)
.hasValue("value1");
});
}
@Test
void shortOptionsWithValuesFromRoot() {
register(ROOT3_SHORT_OPTION_A_B);
List<Token> tokens = tokenize("root3", "-a", "value1", "-b", "value2");
assertThat(tokens).satisfiesExactly(
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.COMMAND)
.hasPosition(0)
.hasValue("root3");
},
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.OPTION)
.hasPosition(1)
.hasValue("-a");
},
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.ARGUMENT)
.hasPosition(2)
.hasValue("value1");
},
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.OPTION)
.hasPosition(3)
.hasValue("-b");
},
token -> {
ParserAssertions.assertThat(token)
.isType(TokenType.ARGUMENT)
.hasPosition(4)
.hasValue("value2");
});
}
@Test
void optionValueFromRoot() {
register(ROOT3);

View File

@@ -251,6 +251,26 @@ class ParserTests extends AbstractParsingTests {
);
assertThat(result.messageResults()).isEmpty();
}
@Test
void shouldFindTwoLongOptionArgument() {
register(ROOT3_OPTION_ARG1_ARG2);
ParseResult result = parse("root3", "--arg1", "value1", "--arg2", "value2");
assertThat(result).isNotNull();
assertThat(result.commandRegistration()).isNotNull();
assertThat(result.optionResults()).isNotEmpty();
assertThat(result.optionResults()).satisfiesExactly(
r -> {
assertThat(r.option().getLongNames()).isEqualTo(new String[] { "arg1" });
assertThat(r.value()).isEqualTo("value1");
},
r -> {
assertThat(r.option().getLongNames()).isEqualTo(new String[] { "arg2" });
assertThat(r.value()).isEqualTo("value2");
}
);
assertThat(result.messageResults()).isEmpty();
}
}
@Nested
@@ -263,6 +283,58 @@ class ParserTests extends AbstractParsingTests {
assertThat(result).isNotNull();
assertThat(result.commandRegistration()).isNotNull();
assertThat(result.optionResults()).isNotEmpty();
assertThat(result.optionResults()).satisfiesExactly(
r -> {
assertThat(r.option().getShortNames()).isEqualTo(new Character[] { 'a' });
assertThat(r.value()).isNull();
}
);
assertThat(result.messageResults()).isEmpty();
}
@Test
void shouldFindShortOptionWithArg() {
register(ROOT3_SHORT_OPTION_A);
ParseResult result = parse("root3", "-a", "aaa");
assertThat(result).isNotNull();
assertThat(result.commandRegistration()).isNotNull();
assertThat(result.optionResults()).isNotEmpty();
assertThat(result.optionResults()).satisfiesExactly(
r -> {
assertThat(r.option().getShortNames()).isEqualTo(new Character[] { 'a' });
assertThat(r.value()).isEqualTo("aaa");
}
);
assertThat(result.messageResults()).isEmpty();
}
@Test
void shouldFindShortOptions() {
register(ROOT3_SHORT_OPTION_A_B);
ParseResult result = parse("root3", "-a", "aaa", "-b", "bbb");
assertThat(result).isNotNull();
assertThat(result.commandRegistration()).isNotNull();
assertThat(result.optionResults()).isNotEmpty();
assertThat(result.optionResults()).satisfiesExactly(
r -> {
assertThat(r.option().getShortNames()).isEqualTo(new Character[] { 'a' });
assertThat(r.value()).isEqualTo("aaa");
},
r -> {
assertThat(r.option().getShortNames()).isEqualTo(new Character[] { 'b' });
assertThat(r.value()).isEqualTo("bbb");
}
);
assertThat(result.messageResults()).isEmpty();
}
@Test
void shouldFindShortOptionsRequired() {
register(ROOT3_SHORT_OPTION_A_B_REQUIRED);
ParseResult result = parse("root3", "-a", "aaa", "-b", "bbb");
assertThat(result).isNotNull();
assertThat(result.commandRegistration()).isNotNull();
assertThat(result.optionResults()).isNotEmpty();
assertThat(result.messageResults()).isEmpty();
}