Date: Fri, 3 Nov 2023 03:11:39 +0100
Subject: [PATCH 25/33] fix: argument onCorrect callbacks not executing other
arguments with allowUnique present (and where used). fix: incorrect usage
count errors appearing for each usage. now just appearing once, at final
parsing or the argument. fix: possible to set no names on arguments/commands.
refactor: instances where code could be simplified by using a more functional
styled approach. fix: parsing occurring even if tokenizing process failed.
refactor: ArgumentParser#into methods. fix: ArgumentType#addError throwing
IllegalStateException instead of ArgumentTypeException. refactor: simplify
initialization of arrays. javadoc: tweak some comments. refactor: change all
occurrences of Arrays.stream to Stream.of. refactor: remove unnecessary error
filtering for parsing errors at ErrorHandler#handleErrors. refactor:
ParseError to display highlighted tokens inside tuples in a non-hacky way.
refactor: Parser#getArgumentByPositionalIndex having embarrassing code
refactor: Pretty#generateTokensView completely to be more readable and easier
to maintain. feat: Pretty#generateTokensView now shows two arrows wrapping
the tokens to highlight if TextFormatter#enableSequences is false. feat: add
TextFormatter#removeFormat fix: some terminals not displaying terminal
sequences after a newline is added. (TextFormatter) fix:
UtlReflection#instantiate not properly forwarding the arguments supplied to
the ctor. refactor: several small improvements and optimizations
---
src/main/java/lanat/Argument.java | 51 +++---
src/main/java/lanat/ArgumentParser.java | 153 ++++++++++--------
src/main/java/lanat/ArgumentType.java | 8 +-
src/main/java/lanat/Command.java | 18 ++-
src/main/java/lanat/CommandTemplate.java | 3 +-
src/main/java/lanat/ErrorFormatter.java | 14 +-
.../lanat/argumentTypes/EnumArgumentType.java | 4 +-
.../argumentTypes/TryParseArgumentType.java | 29 ++--
.../errorFormatterGenerators/Pretty.java | 50 ++++--
src/main/java/lanat/parsing/Parser.java | 41 +++--
.../java/lanat/parsing/ParsingStateBase.java | 2 +-
src/main/java/lanat/parsing/Tokenizer.java | 18 ++-
.../lanat/parsing/errors/ErrorHandler.java | 2 +-
.../java/lanat/parsing/errors/ParseError.java | 48 +++---
src/main/java/lanat/utils/Range.java | 4 +
src/main/java/lanat/utils/UtlReflection.java | 10 +-
src/main/java/lanat/utils/UtlString.java | 4 +-
.../lanat/utils/displayFormatter/Color.java | 4 +-
.../utils/displayFormatter/TextFormatter.java | 87 ++++++----
.../lanat/test/exampleTests/ExampleTest.java | 37 +++--
.../lanat/test/units/TestTerminalOutput.java | 15 +-
21 files changed, 354 insertions(+), 248 deletions(-)
diff --git a/src/main/java/lanat/Argument.java b/src/main/java/lanat/Argument.java
index b40f2cfd..63150cdd 100644
--- a/src/main/java/lanat/Argument.java
+++ b/src/main/java/lanat/Argument.java
@@ -15,10 +15,10 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
+import java.util.stream.Stream;
/**
@@ -276,7 +276,10 @@ public void setDefaultValue(@Nullable TInner value) {
*/
@Override
public void addNames(@NotNull String... names) {
- Arrays.stream(names)
+ if (names.length == 0)
+ throw new IllegalArgumentException("at least one name must be specified");
+
+ Stream.of(names)
.map(UtlString::requireValidName)
.peek(n -> {
if (this.names.contains(n))
@@ -402,16 +405,6 @@ public boolean isHelpArgument() {
* @param values The value array that should be parsed.
*/
public void parseValues(short tokenIndex, @NotNull String... values) {
- // check if the argument was used more times than it should
- if (++this.argType.usageCount > this.argType.getRequiredUsageCount().end()) {
- this.parentCommand.getParser()
- .addError(
- ParseError.ParseErrorType.ARG_INCORRECT_USAGES_COUNT,
- this, values.length, this.argType.getLastTokenIndex() + 1
- );
- return;
- }
-
this.argType.parseAndUpdateValue(tokenIndex, values);
}
@@ -444,19 +437,30 @@ public void parseValues(short tokenIndex, @NotNull String... values) {
* @return {@code true} if the argument was used the correct amount of times.
*/
private boolean finishParsing$checkUsageCount() {
- if (this.getUsageCount() == 0) {
- if (this.required && !this.parentCommand.uniqueArgumentReceivedValue()) {
+ final var usageCount = this.getUsageCount();
+
+ if (usageCount == 0) {
+ if (this.required && !this.parentCommand.uniqueArgumentReceivedValue(this)) {
this.parentCommand.getParser().addError(
+ // just show it at the end. doesnt really matter
ParseError.ParseErrorType.REQUIRED_ARGUMENT_NOT_USED, this, 0
);
- return false;
}
- // make sure that the argument was used the minimum amount of times specified
- } else if (this.argType.usageCount < this.argType.getRequiredUsageCount().start()) {
+ return false;
+ }
+
+ // make sure that the argument was used the minimum number of times specified
+ if (!this.argType.getRequiredUsageCount().isInRangeInclusive(usageCount)) {
this.parentCommand.getParser()
- .addError(ParseError.ParseErrorType.ARG_INCORRECT_USAGES_COUNT, this, 0);
+ .addError(
+ ParseError.ParseErrorType.ARG_INCORRECT_USAGES_COUNT,
+ this,
+ this.argType.getLastReceivedValuesNum(),
+ this.argType.getLastTokenIndex()
+ );
return false;
}
+
return true;
}
@@ -477,7 +481,8 @@ public void parseValues(short tokenIndex, @NotNull String... values) {
new ParseError(
ParseError.ParseErrorType.MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED,
this.argType.getLastTokenIndex(),
- this, this.argType.getLastReceivedValuesNum()
+ this,
+ this.argType.getLastReceivedValuesNum()
)
{{
this.setArgumentGroup(exclusivityResult);
@@ -513,6 +518,13 @@ public boolean checkMatch(char name) {
return this.hasName(Character.toString(name));
}
+ /**
+ * Executes the correct or the error callback depending on whether the argument has errors or not.
+ *
+ * The correct callback is only executed if the argument has no errors, the usage count is greater than 0, the
+ *
+ * @param okValue the value to pass to the correct callback
+ */
// no worries about casting here, it will always receive the correct type
@SuppressWarnings("unchecked")
void invokeCallbacks(@Nullable Object okValue) {
@@ -525,7 +537,6 @@ void invokeCallbacks(@Nullable Object okValue) {
if (okValue == null
|| this.onCorrectCallback == null
|| this.getUsageCount() == 0
- || (!this.allowUnique && this.parentCommand.uniqueArgumentReceivedValue())
|| !this.parentCommand.shouldExecuteCorrectCallback()
) return;
diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java
index 25ff4314..8d2f45ee 100644
--- a/src/main/java/lanat/ArgumentParser.java
+++ b/src/main/java/lanat/ArgumentParser.java
@@ -12,7 +12,6 @@
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
-import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@@ -151,18 +150,16 @@ public static ArgumentParser from(@NotNull Class extends CommandTemplate> temp
@SuppressWarnings("unchecked")
private static
void from$setCommands(@NotNull Class templateClass, @NotNull Command parentCommand) {
- final var commandDefs = Arrays.stream(templateClass.getDeclaredClasses())
+ Stream.of(templateClass.getDeclaredClasses())
.filter(c -> c.isAnnotationPresent(Command.Define.class))
.filter(c -> Modifier.isStatic(c.getModifiers()))
.filter(CommandTemplate.class::isAssignableFrom)
.map(c -> (Class extends CommandTemplate>)c)
- .toList();
-
- for (var commandDef : commandDefs) {
- var command = new Command(commandDef);
- parentCommand.addCommand(command);
- ArgumentParser.from$setCommands(commandDef, command);
- }
+ .forEach(cmdDef -> {
+ var command = new Command(cmdDef);
+ parentCommand.addCommand(command);
+ ArgumentParser.from$setCommands(cmdDef, command);
+ });
}
@@ -181,10 +178,14 @@ public static ArgumentParser from(@NotNull Class extends CommandTemplate> temp
// pass the properties of this Sub-Command to its children recursively (most of the time this is what the user will want)
this.passPropertiesToChildren();
this.tokenize(input.args); // first. This will tokenize all Sub-Commands recursively
+
var errorHandler = new ErrorHandler(this);
- this.parseTokens(); // same thing, this parses all the stuff recursively
- this.invokeCallbacks();
+ // do not parse anything if there are any errors in the tokenizer
+ if (!this.getTokenizer().hasExitErrors()) {
+ this.parseTokens(); // same thing, this parses all the stuff recursively
+ this.invokeCallbacks();
+ }
this.isParsed = true;
@@ -356,80 +357,92 @@ public T into(@NotNull Class clazz) {
/**
* {@link #into(Class)} helper method.
- * @param clazz The Command Template class to instantiate.
+ * @param templateClass The Command Template class to instantiate.
* @param parsedArgs The parsed arguments to set the fields of the Command Template class.
*/
private static T into(
- @NotNull Class clazz,
+ @NotNull Class templateClass,
@NotNull ParsedArguments parsedArgs
)
{
- final T instance = UtlReflection.instantiate(clazz);
+ final T instance = UtlReflection.instantiate(templateClass);
- Stream.of(clazz.getFields())
+ // set the values of the fields
+ Stream.of(templateClass.getFields())
.filter(f -> f.isAnnotationPresent(Argument.Define.class))
- .forEach(f -> {
- final var annotation = f.getAnnotation(Argument.Define.class);
-
- // get the name of the argument from the annotation or field name
- final String argName = annotation.names().length == 0 ? f.getName() : annotation.names()[0];
-
- final @NotNull Optional> parsedValue = parsedArgs.get(argName);
-
- try {
- // if the field has a value already set and the parsed value is empty, skip it (keep the old value)
- if (parsedValue.isEmpty() && f.get(instance) != null)
- return;
-
- // if the type of the field is an Optional, wrap the value in it.
- // otherwise, just set the value
- f.set(
- instance,
- f.getType().isAssignableFrom(Optional.class)
- ? parsedValue
- : AfterParseOptions.into$getNewFieldValue(f, parsedValue)
- );
- } catch (IllegalArgumentException e) {
- if (parsedValue.isEmpty())
- throw new IncompatibleCommandTemplateType(
- "Field '" + f.getName() + "' of type '" + f.getType().getSimpleName() + "' does not"
- + " accept null values, but the parsed argument '" + argName + "' is null"
- );
-
- throw new IncompatibleCommandTemplateType(
- "Field '" + f.getName() + "' of type '" + f.getType().getSimpleName() + "' is not "
- + "compatible with the type (" + parsedValue.get().getClass().getSimpleName() + ") of the "
- + "parsed argument '" + argName + "'"
- );
-
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- });
+ .forEach(field -> AfterParseOptions.into$setFieldValue(field, parsedArgs, instance));
// now handle the sub-command field accessors (if any)
- final var declaredClasses = Stream.of(clazz.getDeclaredClasses())
+ Stream.of(templateClass.getDeclaredClasses())
.filter(c -> c.isAnnotationPresent(Command.Define.class))
- .toList();
-
- for (var cls : declaredClasses) {
- final var field = Stream.of(clazz.getDeclaredFields())
- .filter(f -> f.isAnnotationPresent(CommandTemplate.CommandAccessor.class))
- .filter(f -> f.getType() == cls)
- .findFirst()
- .orElseThrow(() -> {
- throw new CommandTemplateException(
- "The class '" + cls.getSimpleName() + "' is annotated with @Command.Define but it's "
- + "enclosing class does not have a field annotated with @CommandAccessor"
- );
- });
-
- AfterParseOptions.into$handleCommandAccessor(instance, field, parsedArgs);
- }
+ .forEach(cmdDef -> {
+ var commandAccesorField = Stream.of(templateClass.getDeclaredFields())
+ .filter(f -> f.isAnnotationPresent(CommandTemplate.CommandAccessor.class))
+ .filter(f -> f.getType() == cmdDef)
+ .findFirst()
+ .orElseThrow(() -> {
+ throw new CommandTemplateException(
+ "The class '" + cmdDef.getSimpleName() + "' is annotated with @Command.Define but it's "
+ + "enclosing class does not have a field annotated with @CommandAccessor"
+ );
+ });
+
+ AfterParseOptions.into$handleCommandAccessor(instance, commandAccesorField, parsedArgs);
+ });
return instance;
}
+ /**
+ * {@link #into(Class)} helper method. Sets the value of the given field based on the parsed arguments.
+ * @param field The field to set the value of.
+ * @param parsedArgs The parsed arguments to set the field value from.
+ * @param instance The instance of the current Command Template class.
+ * @param The type of the Command Template class.
+ */
+ private static void into$setFieldValue(
+ @NotNull Field field,
+ @NotNull ParsedArguments parsedArgs,
+ @NotNull T instance
+ ) {
+ final var annotation = field.getAnnotation(Argument.Define.class);
+
+ // get the name of the argument from the annotation or field name
+ final String argName = annotation.names().length == 0 ? field.getName() : annotation.names()[0];
+
+ final @NotNull Optional> parsedValue = parsedArgs.get(argName);
+
+ try {
+ // if the field has a value already set and the parsed value is empty, skip it (keep the old value)
+ if (parsedValue.isEmpty() && field.get(instance) != null)
+ return;
+
+ // if the type of the field is an Optional, wrap the value in it.
+ // otherwise, just set the value
+ field.set(
+ instance,
+ field.getType().isAssignableFrom(Optional.class)
+ ? parsedValue
+ : AfterParseOptions.into$getNewFieldValue(field, parsedValue)
+ );
+ } catch (IllegalArgumentException e) {
+ if (parsedValue.isEmpty())
+ throw new IncompatibleCommandTemplateType(
+ "Field '" + field.getName() + "' of type '" + field.getType().getSimpleName() + "' does not"
+ + " accept null values, but the parsed argument '" + argName + "' is null"
+ );
+
+ throw new IncompatibleCommandTemplateType(
+ "Field '" + field.getName() + "' of type '" + field.getType().getSimpleName() + "' is not "
+ + "compatible with the type (" + parsedValue.get().getClass().getSimpleName() + ") of the "
+ + "parsed argument '" + argName + "'"
+ );
+
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
/**
* {@link #into(Class)} helper method. Handles the {@link CommandTemplate.CommandAccessor} annotation.
* @param parsedTemplateInstance The instance of the current Command Template class.
diff --git a/src/main/java/lanat/ArgumentType.java b/src/main/java/lanat/ArgumentType.java
index 49e9c910..02400193 100644
--- a/src/main/java/lanat/ArgumentType.java
+++ b/src/main/java/lanat/ArgumentType.java
@@ -1,6 +1,9 @@
package lanat;
-import lanat.argumentTypes.*;
+import lanat.argumentTypes.FromParseableArgumentType;
+import lanat.argumentTypes.IntegerArgumentType;
+import lanat.argumentTypes.Parseable;
+import lanat.exceptions.ArgumentTypeException;
import lanat.parsing.errors.CustomError;
import lanat.utils.ErrorsContainerImpl;
import lanat.utils.Range;
@@ -102,6 +105,7 @@ public ArgumentType() {
* @param values The values to parse.
*/
public final void parseAndUpdateValue(short tokenIndex, @NotNull String... values) {
+ this.usageCount++;
this.lastTokenIndex = tokenIndex;
this.lastReceivedValuesNum = values.length;
this.currentValue = this.parseValues(values);
@@ -234,7 +238,7 @@ protected void addError(@NotNull String message, int index, @NotNull ErrorLevel
@Override
public void addError(@NotNull CustomError error) {
if (this.lastTokenIndex == -1) {
- throw new IllegalStateException("Cannot add an error to an argument that has not been parsed yet.");
+ throw new ArgumentTypeException("Cannot add an error to an argument that has not been parsed yet.");
}
// the index of the error should never be less than 0 or greater than the max value count
diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java
index 578f2ebd..4413c22c 100644
--- a/src/main/java/lanat/Command.java
+++ b/src/main/java/lanat/Command.java
@@ -18,7 +18,10 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -195,7 +198,10 @@ public void setTupleChars(@NotNull TupleChar tupleChars) {
@Override
public void addNames(@NotNull String... names) {
- Arrays.stream(names)
+ if (names.length == 0)
+ throw new IllegalArgumentException("at least one name must be specified");
+
+ Stream.of(names)
.map(UtlString::requireValidName)
.peek(newName -> {
if (this.hasName(newName))
@@ -268,9 +274,11 @@ public void addError(@NotNull String message, @NotNull ErrorLevel level) {
* Returns {@code true} if an argument with allowsUnique set in the command was used.
* @return {@code true} if an argument with {@link Argument#setAllowUnique(boolean)} in the command was used.
*/
- boolean uniqueArgumentReceivedValue() {
- return this.arguments.stream().anyMatch(a -> a.getUsageCount() >= 1 && a.isUniqueAllowed())
- || this.subCommands.stream().anyMatch(Command::uniqueArgumentReceivedValue);
+ boolean uniqueArgumentReceivedValue(@Nullable Argument, ?> exclude) {
+ return this.arguments.stream()
+ .filter(a -> a != exclude)
+ .anyMatch(a -> a.getUsageCount() >= 1 && a.isUniqueAllowed())
+ || this.subCommands.stream().anyMatch(cmd -> cmd.uniqueArgumentReceivedValue(exclude));
}
diff --git a/src/main/java/lanat/CommandTemplate.java b/src/main/java/lanat/CommandTemplate.java
index bb4df964..39d4a969 100644
--- a/src/main/java/lanat/CommandTemplate.java
+++ b/src/main/java/lanat/CommandTemplate.java
@@ -131,6 +131,7 @@ public record CommandBuildHelper(@NotNull Command cmd, @NotNull List The type of the argument.
* @param The type of the value passed to the argument.
+ * @throws ArgumentNotFoundException If there is no argument with the given name.
*/
@SuppressWarnings("unchecked")
public , TInner>
@@ -208,7 +209,7 @@ public static class Default extends CommandTemplate {
/*
* The reason we add these arguments here is so that they do not "physically" appear in the
* actual class that extends this one. 'help' and 'version' are just
- * arguments that execute actions, and they not really provide any useful values.
+ * arguments that execute actions, and they don't really provide any useful values.
*/
@InitDef
public static void afterInit(@NotNull Command cmd) {
diff --git a/src/main/java/lanat/ErrorFormatter.java b/src/main/java/lanat/ErrorFormatter.java
index eafce167..872f16e4 100644
--- a/src/main/java/lanat/ErrorFormatter.java
+++ b/src/main/java/lanat/ErrorFormatter.java
@@ -72,20 +72,20 @@ public String toString() {
/**
* Indicates the generator to display all tokens.
*
- * Tokens between the index {@code start} and the {@code offset} from it will be highlighted. If {@code placeArrow}
+ * Tokens between the index {@code start} and the {@code offsetEnd} from it will be highlighted. If {@code showArrows}
* is {@code true}, an arrow will be placed at each token index in that range.
*
* @param start The index of the first token to highlight.
- * @param offset The number of tokens to highlight after the token at the index {@code start}. A value of {@code 0}
+ * @param offsetEnd The number of tokens to highlight after the token at the index {@code start}. A value of {@code 0}
* may be used to highlight only the token at the index {@code start}.
- * @param placeArrow Whether to place an arrow at each token index in the range.
+ * @param showArrows Whether to place an arrow at each token index in the range.
*/
- public ErrorFormatter displayTokens(int start, int offset, boolean placeArrow) {
+ public ErrorFormatter displayTokens(int start, int offsetEnd, boolean showArrows) {
final var startTokenIndex = this.mainErrorHandler.getAbsoluteCmdTokenIndex() + start;
this.tokensViewOptions = new DisplayTokensOptions(
- Range.from(startTokenIndex).to(startTokenIndex + offset),
- placeArrow
+ Range.from(startTokenIndex).to(startTokenIndex + offsetEnd),
+ showArrows
);
return this;
}
@@ -102,7 +102,7 @@ public ErrorFormatter displayTokens(int index) {
/**
* Options used to display tokens.
*/
- public record DisplayTokensOptions(@NotNull Range tokensRange, boolean placeArrow) { }
+ public record DisplayTokensOptions(@NotNull Range tokensRange, boolean showArrows) { }
/**
diff --git a/src/main/java/lanat/argumentTypes/EnumArgumentType.java b/src/main/java/lanat/argumentTypes/EnumArgumentType.java
index cb90d941..5be6dd14 100644
--- a/src/main/java/lanat/argumentTypes/EnumArgumentType.java
+++ b/src/main/java/lanat/argumentTypes/EnumArgumentType.java
@@ -7,7 +7,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.Arrays;
+import java.util.stream.Stream;
/**
* An argument type that takes an enum value.
@@ -65,7 +65,7 @@ public T parseValues(@NotNull String @NotNull [] args) {
@Override
public @Nullable String getDescription() {
return "Specify one of the following values (case is ignored): "
- + String.join(", ", Arrays.stream(this.values).map(Enum::name).toList())
+ + String.join(", ", Stream.of(this.values).map(Enum::name).toList())
+ ". Default is " + this.getInitialValue().name() + ".";
}
}
diff --git a/src/main/java/lanat/argumentTypes/TryParseArgumentType.java b/src/main/java/lanat/argumentTypes/TryParseArgumentType.java
index f4ba5747..129cc580 100644
--- a/src/main/java/lanat/argumentTypes/TryParseArgumentType.java
+++ b/src/main/java/lanat/argumentTypes/TryParseArgumentType.java
@@ -24,7 +24,7 @@
public class TryParseArgumentType extends ArgumentType {
private final Function parseMethod;
private final @NotNull Class type;
- private static final String[] TRY_PARSE_METHOD_NAMES = new String[] { "valueOf", "from", "parse" };
+ private static final String[] TRY_PARSE_METHOD_NAMES = { "valueOf", "from", "parse" };
public TryParseArgumentType(@NotNull Class type) {
@@ -45,8 +45,7 @@ private static boolean isValidExecutable(Executable executable) {
}
private boolean isValidMethod(Method method) {
- return TryParseArgumentType.isValidExecutable(method)
- && method.getReturnType() == this.type;
+ return method.getReturnType() == this.type && TryParseArgumentType.isValidExecutable(method);
}
@@ -74,18 +73,20 @@ protected void addError(@NotNull String value) {
}
// Otherwise, try to find a constructor that takes a string.
- final var ctor = Stream.of(this.type.getConstructors())
+ return Stream.of(this.type.getConstructors())
.filter(TryParseArgumentType::isValidExecutable)
- .findFirst();
-
- return ctor.>map(tConstructor -> input -> {
- try {
- return tConstructor.newInstance(input);
- } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
- this.addError(input);
- }
- return null;
- }).orElse(null);
+ .findFirst()
+ .>map(c -> input -> {
+ try {
+ return c.newInstance(input);
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
+ this.addError(
+ "Unable to instantiate type '" + this.type.getSimpleName() + "' with value '" + input + "'."
+ );
+ }
+ return null;
+ })
+ .orElse(null);
}
@Override
diff --git a/src/main/java/lanat/errorFormatterGenerators/Pretty.java b/src/main/java/lanat/errorFormatterGenerators/Pretty.java
index 660c18a9..f2995321 100644
--- a/src/main/java/lanat/errorFormatterGenerators/Pretty.java
+++ b/src/main/java/lanat/errorFormatterGenerators/Pretty.java
@@ -1,12 +1,14 @@
package lanat.errorFormatterGenerators;
import lanat.ErrorFormatter;
+import lanat.utils.Range;
import lanat.utils.UtlString;
import lanat.utils.displayFormatter.FormatOption;
import lanat.utils.displayFormatter.TextFormatter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
+import java.util.List;
public class Pretty extends ErrorFormatter.Generator {
@Override
@@ -28,36 +30,50 @@ public class Pretty extends ErrorFormatter.Generator {
@Override
protected @NotNull String generateTokensView(@NotNull ErrorFormatter.DisplayTokensOptions options) {
- final var arrow = TextFormatter.ERROR("<-").withForegroundColor(this.getErrorLevel().color);
final var tokensFormatters = new ArrayList<>(this.getTokensFormatters());
final int tokensLength = tokensFormatters.size();
final var tokensRange = options.tokensRange();
// add an arrow at the start or end if the index is out of bounds
- if (tokensRange.start() < 0) {
- tokensFormatters.add(0, arrow);
- } else if (tokensRange.start() >= tokensLength) {
- tokensFormatters.add(arrow);
- }
+ if (options.showArrows() || !TextFormatter.enableSequences)
+ this.putArrows(tokensFormatters, tokensRange);
+ else
+ this.highlightTokens(tokensFormatters, tokensRange);
+
for (int i = 0; i < tokensLength; i++) {
// dim tokens before the command
if (i < this.getAbsoluteCmdTokenIndex()) {
tokensFormatters.get(i).addFormat(FormatOption.DIM);
}
-
- // highlight tokens in the range
- if (i >= tokensRange.start() && i < tokensRange.end() + 1) {
- if (options.placeArrow()) {
- tokensFormatters.add(i, arrow);
- } else {
- tokensFormatters.get(i)
- .withForegroundColor(this.getErrorLevel().color)
- .addFormat(FormatOption.REVERSE, FormatOption.BOLD);
- }
- }
}
return String.join(" ", tokensFormatters.stream().map(TextFormatter::toString).toList());
}
+
+ private void highlightTokens(@NotNull List tokensFormatters, @NotNull Range range) {
+ for (int i = range.start(); i <= range.end(); i++) {
+ tokensFormatters.get(i)
+ .withForegroundColor(this.getErrorLevel().color)
+ .addFormat(FormatOption.REVERSE, FormatOption.BOLD);
+ }
+ }
+
+ private void putArrows(@NotNull List tokensFormatters, @NotNull Range range) {
+ if (!range.isRange()) {
+ if (range.start() >= tokensFormatters.size())
+ tokensFormatters.add(this.getArrow(true));
+ else
+ tokensFormatters.add(range.start() + 1, this.getArrow(true));
+ return;
+ }
+
+ tokensFormatters.add(range.end() + 1, this.getArrow(true));
+ tokensFormatters.add(range.start(), this.getArrow(false));
+ }
+
+ private @NotNull TextFormatter getArrow(boolean isLeft) {
+ return new TextFormatter(isLeft ? "<-" : "->", this.getErrorLevel().color)
+ .addFormat(FormatOption.REVERSE, FormatOption.BOLD);
+ }
}
diff --git a/src/main/java/lanat/parsing/Parser.java b/src/main/java/lanat/parsing/Parser.java
index ea05bc96..062bdf5a 100644
--- a/src/main/java/lanat/parsing/Parser.java
+++ b/src/main/java/lanat/parsing/Parser.java
@@ -30,6 +30,11 @@ public class Parser extends ParsingStateBase {
*/
private short currentTokenIndex = 0;
+ /**
+ * Whether we are currently parsing values in a tuple.
+ */
+ private boolean isInTuple = false;
+
/**
* The parsed arguments. This is a map of the argument to the value that it parsed. The reason this is saved is that
* we don't want to run {@link Parser#getParsedArgumentsHashMap()} multiple times because that can break stuff badly
@@ -58,6 +63,12 @@ public boolean hasDisplayErrors() {
return this.getErrorsInLevelMinimum(this.customErrors, true);
}
+ @Override
+ public void addError(@NotNull ParseError error) {
+ error.setIsInTuple(this.isInTuple); // set whether the error was caused while parsing values in a tuple
+ super.addError(error);
+ }
+
public void addError(@NotNull ParseError.ParseErrorType type, @Nullable Argument, ?> arg, int argValueCount, int currentIndex) {
this.addError(new ParseError(type, currentIndex, arg, argValueCount));
}
@@ -150,12 +161,12 @@ private void executeArgParse(@NotNull Argument, ?> arg) {
return;
}
- final boolean isInTuple = (
+ this.isInTuple = (
this.currentTokenIndex < this.tokens.size()
&& this.tokens.get(this.currentTokenIndex).type() == TokenType.ARGUMENT_VALUE_TUPLE_START
);
- final byte ifTupleOffset = (byte)(isInTuple ? 1 : 0);
+ final byte ifTupleOffset = (byte)(this.isInTuple ? 1 : 0);
final ArrayList values = new ArrayList<>();
short numValues = 0;
@@ -167,7 +178,7 @@ private void executeArgParse(@NotNull Argument, ?> arg) {
numValues++, tokenIndex++
) {
final Token currentToken = this.tokens.get(tokenIndex);
- if (!isInTuple && (
+ if (!this.isInTuple && (
currentToken.type().isArgumentSpecifier() || numValues >= argNumValuesRange.end()
)
|| currentToken.type().isTuple()
@@ -179,7 +190,7 @@ private void executeArgParse(@NotNull Argument, ?> arg) {
final int skipIndexCount = numValues + ifTupleOffset*2;
if (numValues > argNumValuesRange.end() || numValues < argNumValuesRange.start()) {
- this.addError(ParseError.ParseErrorType.ARG_INCORRECT_VALUE_NUMBER, arg, numValues + ifTupleOffset);
+ this.addError(ParseError.ParseErrorType.ARG_INCORRECT_VALUE_NUMBER, arg, numValues);
this.currentTokenIndex += skipIndexCount;
return;
}
@@ -230,21 +241,21 @@ private void parseArgNameList(@NotNull String args) {
for (short i = 0; i < args.length(); i++) {
final short constIndex = i; // this is because the lambda requires the variable to be final
- if (!this.runForArgument(args.charAt(i), a -> {
+ if (!this.runForArgument(args.charAt(i), argument -> {
// if the argument accepts 0 values, then we can just parse it like normal
- if (a.argType.getRequiredArgValueCount().isZero()) {
- this.executeArgParse(a);
+ if (argument.argType.getRequiredArgValueCount().isZero()) {
+ this.executeArgParse(argument);
// -- arguments now may accept 1 or more values from now on:
// if this argument is the last one in the list, then we can parse the next values after it
} else if (constIndex == args.length() - 1) {
this.currentTokenIndex++;
- this.executeArgParse(a);
+ this.executeArgParse(argument);
// if this argument is not the last one in the list, then we can parse the rest of the chars as the value
} else {
- this.executeArgParse(a, args.substring(constIndex + 1));
+ this.executeArgParse(argument, args.substring(constIndex + 1));
}
}))
return;
@@ -254,14 +265,12 @@ private void parseArgNameList(@NotNull String args) {
/** Returns the positional argument at the given index of declaration. */
private @Nullable Argument, ?> getArgumentByPositionalIndex(short index) {
- final var posArgs = this.command.getPositionalArguments();
+ var posArgs = this.command.getPositionalArguments();
- for (short i = 0; i < posArgs.size(); i++) {
- if (i == index) {
- return posArgs.get(i);
- }
- }
- return null;
+ if (index >= posArgs.size())
+ return null;
+
+ return posArgs.get(index);
}
/**
diff --git a/src/main/java/lanat/parsing/ParsingStateBase.java b/src/main/java/lanat/parsing/ParsingStateBase.java
index 12e5cd13..b14fea60 100644
--- a/src/main/java/lanat/parsing/ParsingStateBase.java
+++ b/src/main/java/lanat/parsing/ParsingStateBase.java
@@ -22,7 +22,7 @@ public ParsingStateBase(@NotNull Command command) {
/**
* Executes a callback for the argument found by the name specified.
*
- * @return ParseErrorType.ArgumentNotFound if an argument was found
+ * @return {@code true} if an argument was found
*/
protected boolean runForArgument(@NotNull String argName, @NotNull Consumer<@NotNull Argument, ?>> f) {
for (final var argument : this.getArguments()) {
diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java
index 0397ac40..ba15c069 100644
--- a/src/main/java/lanat/parsing/Tokenizer.java
+++ b/src/main/java/lanat/parsing/Tokenizer.java
@@ -217,15 +217,20 @@ private void addToken(@NotNull TokenType type, char contents) {
*/
private void tokenizeCurrentValue() {
final Token token = this.tokenizeWord(this.currentValue.toString());
- Command subCmd;
+
// if this is a Sub-Command, continue tokenizing next elements
- if (token.type() == TokenType.COMMAND && (subCmd = this.getSubCommandByName(token.contents())) != null) {
+ if (token.type() == TokenType.COMMAND) {
// forward the rest of stuff to the Sub-Command
- subCmd.getTokenizer().tokenize(this.inputString.substring(this.currentCharIndex));
+ this.getSubCommandByName(token.contents())
+ .getTokenizer()
+ .tokenize(this.inputString.substring(this.currentCharIndex));
+
this.hasFinished = true;
} else {
+ // otherwise, just add the token to the final tokens list
this.finalTokens.add(token);
}
+
this.currentValue.setLength(0);
}
@@ -245,7 +250,7 @@ private boolean isArgNameList(@NotNull String str) {
final var charArray = str.substring(1).toCharArray();
for (final char argName : charArray) {
- if (!this.runForArgument(argName, a -> possiblePrefixes.add(a.getPrefix().character)))
+ if (!this.runForArgument(argName, argument -> possiblePrefixes.add(argument.getPrefix().character)))
break;
}
@@ -290,9 +295,8 @@ private boolean isCharAtRelativeIndex(int index, char character) {
}
/** Returns a command from the Sub-Commands of {@link Tokenizer#command} that matches the given name */
- private Command getSubCommandByName(@NotNull String name) {
- var x = this.getCommands().stream().filter(sc -> sc.hasName(name)).toList();
- return x.isEmpty() ? null : x.get(0);
+ private @NotNull Command getSubCommandByName(@NotNull String name) {
+ return this.command.getCommand(name);
}
/**
diff --git a/src/main/java/lanat/parsing/errors/ErrorHandler.java b/src/main/java/lanat/parsing/errors/ErrorHandler.java
index d3f39f48..fe6ee3c9 100644
--- a/src/main/java/lanat/parsing/errors/ErrorHandler.java
+++ b/src/main/java/lanat/parsing/errors/ErrorHandler.java
@@ -47,7 +47,7 @@ public ErrorHandler(@NotNull Command rootCommand) {
this.addAll(cmd.getErrorsUnderDisplayLevel());
this.addAll(cmd.getTokenizer().getErrorsUnderDisplayLevel());
this.addAll(cmd.getParser().getCustomErrors());
- this.addAll(ParseError.filter(cmd.getParser().getErrorsUnderDisplayLevel()));
+ this.addAll(cmd.getParser().getErrorsUnderDisplayLevel());
}}.stream()
.sorted(Comparator.comparingInt(x -> x.tokenIndex)) // sort them by their token index...
.forEach(e -> errors.add(e.handle(this))); // ...and handle them
diff --git a/src/main/java/lanat/parsing/errors/ParseError.java b/src/main/java/lanat/parsing/errors/ParseError.java
index 77f756cc..72dbe68b 100644
--- a/src/main/java/lanat/parsing/errors/ParseError.java
+++ b/src/main/java/lanat/parsing/errors/ParseError.java
@@ -9,13 +9,11 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.ArrayList;
-import java.util.List;
-
@SuppressWarnings("unused")
public class ParseError extends ParseStateErrorBase {
public final Argument, ?> argument;
- public final int valueCount;
+ private final int valueCount;
+ private boolean isInTuple = false;
private ArgumentGroup argumentGroup;
public enum ParseErrorType implements ErrorLevelProvider {
@@ -51,23 +49,21 @@ public void setArgumentGroup(@NotNull ArgumentGroup argumentGroup) {
this.argumentGroup = argumentGroup;
}
- public static @NotNull List<@NotNull ParseError> filter(@NotNull List<@NotNull ParseError> errors) {
- final var newList = new ArrayList<>(errors);
-
- for (final var err : errors) {
- /* if we are going to show an error about an argument being incorrectly used, and that argument is defined
- * as required, we don't need to show the required error since its obvious that the user knows that
- * the argument is required */
- if (err.errorType == ParseErrorType.ARG_INCORRECT_VALUE_NUMBER) {
- newList.removeIf(e ->
- e.argument != null
- && e.argument.equals(err.argument)
- && e.errorType == ParseErrorType.REQUIRED_ARGUMENT_NOT_USED
- );
- }
- }
+ /**
+ * Sets whether the error was caused while parsing values in a tuple.
+ * @param isInTuple whether the error was caused while parsing values in a tuple
+ */
+ public void setIsInTuple(boolean isInTuple) {
+ this.isInTuple = isInTuple;
+ }
- return newList;
+ /**
+ * Returns the offset from the token index to the value tokens. Adds 2 if the error was caused while parsing values
+ * in a tuple.
+ * @return the offset from the token index to the value tokens
+ */
+ private int getValueTokensOffset() {
+ return this.valueCount + (this.isInTuple ? 2 : 0); // 2 for the tuple tokens
}
@Handler("ARG_INCORRECT_VALUE_NUMBER")
@@ -78,10 +74,10 @@ protected void handleIncorrectValueNumber() {
.setContent("Incorrect number of values for argument '%s'.%nExpected %s, but got %d."
.formatted(
this.argument.getName(), this.argument.argType.getRequiredArgValueCount().getMessage("value"),
- Math.max(this.valueCount - 1, 0) // this is done because if there are tuples, the end token is counted as a value (maybe a bit hacky?)
+ this.valueCount
)
)
- .displayTokens(this.tokenIndex + 1, this.valueCount, this.valueCount == 0);
+ .displayTokens(this.tokenIndex, this.getValueTokensOffset(), this.getValueTokensOffset() == 0);
}
@Handler("ARG_INCORRECT_USAGES_COUNT")
@@ -95,7 +91,7 @@ protected void handleIncorrectUsagesCount() {
UtlString.plural("time", this.argument.getUsageCount())
)
)
- .displayTokens(this.tokenIndex + 1, this.valueCount, this.valueCount == 0);
+ .displayTokens(this.tokenIndex, this.getValueTokensOffset(), false);
}
@Handler("REQUIRED_ARGUMENT_NOT_USED")
@@ -109,7 +105,7 @@ protected void handleRequiredArgumentNotUsed() {
? "Required argument '%s' not used.".formatted(this.argument.getName())
: "Required argument '%s' for command '%s' not used.".formatted(this.argument.getName(), argCmd.getName())
)
- .displayTokens(this.tokenIndex + 1);
+ .displayTokens(this.tokenIndex);
}
@Handler("UNMATCHED_TOKEN")
@@ -118,7 +114,7 @@ protected void handleUnmatchedToken() {
.setContent("Token '%s' does not correspond with a valid argument, value, or command."
.formatted(this.getCurrentToken().contents())
)
- .displayTokens(this.tokenIndex, this.valueCount, false);
+ .displayTokens(this.tokenIndex, 0, false);
}
@Handler("MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED")
@@ -127,6 +123,6 @@ protected void handleMultipleArgsInExclusiveGroupUsed() {
.setContent("Multiple arguments in exclusive group '%s' used."
.formatted(this.argumentGroup.getName())
)
- .displayTokens(this.tokenIndex, this.valueCount, false);
+ .displayTokens(this.tokenIndex, this.getValueTokensOffset(), false);
}
}
diff --git a/src/main/java/lanat/utils/Range.java b/src/main/java/lanat/utils/Range.java
index b7a1395d..9726341a 100644
--- a/src/main/java/lanat/utils/Range.java
+++ b/src/main/java/lanat/utils/Range.java
@@ -126,6 +126,10 @@ public int end() {
* @return {@code true} if the value is in the range
*/
public boolean isInRange(int value, boolean startInclusive, boolean endInclusive) {
+ if (!this.isRange()) {
+ return value == this.start;
+ }
+
boolean isInStart = startInclusive
? value >= this.start
: value > this.start;
diff --git a/src/main/java/lanat/utils/UtlReflection.java b/src/main/java/lanat/utils/UtlReflection.java
index bc6800df..75149712 100644
--- a/src/main/java/lanat/utils/UtlReflection.java
+++ b/src/main/java/lanat/utils/UtlReflection.java
@@ -47,14 +47,18 @@ public static boolean hasParameters(Method method, Class>... parameters) {
* @return The instantiated class. If the class could not be instantiated, a {@link RuntimeException} is thrown.
*/
public static T instantiate(Class clazz, Object... args) {
+ final Class>[] parameterTypes = Stream.of(args)
+ .map(Object::getClass)
+ .toArray(Class>[]::new);
+
try {
- return clazz.getDeclaredConstructor().newInstance(args);
+ return clazz.getDeclaredConstructor(parameterTypes).newInstance(args);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find a public constructor for the class '" + clazz.getName()
+ """
'. Please, make sure:
- - This class has a public constructor with no arguments. (Or no constructor at all)
- - This is a static class. (Not an inner class)"""
+ - This class has a public constructor with the parameters: %s
+ - This is a static class. (Not an inner class)""".formatted(Arrays.toString(parameterTypes))
);
} catch (IllegalAccessException e) {
throw new RuntimeException(
diff --git a/src/main/java/lanat/utils/UtlString.java b/src/main/java/lanat/utils/UtlString.java
index a9436da5..23d12cb8 100644
--- a/src/main/java/lanat/utils/UtlString.java
+++ b/src/main/java/lanat/utils/UtlString.java
@@ -4,8 +4,8 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.Arrays;
import java.util.regex.Pattern;
+import java.util.stream.Stream;
public final class UtlString {
private UtlString() {}
@@ -29,7 +29,7 @@ private UtlString() {}
* Get the longest line from the contents of a string. Lines are separated by newlines.
*/
public static @NotNull String getLongestLine(@NotNull String str) {
- return Arrays.stream(str.split("\n")).min((a, b) -> b.length() - a.length()).orElse("");
+ return Stream.of(str.split("\n")).min((a, b) -> b.length() - a.length()).orElse("");
}
/**
diff --git a/src/main/java/lanat/utils/displayFormatter/Color.java b/src/main/java/lanat/utils/displayFormatter/Color.java
index cd7c6253..9c36e2d3 100644
--- a/src/main/java/lanat/utils/displayFormatter/Color.java
+++ b/src/main/java/lanat/utils/displayFormatter/Color.java
@@ -58,7 +58,7 @@ public String toString() {
/**
* Immutable list of all the dark colors.
*/
- public static final @NotNull Color[] BRIGHT_COLORS = new Color[] {
+ public static final @NotNull Color[] BRIGHT_COLORS = {
BRIGHT_RED,
BRIGHT_GREEN,
BRIGHT_YELLOW,
@@ -71,7 +71,7 @@ public String toString() {
/**
* Immutable list of all the bright colors.
*/
- public static final @NotNull Color[] DARK_COLORS = new Color[] {
+ public static final @NotNull Color[] DARK_COLORS = {
RED,
GREEN,
YELLOW,
diff --git a/src/main/java/lanat/utils/displayFormatter/TextFormatter.java b/src/main/java/lanat/utils/displayFormatter/TextFormatter.java
index f2594f5e..4ca9b48f 100644
--- a/src/main/java/lanat/utils/displayFormatter/TextFormatter.java
+++ b/src/main/java/lanat/utils/displayFormatter/TextFormatter.java
@@ -24,7 +24,7 @@ public class TextFormatter {
/**
* When set to {@code true}, the {@link #toString()} method will not add any terminal sequences, but rather
- * return the sequences that would be added by marking them as {@code ESC[}
+ * return the sequences that would be added by marking them as {@code ESC[]}
*/
public static boolean debug = false;
@@ -74,6 +74,15 @@ public TextFormatter addFormat(@NotNull FormatOption... options) {
return this;
}
+ /**
+ * Removes the specified formatting options from the formatter.
+ * @param options The formatting options to remove.
+ */
+ public TextFormatter removeFormat(@NotNull FormatOption... options) {
+ this.formatOptions.removeAll(Arrays.asList(options));
+ return this;
+ }
+
/**
* Sets the foreground color of the formatter.
* @param foreground The foreground color of the formatter.
@@ -145,11 +154,8 @@ public TextFormatter concat(@NotNull String... strings) {
* @return {@code true} if the formatter is simple
*/
public boolean isSimple() {
- return (
- this.contents.length() == 0
- || this.formattingNotDefined()
- || !enableSequences
- ) && this.concatList.size() == 0; // we cant skip if we need to concat stuff!
+ return (this.contents.length() == 0 || this.formattingNotDefined())
+ && this.concatList.size() == 0; // we cant skip if we need to concat stuff!
}
/**
@@ -170,7 +176,7 @@ public boolean formattingNotDefined() {
* @return the start sequences to add to the contents of the formatter
*/
private @NotNull String getStartSequences() {
- if (this.formattingNotDefined() || !TextFormatter.enableSequences) return "";
+ if (this.formattingNotDefined()) return "";
final var buffer = new StringBuilder();
if (this.foregroundColor != null)
@@ -186,7 +192,7 @@ public boolean formattingNotDefined() {
}
private @NotNull String getEndSequences() {
- if (this.formattingNotDefined() || !TextFormatter.enableSequences) return "";
+ if (this.formattingNotDefined()) return "";
final var buffer = new StringBuilder();
if (this.backgroundColor != null) {
@@ -246,45 +252,56 @@ public boolean formattingNotDefined() {
*/
@Override
public @NotNull String toString() {
- if (this.isSimple()) {
+ if (!TextFormatter.enableSequences || this.isSimple()) {
return this.contents;
}
- final var buffer = new StringBuilder();
+ final var buff = new StringBuilder();
- // for some reason, some terminals reset sequences when a new line is added.
- {
- final var split = UtlString.splitAtLeadingWhitespace(this.contents);
+ if (this.contents.contains("\n")) {
+ // for some reason, some terminals reset sequences when a new line is added.
+ this.putContentsSanitized(buff);
+ } else {
+ buff.append(this.getStartSequences());
+ buff.append(this.contents);
+ }
- // start by adding the leading whitespace
- if (!split.first().isEmpty()) {
- buffer.append(split.first());
- }
+ // then do the same thing for the concatenated formatters
+ for (TextFormatter subFormatter : this.concatList) {
+ buff.append(subFormatter);
+ }
- // then add the start sequences
- buffer.append(this.getStartSequences());
+ buff.append(this.getEndSequences());
- char[] charArray = split.second().toCharArray();
- for (int i = 0; i < charArray.length; i++) {
- var chr = charArray[i];
+ return buff.toString();
+ }
- // if we encounter a new line, and the next character is not a whitespace, then add the start sequences
- if (chr == '\n' && (i < charArray.length - 1 && !Character.isWhitespace(charArray[i + 1])))
- buffer.append(this.getStartSequences());
+ /**
+ * Adds the start sequences to the contents of the formatter. This is done by adding the start sequences after
+ * every new line. (and at the first line)
+ * @param buff The buffer to add the contents to.
+ */
+ private void putContentsSanitized(@NotNull StringBuilder buff) {
+ final var split = UtlString.splitAtLeadingWhitespace(this.contents);
+ final var startSequences = this.getStartSequences();
- // add the character
- buffer.append(chr);
- }
- }
+ // start by adding the leading whitespace
+ buff.append(split.first());
- // then do the same thing for the concatenated formatters
- for (TextFormatter subFormatter : this.concatList) {
- buffer.append(subFormatter);
- }
+ // then add the start sequences
+ buff.append(startSequences);
- buffer.append(this.getEndSequences());
+ char[] charArray = split.second().toCharArray();
+ for (int i = 0; i < charArray.length; i++) {
+ var chr = charArray[i];
- return buffer.toString();
+ // if we encounter a new line, and the next character is not a whitespace, then add the start sequences
+ if (chr == '\n' && (i < charArray.length - 1 && !Character.isWhitespace(charArray[i + 1])))
+ buff.append(startSequences);
+
+ // add the character
+ buff.append(chr);
+ }
}
/** Returns a template for a {@link TextFormatter} that is used for errors */
diff --git a/src/test/java/lanat/test/exampleTests/ExampleTest.java b/src/test/java/lanat/test/exampleTests/ExampleTest.java
index e3009ed6..9cff39e7 100644
--- a/src/test/java/lanat/test/exampleTests/ExampleTest.java
+++ b/src/test/java/lanat/test/exampleTests/ExampleTest.java
@@ -1,7 +1,9 @@
package lanat.test.exampleTests;
import lanat.*;
+import lanat.argumentTypes.IntegerArgumentType;
import lanat.argumentTypes.NumberRangeArgumentType;
+import lanat.utils.Range;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -9,19 +11,34 @@
public final class ExampleTest {
@Test
public void main() {
- new ArgumentParser("my-program") {{
- this.addArgument(Argument.create(new Example1Type(), "user"));
- this.addArgument(Argument.create(new NumberRangeArgumentType<>(0.0, 15.23), "number"));
- }}.parse(CLInput.from("--user hello --number 42.1"))
- .getErrors()
- .forEach(System.out::println);
+ Argument.PrefixChar.defaultPrefix = Argument.PrefixChar.MINUS;
+ var parsedArgs = new ArgumentParser("my-program") {{
+ this.setCallbackInvocationOption(CallbacksInvocationOption.NO_ERROR_IN_ARGUMENT);
+ this.addHelpArgument();
+ this.addArgument(Argument.create(new Example1Type(), "user").required().positional());
+ this.addArgument(Argument.create(new NumberRangeArgumentType<>(0.0, 15.23), "number").onOk(System.out::println));
+ this.addArgument(Argument.create(new IntegerArgumentType(), "test").onOk(System.out::println).allowsUnique());
+ }}.parse(CLInput.from("-h --number=3' --user [jo test ! hello]"))
+ .printErrors()
+ .getParsedArguments();
+
+ System.out.println(parsedArgs);
}
- public static class Example1Type extends ArgumentType {
+ public static class Example1Type extends ArgumentType {
+ @Override
+ public @Nullable String[] parseValues(@NotNull String... args) {
+ this.forEachArgValue(args, str -> {
+ if (str.equals("!")) {
+ this.addError("The user cannot be '!'.", ErrorLevel.ERROR);
+ }
+ });
+ return args;
+ }
+
@Override
- public @Nullable String parseValues(@NotNull String... args) {
- this.addError("Could not find the user '" + args[0] + "' in the database.", ErrorLevel.WARNING);
- return args[0];
+ public @NotNull Range getRequiredArgValueCount() {
+ return Range.from(2).toInfinity();
}
}
}
\ No newline at end of file
diff --git a/src/test/java/lanat/test/units/TestTerminalOutput.java b/src/test/java/lanat/test/units/TestTerminalOutput.java
index 5e3dc8df..1a2a3aa5 100644
--- a/src/test/java/lanat/test/units/TestTerminalOutput.java
+++ b/src/test/java/lanat/test/units/TestTerminalOutput.java
@@ -9,6 +9,8 @@
public class TestTerminalOutput extends UnitTests {
private void assertErrorOutput(String args, String expected) {
final var errors = this.parser.parseGetErrors(args);
+ System.out.printf("Test error output:\n%s", errors.get(0));
+
// remove all the decorations to not make the tests a pain to write
assertEquals(
expected,
@@ -17,7 +19,6 @@ private void assertErrorOutput(String args, String expected) {
.replaceAll(" *[│─└┌\r] ?", "")
.strip()
);
- System.out.printf("Test error output:\n%s", errors.get(0));
}
@Test
@@ -43,7 +44,7 @@ public void testLastRequiredArgument() {
public void testExceedValueCount() {
this.assertErrorOutput("--what [1 2 3 4 5 6 7 8 9 10]", """
ERROR
- Testing --what [ 1 2 3 4 5 6 7 8 9 10 ]
+ Testing -> --what [ 1 2 3 4 5 6 7 8 9 10 ] <-
Incorrect number of values for argument 'what'.
Expected from 1 to 3 values, but got 10.""");
}
@@ -74,7 +75,7 @@ public void testMissingValueBeforeToken() {
public void testMissingValueWithTuple() {
this.assertErrorOutput("--what []", """
ERROR
- Testing --what [ ]
+ Testing -> --what [ ] <-
Incorrect number of values for argument 'what'.
Expected from 1 to 3 values, but got 0.""");
}
@@ -84,7 +85,7 @@ public void testMissingValueWithTuple() {
public void testInvalidArgumentTypeValue() {
this.assertErrorOutput("foo subCommand another bar", """
ERROR
- Testing foo subCommand another bar
+ Testing foo subCommand another bar <-
Invalid Integer value: 'bar'.""");
}
@@ -93,7 +94,7 @@ public void testInvalidArgumentTypeValue() {
public void testUnmatchedToken() {
this.assertErrorOutput("[foo] --unknown", """
WARNING
- Testing [ foo ] --unknown
+ Testing [ foo ] --unknown <-
Token '--unknown' does not correspond with a valid argument, value, or command.""");
}
@@ -102,13 +103,13 @@ public void testUnmatchedToken() {
public void testIncorrectUsageCount() {
this.assertErrorOutput("foo --double-adder 5.0", """
ERROR
- Testing foo --double-adder 5.0 <-
+ Testing foo -> --double-adder 5.0 <-
Argument 'double-adder' was used an incorrect amount of times.
Expected from 2 to 4 usages, but was used 1 time.""");
this.assertErrorOutput("foo --double-adder 5.0 --double-adder 5.0 --double-adder 5.0 --double-adder 5.0 --double-adder 5.0", """
ERROR
- Testing foo --double-adder 5.0 --double-adder 5.0 --double-adder 5.0 --double-adder 5.0 --double-adder 5.0
+ Testing foo --double-adder 5.0 --double-adder 5.0 --double-adder 5.0 --double-adder 5.0 -> --double-adder 5.0 <-
Argument 'double-adder' was used an incorrect amount of times.
Expected from 2 to 4 usages, but was used 5 times.""");
}
From bfaa0ea5ad7006841e19912723328d8d05285894 Mon Sep 17 00:00:00 2001
From: DarviL
Date: Fri, 3 Nov 2023 20:44:19 +0100
Subject: [PATCH 26/33] fix: incorrect value numbers errors not highlighting
tokens correctly.
---
src/main/java/lanat/parsing/errors/ParseError.java | 9 ++++++++-
src/test/java/lanat/test/exampleTests/ExampleTest.java | 4 ++--
src/test/java/lanat/test/units/TestTerminalOutput.java | 7 +++----
3 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/src/main/java/lanat/parsing/errors/ParseError.java b/src/main/java/lanat/parsing/errors/ParseError.java
index 72dbe68b..a3bf546c 100644
--- a/src/main/java/lanat/parsing/errors/ParseError.java
+++ b/src/main/java/lanat/parsing/errors/ParseError.java
@@ -70,6 +70,9 @@ private int getValueTokensOffset() {
protected void handleIncorrectValueNumber() {
assert this.argument != null;
+ // offset to just show the value tokens (we don't want to highlight the argument token as well)
+ final var inTupleOffset = this.isInTuple ? 1 : 0;
+
this.fmt()
.setContent("Incorrect number of values for argument '%s'.%nExpected %s, but got %d."
.formatted(
@@ -77,7 +80,11 @@ protected void handleIncorrectValueNumber() {
this.valueCount
)
)
- .displayTokens(this.tokenIndex, this.getValueTokensOffset(), this.getValueTokensOffset() == 0);
+ .displayTokens(
+ this.tokenIndex + inTupleOffset,
+ this.getValueTokensOffset() - inTupleOffset,
+ this.getValueTokensOffset() == 0
+ );
}
@Handler("ARG_INCORRECT_USAGES_COUNT")
diff --git a/src/test/java/lanat/test/exampleTests/ExampleTest.java b/src/test/java/lanat/test/exampleTests/ExampleTest.java
index 9cff39e7..45a10f7c 100644
--- a/src/test/java/lanat/test/exampleTests/ExampleTest.java
+++ b/src/test/java/lanat/test/exampleTests/ExampleTest.java
@@ -15,10 +15,10 @@ public void main() {
var parsedArgs = new ArgumentParser("my-program") {{
this.setCallbackInvocationOption(CallbacksInvocationOption.NO_ERROR_IN_ARGUMENT);
this.addHelpArgument();
- this.addArgument(Argument.create(new Example1Type(), "user").required().positional());
+ this.addArgument(Argument.create(new Example1Type(), "user", "u").required().positional());
this.addArgument(Argument.create(new NumberRangeArgumentType<>(0.0, 15.23), "number").onOk(System.out::println));
this.addArgument(Argument.create(new IntegerArgumentType(), "test").onOk(System.out::println).allowsUnique());
- }}.parse(CLInput.from("-h --number=3' --user [jo test ! hello]"))
+ }}.parse(CLInput.from("-h --number=3 -u [jo ]"))
.printErrors()
.getParsedArguments();
diff --git a/src/test/java/lanat/test/units/TestTerminalOutput.java b/src/test/java/lanat/test/units/TestTerminalOutput.java
index 1a2a3aa5..f03cb849 100644
--- a/src/test/java/lanat/test/units/TestTerminalOutput.java
+++ b/src/test/java/lanat/test/units/TestTerminalOutput.java
@@ -41,15 +41,14 @@ public void testLastRequiredArgument() {
@Test
@DisplayName("Tuple is highlighted correctly")
- public void testExceedValueCount() {
+ public void testExceedValueCountTuple() {
this.assertErrorOutput("--what [1 2 3 4 5 6 7 8 9 10]", """
ERROR
- Testing -> --what [ 1 2 3 4 5 6 7 8 9 10 ] <-
+ Testing --what -> [ 1 2 3 4 5 6 7 8 9 10 ] <-
Incorrect number of values for argument 'what'.
Expected from 1 to 3 values, but got 10.""");
}
-
@Test
@DisplayName("Arrow points to the last token on last argument missing value")
public void testMissingValue() {
@@ -75,7 +74,7 @@ public void testMissingValueBeforeToken() {
public void testMissingValueWithTuple() {
this.assertErrorOutput("--what []", """
ERROR
- Testing -> --what [ ] <-
+ Testing --what -> [ ] <-
Incorrect number of values for argument 'what'.
Expected from 1 to 3 values, but got 0.""");
}
From d45e01884ce97a8fa30a6dba238f957b9da6d127 Mon Sep 17 00:00:00 2001
From: DarviL
Date: Fri, 3 Nov 2023 21:45:15 +0100
Subject: [PATCH 27/33] fix: strings not behaving properly when placed right
after another value feat: tokenizing process now may push multiple errors
(instead of being limited to just one) feat: improved displaying of tokenizer
errors.
---
src/main/java/lanat/parsing/Tokenizer.java | 44 ++++++++++---------
.../lanat/parsing/errors/TokenizeError.java | 4 +-
.../lanat/test/exampleTests/ExampleTest.java | 5 ++-
3 files changed, 29 insertions(+), 24 deletions(-)
diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java
index ba15c069..7d216fd7 100644
--- a/src/main/java/lanat/parsing/Tokenizer.java
+++ b/src/main/java/lanat/parsing/Tokenizer.java
@@ -65,7 +65,6 @@ public void tokenize(@NotNull String input) {
}
char currentStringChar = 0; // the character that opened the string
- TokenizeError.TokenizeErrorType errorType = null;
for (
this.currentCharIndex = 0;
@@ -79,7 +78,7 @@ public void tokenize(@NotNull String input) {
this.currentValue.append(this.inputChars[++this.currentCharIndex]); // skip the \ character and append the next character
// reached a possible value wrapped in quotes
- } else if (cChar == '"' || cChar == '\'') {
+ } else if ((cChar == '"' || cChar == '\'')) {
// if we are already in an open string, push the current value and close the string. Make sure
// that the current char is the same as the one that opened the string
if (this.stringOpen && currentStringChar == cChar) {
@@ -87,8 +86,9 @@ public void tokenize(@NotNull String input) {
this.currentValue.setLength(0);
this.stringOpen = false;
- // the string is open, but the character does not match. Push it as a normal character
- } else if (this.stringOpen) {
+ // the string is open, but the character does not match, or there's something already in the current value.
+ // Push it as a normal character
+ } else if (this.stringOpen || !this.currentValue.isEmpty()) {
this.currentValue.append(cChar);
// the string is not open, so open it and set the current string char to the current char
@@ -103,15 +103,17 @@ public void tokenize(@NotNull String input) {
// reached a possible tuple start character
} else if (cChar == this.tupleOpenChar) {
- // if we are already in a tuple, set error and stop tokenizing
+ // if we are already in a tuple, add error
if (this.tupleOpen) {
- errorType = TokenizeError.TokenizeErrorType.TUPLE_ALREADY_OPEN;
- break;
+ // push tuple start token so the user can see the incorrect tuple char
+ this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_START, this.tupleOpenChar);
+ this.addError(TokenizeError.TokenizeErrorType.TUPLE_ALREADY_OPEN);
+ continue;
} else if (!this.currentValue.isEmpty()) { // if there was something before the tuple, tokenize it
this.tokenizeCurrentValue();
}
- // push the tuple token and set the state to tuple open
+ // set the state to tuple open
this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_START, this.tupleOpenChar);
this.tupleOpen = true;
@@ -119,8 +121,10 @@ public void tokenize(@NotNull String input) {
} else if (cChar == this.tupleCloseChar) {
// if we are not in a tuple, set error and stop tokenizing
if (!this.tupleOpen) {
- errorType = TokenizeError.TokenizeErrorType.UNEXPECTED_TUPLE_CLOSE;
- break;
+ // push tuple start token so the user can see the incorrect tuple char
+ this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_END, this.tupleCloseChar);
+ this.addError(TokenizeError.TokenizeErrorType.UNEXPECTED_TUPLE_CLOSE);
+ continue;
}
// if there was something before the tuple, tokenize it
@@ -128,7 +132,7 @@ public void tokenize(@NotNull String input) {
this.addToken(TokenType.ARGUMENT_VALUE, this.currentValue.toString());
}
- // push the tuple token and set the state to tuple closed
+ // set the state to tuple closed
this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_END, this.tupleCloseChar);
this.currentValue.setLength(0);
this.tupleOpen = false;
@@ -158,22 +162,16 @@ public void tokenize(@NotNull String input) {
}
}
- if (errorType == null)
- if (this.tupleOpen) {
- errorType = TokenizeError.TokenizeErrorType.TUPLE_NOT_CLOSED;
- } else if (this.stringOpen) {
- errorType = TokenizeError.TokenizeErrorType.STRING_NOT_CLOSED;
- }
+ if (this.tupleOpen)
+ this.addError(TokenizeError.TokenizeErrorType.TUPLE_NOT_CLOSED);
+ if (this.stringOpen)
+ this.addError(TokenizeError.TokenizeErrorType.STRING_NOT_CLOSED);
// we left something in the current value, tokenize it
if (!this.currentValue.isEmpty()) {
this.tokenizeCurrentValue();
}
- if (errorType != null) {
- this.addError(errorType, this.finalTokens.size());
- }
-
this.hasFinished = true;
}
@@ -187,6 +185,10 @@ private void addToken(@NotNull TokenType type, char contents) {
this.finalTokens.add(new Token(type, String.valueOf(contents)));
}
+ private void addError(@NotNull TokenizeError.TokenizeErrorType type) {
+ this.addError(type, this.finalTokens.size());
+ }
+
/**
* Tokenizes a single word and returns the token matching it. If no match could be found, returns
* {@link TokenType#ARGUMENT_VALUE}
diff --git a/src/main/java/lanat/parsing/errors/TokenizeError.java b/src/main/java/lanat/parsing/errors/TokenizeError.java
index 9122be83..7122a01b 100644
--- a/src/main/java/lanat/parsing/errors/TokenizeError.java
+++ b/src/main/java/lanat/parsing/errors/TokenizeError.java
@@ -26,7 +26,7 @@ public TokenizeError(@NotNull TokenizeErrorType type, int index) {
protected void handleTupleAlreadyOpen() {
this.fmt()
.setContent("Tuple already open.")
- .displayTokens(this.tokenIndex + 1);
+ .displayTokens(this.tokenIndex, 0, false);
}
@Handler("TUPLE_NOT_CLOSED")
@@ -40,7 +40,7 @@ protected void handleTupleNotClosed() {
protected void handleUnexpectedTupleClose() {
this.fmt()
.setContent("Unexpected tuple close.")
- .displayTokens(this.tokenIndex + 1);
+ .displayTokens(this.tokenIndex, 0, false);
}
@Handler("STRING_NOT_CLOSED")
diff --git a/src/test/java/lanat/test/exampleTests/ExampleTest.java b/src/test/java/lanat/test/exampleTests/ExampleTest.java
index 45a10f7c..32cbce9a 100644
--- a/src/test/java/lanat/test/exampleTests/ExampleTest.java
+++ b/src/test/java/lanat/test/exampleTests/ExampleTest.java
@@ -3,6 +3,7 @@
import lanat.*;
import lanat.argumentTypes.IntegerArgumentType;
import lanat.argumentTypes.NumberRangeArgumentType;
+import lanat.argumentTypes.StringArgumentType;
import lanat.utils.Range;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -12,13 +13,15 @@ public final class ExampleTest {
@Test
public void main() {
Argument.PrefixChar.defaultPrefix = Argument.PrefixChar.MINUS;
+// TextFormatter.enableSequences = false;
var parsedArgs = new ArgumentParser("my-program") {{
this.setCallbackInvocationOption(CallbacksInvocationOption.NO_ERROR_IN_ARGUMENT);
this.addHelpArgument();
this.addArgument(Argument.create(new Example1Type(), "user", "u").required().positional());
this.addArgument(Argument.create(new NumberRangeArgumentType<>(0.0, 15.23), "number").onOk(System.out::println));
+ this.addArgument(Argument.create(new StringArgumentType(), "string").onOk(System.out::println));
this.addArgument(Argument.create(new IntegerArgumentType(), "test").onOk(System.out::println).allowsUnique());
- }}.parse(CLInput.from("-h --number=3 -u [jo ]"))
+ }}.parse(CLInput.from("-h --number 3' --string 'hello world' -u ['[jo]"))
.printErrors()
.getParsedArguments();
From 57f34d4bbfd9fa3dbd49312afd23f41c821baa54 Mon Sep 17 00:00:00 2001
From: DarviL
Date: Fri, 3 Nov 2023 21:50:13 +0100
Subject: [PATCH 28/33] add: comment
---
src/main/java/lanat/parsing/Tokenizer.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java
index 7d216fd7..3023ad42 100644
--- a/src/main/java/lanat/parsing/Tokenizer.java
+++ b/src/main/java/lanat/parsing/Tokenizer.java
@@ -185,6 +185,10 @@ private void addToken(@NotNull TokenType type, char contents) {
this.finalTokens.add(new Token(type, String.valueOf(contents)));
}
+ /**
+ * Inserts an error at the current token index with the given type.
+ * @param type The type of the error to insert.
+ */
private void addError(@NotNull TokenizeError.TokenizeErrorType type) {
this.addError(type, this.finalTokens.size());
}
From fd254362f17a4f9b04973d4808d73f215611f7c4 Mon Sep 17 00:00:00 2001
From: DarviL
Date: Sat, 4 Nov 2023 17:35:45 +0100
Subject: [PATCH 29/33] add: Argument.PrefixChar#COMMON_PREFIXES. This is an
array of prefixes that a user will normally be familiar with. feat: add parse
error for when unmatched extra values are present in an argument name list.
fix: parse errors added by parsing of argument name lists not properly
highlighting tokens. fix: incorrect value number shown on a specific
incorrect value number error case. fix: token being skipped from input in a
case when parsing argument name lists. fix: error decorations not being
properly adapted with text on a lower line width limit. feat: updated
unmatched token error to be clearer. feat: add an info level error for when a
token has a correct argument name, but with an incorrect prefix. feat: made
argument name list checking more permissive by being less strict with which
prefixes you can use. Now also checks if the prefix used is any in
Argument.PrefixChar#COMMON_PREFIXES. fix: input incorrectly tokenized as a
name list in some cases. refactor: improved quality of parser/tokenizer
generally.
---
src/main/java/lanat/Argument.java | 6 ++
.../errorFormatterGenerators/Pretty.java | 9 +-
src/main/java/lanat/parsing/Parser.java | 77 +++++++++------
.../java/lanat/parsing/ParsingStateBase.java | 45 +++++++--
src/main/java/lanat/parsing/Tokenizer.java | 93 ++++++++++++++-----
.../java/lanat/parsing/errors/ParseError.java | 48 ++++++++--
.../lanat/parsing/errors/TokenizeError.java | 35 ++++++-
.../lanat/test/exampleTests/ExampleTest.java | 11 ++-
.../lanat/test/units/TestTerminalOutput.java | 2 +-
9 files changed, 246 insertions(+), 80 deletions(-)
diff --git a/src/main/java/lanat/Argument.java b/src/main/java/lanat/Argument.java
index 63150cdd..a5650406 100644
--- a/src/main/java/lanat/Argument.java
+++ b/src/main/java/lanat/Argument.java
@@ -217,6 +217,8 @@ public boolean isPositional() {
* Specify the prefix of this argument. By default, this is {@link PrefixChar#MINUS}. If this argument is used in an
* argument name list (-abc), the prefix that will be valid is any against all the arguments specified in that name
* list.
+ *
+ * Note that, for ease of use, the prefixes defined in {@link PrefixChar#COMMON_PREFIXES} are also valid.
*
* @param prefixChar the prefix that should be used for this argument.
* @see PrefixChar
@@ -669,6 +671,10 @@ public static class PrefixChar {
public final char character;
public static @NotNull PrefixChar defaultPrefix = PrefixChar.AUTO;
+ /** Prefixes that a user may be familiar with. */
+ public static final @NotNull PrefixChar[] COMMON_PREFIXES = { MINUS, SLASH };
+
+
private PrefixChar(char character) {
this.character = character;
}
diff --git a/src/main/java/lanat/errorFormatterGenerators/Pretty.java b/src/main/java/lanat/errorFormatterGenerators/Pretty.java
index f2995321..e42531e7 100644
--- a/src/main/java/lanat/errorFormatterGenerators/Pretty.java
+++ b/src/main/java/lanat/errorFormatterGenerators/Pretty.java
@@ -13,18 +13,19 @@
public class Pretty extends ErrorFormatter.Generator {
@Override
public @NotNull String generate() {
- // first figure out the length of the longest line
- final var maxLength = UtlString.getLongestLine(this.getContents()).length();
+ final var contents = this.getContentsWrapped();
final var formatter = this.getErrorLevelFormatter();
final String tokensFormatting = this.getTokensView();
+ final var longestLineLength = UtlString.getLongestLine(contents).length();
+
return formatter.withContents(" ┌─%s".formatted(this.getErrorLevel())).toString()
// only add a new line if there are tokens to display
+ (tokensFormatting.isEmpty() ? "" : "\n" + tokensFormatting)
// first insert a vertical bar at the start of each line
- + this.getContentsWrapped().replaceAll("^|\\n", formatter.withContents("\n │ ").toString())
+ + contents.replaceAll("^|\\n", formatter.withContents("\n │ ").toString())
// then insert a horizontal bar at the end, with the length of the longest line approximately
- + formatter.withContents("\n └" + "─".repeat(Math.max(maxLength - 5, 0)) + " ───── ── ─")
+ + formatter.withContents("\n └" + "─".repeat(Math.max(longestLineLength - 5, 0)) + " ───── ── ─")
+ '\n';
}
diff --git a/src/main/java/lanat/parsing/Parser.java b/src/main/java/lanat/parsing/Parser.java
index 062bdf5a..5b76f61e 100644
--- a/src/main/java/lanat/parsing/Parser.java
+++ b/src/main/java/lanat/parsing/Parser.java
@@ -213,11 +213,6 @@ private void executeArgParse(@NotNull Argument, ?> arg) {
private void executeArgParse(@NotNull Argument, ?> arg, @Nullable String value) {
final Range argumentValuesRange = arg.argType.getRequiredArgValueCount();
- if (value == null || value.isEmpty()) {
- this.executeArgParse(arg); // value is not present in the suffix of the argList. Continue parsing values.
- return;
- }
-
// just skip the whole thing if it doesn't need any values
if (argumentValuesRange.isZero()) {
arg.parseValues(this.currentTokenIndex);
@@ -225,10 +220,24 @@ private void executeArgParse(@NotNull Argument, ?> arg, @Nullable String value
}
if (argumentValuesRange.start() > 1) {
- this.addError(ParseError.ParseErrorType.ARG_INCORRECT_VALUE_NUMBER, arg, 0);
+ this.addError(
+ new ParseError(
+ ParseError.ParseErrorType.ARG_INCORRECT_VALUE_NUMBER,
+ this.currentTokenIndex + 1,
+ arg,
+ 1
+ ) {{
+ this.setIsInArgNameList(true); // set that the error was caused by an argument name list
+ }}
+ );
return;
}
+ if (value == null || value.isEmpty()) {
+ this.executeArgParse(arg); // value is not present in the suffix of the argList. Continue parsing values.
+ return;
+ }
+
// pass the arg values to the argument subParser
arg.parseValues(this.currentTokenIndex, value);
}
@@ -237,30 +246,44 @@ private void executeArgParse(@NotNull Argument, ?> arg, @Nullable String value
* Parses the given string as a list of single-char argument names.
*/
private void parseArgNameList(@NotNull String args) {
+ var doSkipToken = true; // atomic because we need to modify it in the lambda
+ Argument, ?> lastArgument = null;
+
// its multiple of them. We can only do this with arguments that accept 0 values.
for (short i = 0; i < args.length(); i++) {
- final short constIndex = i; // this is because the lambda requires the variable to be final
-
- if (!this.runForArgument(args.charAt(i), argument -> {
- // if the argument accepts 0 values, then we can just parse it like normal
- if (argument.argType.getRequiredArgValueCount().isZero()) {
- this.executeArgParse(argument);
-
- // -- arguments now may accept 1 or more values from now on:
-
- // if this argument is the last one in the list, then we can parse the next values after it
- } else if (constIndex == args.length() - 1) {
- this.currentTokenIndex++;
- this.executeArgParse(argument);
-
- // if this argument is not the last one in the list, then we can parse the rest of the chars as the value
- } else {
- this.executeArgParse(argument, args.substring(constIndex + 1));
- }
- }))
- return;
+ var argument = this.getArgument(args.charAt(i));
+
+ if (argument == null) {
+ this.addError(
+ ParseError.ParseErrorType.UNMATCHED_IN_ARG_NAME_LIST,
+ lastArgument,
+ i + 1, // substr for the current token
+ this.currentTokenIndex + 1 // the next token is the one that caused the error
+ );
+ break;
+ }
+
+ // if the argument accepts 0 values, then we can just parse it like normal
+ if (argument.argType.getRequiredArgValueCount().isZero()) {
+ this.executeArgParse(argument);
+
+ // -- arguments now may accept 1 or more values from now on:
+
+ // if this argument is the last one in the list, then we can parse the next values after it
+ } else if (i == args.length() - 1) {
+ this.currentTokenIndex++;
+ this.executeArgParse(argument);
+ doSkipToken = false; // we don't want to skip the next token because executeArgParse already did that
+
+ // if this argument is not the last one in the list, then we can parse the rest of the chars as the value
+ } else {
+ this.executeArgParse(argument, args.substring(i + 1));
+ }
+
+ lastArgument = argument;
}
- this.currentTokenIndex++;
+
+ if (doSkipToken) this.currentTokenIndex++;
}
/** Returns the positional argument at the given index of declaration. */
diff --git a/src/main/java/lanat/parsing/ParsingStateBase.java b/src/main/java/lanat/parsing/ParsingStateBase.java
index b14fea60..59d55bb4 100644
--- a/src/main/java/lanat/parsing/ParsingStateBase.java
+++ b/src/main/java/lanat/parsing/ParsingStateBase.java
@@ -5,6 +5,7 @@
import lanat.utils.ErrorLevelProvider;
import lanat.utils.ErrorsContainerImpl;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Consumer;
@@ -25,18 +26,17 @@ public ParsingStateBase(@NotNull Command command) {
* @return {@code true} if an argument was found
*/
protected boolean runForArgument(@NotNull String argName, @NotNull Consumer<@NotNull Argument, ?>> f) {
- for (final var argument : this.getArguments()) {
- if (argument.checkMatch(argName)) {
- f.accept(argument);
- return true;
- }
+ var arg = this.getArgument(argName);
+ if (arg != null) {
+ f.accept(arg);
+ return true;
}
return false;
}
/**
- * Executes a callback for the argument found by the name specified.
+ * Executes a callback for the argument found by the single character name specified.
*
* @return {@code true} if an argument was found
*/
@@ -47,13 +47,40 @@ protected boolean runForArgument(@NotNull String argName, @NotNull Consumer<@Not
* I don't really want to make "checkMatch" have different behavior depending on the length of the string, so
* an overload seems better. */
protected boolean runForArgument(char argName, @NotNull Consumer<@NotNull Argument, ?>> f) {
+ var arg = this.getArgument(argName);
+ if (arg != null) {
+ f.accept(arg);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the argument found by the single character name specified.
+ * @param argName the name of the argument to find
+ * @return the argument found, or {@code null} if no argument was found
+ */
+ protected @Nullable Argument, ?> getArgument(char argName) {
for (final var argument : this.getArguments()) {
if (argument.checkMatch(argName)) {
- f.accept(argument);
- return true;
+ return argument;
}
}
- return false;
+ return null;
+ }
+
+ /**
+ * Returns the argument found by the name specified.
+ * @param argName the name of the argument to find
+ * @return the argument found, or {@code null} if no argument was found
+ */
+ protected @Nullable Argument, ?> getArgument(String argName) {
+ for (final var argument : this.getArguments()) {
+ if (argument.checkMatch(argName)) {
+ return argument;
+ }
+ }
+ return null;
}
protected @NotNull List<@NotNull Argument, ?>> getArguments() {
diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java
index 3023ad42..9d827d91 100644
--- a/src/main/java/lanat/parsing/Tokenizer.java
+++ b/src/main/java/lanat/parsing/Tokenizer.java
@@ -1,12 +1,16 @@
package lanat.parsing;
+import lanat.Argument;
import lanat.Command;
import lanat.parsing.errors.TokenizeError;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.stream.Stream;
public class Tokenizer extends ParsingStateBase {
/** Are we currently within a tuple? */
@@ -38,11 +42,6 @@ public Tokenizer(@NotNull Command command) {
super(command);
}
- // ------------------------------------------------ Error Handling ------------------------------------------------
- void addError(@NotNull TokenizeError.TokenizeErrorType type, int index) {
- this.addError(new TokenizeError(type, index));
- }
- // ------------------------------------------------ ////////////// ------------------------------------------------
private void setInputString(@NotNull String inputString) {
this.inputString = inputString;
@@ -185,14 +184,6 @@ private void addToken(@NotNull TokenType type, char contents) {
this.finalTokens.add(new Token(type, String.valueOf(contents)));
}
- /**
- * Inserts an error at the current token index with the given type.
- * @param type The type of the error to insert.
- */
- private void addError(@NotNull TokenizeError.TokenizeErrorType type) {
- this.addError(type, this.finalTokens.size());
- }
-
/**
* Tokenizes a single word and returns the token matching it. If no match could be found, returns
* {@link TokenType#ARGUMENT_VALUE}
@@ -209,6 +200,7 @@ private void addError(@NotNull TokenizeError.TokenizeErrorType type) {
} else if (this.isSubCommand(str)) {
type = TokenType.COMMAND;
} else {
+ this.checkForSimilar(str);
type = TokenType.ARGUMENT_VALUE;
}
@@ -250,17 +242,26 @@ private void tokenizeCurrentValue() {
*
*/
private boolean isArgNameList(@NotNull String str) {
- if (str.length() < 2) return false;
-
- final var possiblePrefixes = new ArrayList();
- final var charArray = str.substring(1).toCharArray();
-
- for (final char argName : charArray) {
- if (!this.runForArgument(argName, argument -> possiblePrefixes.add(argument.getPrefix().character)))
+ if (str.length() < 2 || !Character.isAlphabetic(str.charAt(1))) return false;
+
+ // store the possible prefixes. Start with the common ones (single and double dash)
+ // We add the common prefixes because it can be confusing for the user to have to put a specific prefix
+ // used by any argument in the name list
+ final var possiblePrefixes = new HashSet<>(Arrays.asList(Argument.PrefixChar.COMMON_PREFIXES));
+ int foundArgs = 0; // how many characters in the string are valid arguments
+
+ // iterate over the characters in the string, starting from the second one (the first one is the prefix)
+ for (final char argName : str.substring(1).toCharArray()) {
+ // if an argument is found with that char name, append its prefix to the possible prefixes
+ // and increment the foundArgs counter.
+ // If no argument is found, stop checking
+ if (!this.runForArgument(argName, argument -> possiblePrefixes.add(argument.getPrefix())))
break;
+ foundArgs++;
}
- return possiblePrefixes.size() >= 1 && possiblePrefixes.contains(str.charAt(0));
+ // if there's at least one argument and the first character is a valid prefix, return true
+ return foundArgs >= 1 && possiblePrefixes.stream().anyMatch(p -> p.character == str.charAt(0));
}
/**
@@ -287,6 +288,35 @@ private boolean isSubCommand(@NotNull String str) {
return this.getCommands().stream().anyMatch(c -> c.hasName(str));
}
+ /**
+ * Checks if the given string is similar to any of the argument names.
+ *
+ * If so, adds an error to the error list.
+ * @param str The string to check.
+ */
+ private void checkForSimilar(@NotNull String str) {
+ // if the string is too short, don't bother checking
+ if (str.length() < 2) return;
+
+ // check for the common prefixes
+ Stream.of(Argument.PrefixChar.COMMON_PREFIXES)
+ .map(c -> c.character)
+ .forEach(checkPrefix -> {
+ // if not present, don't bother checking
+ if (str.charAt(0) != checkPrefix) return;
+
+ // get rid of the prefix (single or double)
+ final var nameToCheck = str.substring(str.charAt(1) == checkPrefix ? 2 : 1);
+
+ for (var arg : this.getArguments()) {
+ if (!arg.hasName(nameToCheck)) continue;
+
+ // offset 1 because this is called before a token is pushed
+ this.addError(TokenizeError.TokenizeErrorType.SIMILAR_ARGUMENT, arg, 1);
+ }
+ });
+ }
+
/**
* Returns {@code true} if the character of {@link Tokenizer#inputChars} at a relative index from
* {@link Tokenizer#currentCharIndex} is equal to the specified character.
@@ -339,4 +369,25 @@ private boolean isCharAtRelativeIndex(int index, char character) {
public boolean isFinishedTokenizing() {
return this.hasFinished;
}
+
+
+ // ------------------------------------------------ Error Handling ------------------------------------------------
+
+ /**
+ * Inserts an error at the current token index with the given type.
+ * @param type The type of the error to insert.
+ */
+ private void addError(@NotNull TokenizeError.TokenizeErrorType type) {
+ this.addError(type, null, 0);
+ }
+
+ /**
+ * Inserts an error at the current token index with the given type and argument.
+ * @param type The type of the error to insert.
+ * @param argument The argument that caused the error.
+ * @param indexOffset The offset from the current token index to the token that caused the error.
+ */
+ private void addError(@NotNull TokenizeError.TokenizeErrorType type, @Nullable Argument, ?> argument, int indexOffset) {
+ this.addError(new TokenizeError(type, this.finalTokens.size() + indexOffset, argument));
+ }
}
diff --git a/src/main/java/lanat/parsing/errors/ParseError.java b/src/main/java/lanat/parsing/errors/ParseError.java
index a3bf546c..ee0ba55b 100644
--- a/src/main/java/lanat/parsing/errors/ParseError.java
+++ b/src/main/java/lanat/parsing/errors/ParseError.java
@@ -11,19 +11,21 @@
@SuppressWarnings("unused")
public class ParseError extends ParseStateErrorBase {
- public final Argument, ?> argument;
+ public final @Nullable Argument, ?> argument;
private final int valueCount;
private boolean isInTuple = false;
+ private boolean isInArgNameList = false;
private ArgumentGroup argumentGroup;
public enum ParseErrorType implements ErrorLevelProvider {
REQUIRED_ARGUMENT_NOT_USED,
UNMATCHED_TOKEN(ErrorLevel.WARNING),
+ UNMATCHED_IN_ARG_NAME_LIST(ErrorLevel.WARNING),
ARG_INCORRECT_VALUE_NUMBER,
ARG_INCORRECT_USAGES_COUNT,
MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED;
- public final @NotNull ErrorLevel level;
+ private final @NotNull ErrorLevel level;
ParseErrorType() {
this.level = ErrorLevel.ERROR;
@@ -57,6 +59,14 @@ public void setIsInTuple(boolean isInTuple) {
this.isInTuple = isInTuple;
}
+ /**
+ * Sets whether the error was caused by an argument name list.
+ * @param isInArgNameList whether the error was caused by an argument name list
+ */
+ public void setIsInArgNameList(boolean isInArgNameList) {
+ this.isInArgNameList = isInArgNameList;
+ }
+
/**
* Returns the offset from the token index to the value tokens. Adds 2 if the error was caused while parsing values
* in a tuple.
@@ -79,8 +89,13 @@ protected void handleIncorrectValueNumber() {
this.argument.getName(), this.argument.argType.getRequiredArgValueCount().getMessage("value"),
this.valueCount
)
- )
- .displayTokens(
+ );
+
+ if (this.isInArgNameList)
+ // special case for when the error is caused by an argument name list
+ this.fmt().displayTokens(this.tokenIndex, 0, false);
+ else
+ this.fmt().displayTokens(
this.tokenIndex + inTupleOffset,
this.getValueTokensOffset() - inTupleOffset,
this.getValueTokensOffset() == 0
@@ -109,7 +124,7 @@ protected void handleRequiredArgumentNotUsed() {
this.fmt()
.setContent(
argCmd instanceof ArgumentParser
- ? "Required argument '%s' not used.".formatted(this.argument.getName())
+ ? "Required argument '" + this.argument.getName() + "' not used."
: "Required argument '%s' for command '%s' not used.".formatted(this.argument.getName(), argCmd.getName())
)
.displayTokens(this.tokenIndex);
@@ -118,8 +133,23 @@ protected void handleRequiredArgumentNotUsed() {
@Handler("UNMATCHED_TOKEN")
protected void handleUnmatchedToken() {
this.fmt()
- .setContent("Token '%s' does not correspond with a valid argument, value, or command."
- .formatted(this.getCurrentToken().contents())
+ .setContent(
+ "Token '"
+ + this.getCurrentToken().contents()
+ + "' does not correspond with a valid argument, argument list, value, or command."
+ )
+ .displayTokens(this.tokenIndex, 0, false);
+ }
+
+ // here we use valueCount as the offset to the unmatched token, to substr the token contents
+ @Handler("UNMATCHED_IN_ARG_NAME_LIST")
+ protected void handleUnmatchedInArgNameList() {
+ assert this.argument != null;
+
+ this.fmt()
+ .setContent(
+ "Argument '" + this.argument.getName() + "' does not take any values, but got '"
+ + this.getCurrentToken().contents().substring(this.valueCount) + "'."
)
.displayTokens(this.tokenIndex, 0, false);
}
@@ -127,9 +157,7 @@ protected void handleUnmatchedToken() {
@Handler("MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED")
protected void handleMultipleArgsInExclusiveGroupUsed() {
this.fmt()
- .setContent("Multiple arguments in exclusive group '%s' used."
- .formatted(this.argumentGroup.getName())
- )
+ .setContent("Multiple arguments in exclusive group '" + this.argumentGroup.getName() + "' used.")
.displayTokens(this.tokenIndex, this.getValueTokensOffset(), false);
}
}
diff --git a/src/main/java/lanat/parsing/errors/TokenizeError.java b/src/main/java/lanat/parsing/errors/TokenizeError.java
index 7122a01b..cbb07091 100644
--- a/src/main/java/lanat/parsing/errors/TokenizeError.java
+++ b/src/main/java/lanat/parsing/errors/TokenizeError.java
@@ -1,25 +1,41 @@
package lanat.parsing.errors;
+import lanat.Argument;
import lanat.ErrorLevel;
import lanat.utils.ErrorLevelProvider;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
@SuppressWarnings("unused")
public class TokenizeError extends ParseStateErrorBase {
+ private final @Nullable Argument, ?> argument;
+
public enum TokenizeErrorType implements ErrorLevelProvider {
TUPLE_ALREADY_OPEN,
UNEXPECTED_TUPLE_CLOSE,
TUPLE_NOT_CLOSED,
- STRING_NOT_CLOSED;
+ STRING_NOT_CLOSED,
+ SIMILAR_ARGUMENT(ErrorLevel.INFO);
+
+ private final @NotNull ErrorLevel level;
+
+ TokenizeErrorType() {
+ this.level = ErrorLevel.ERROR;
+ }
+
+ TokenizeErrorType(@NotNull ErrorLevel level) {
+ this.level = level;
+ }
@Override
public @NotNull ErrorLevel getErrorLevel() {
- return ErrorLevel.ERROR;
+ return this.level;
}
}
- public TokenizeError(@NotNull TokenizeErrorType type, int index) {
+ public TokenizeError(@NotNull TokenizeErrorType type, int index, @Nullable Argument, ?> argument) {
super(type, index);
+ this.argument = argument;
}
@Handler("TUPLE_ALREADY_OPEN")
@@ -49,4 +65,17 @@ protected void handleStringNotClosed() {
.setContent("String not closed.")
.displayTokens(this.tokenIndex + 1);
}
+
+ @Handler("SIMILAR_ARGUMENT")
+ protected void handleSimilarArgument() {
+ assert this.argument != null;
+
+ this.fmt()
+ .setContent(
+ "Found argument with name given, but with a different prefix ("
+ + this.argument.getPrefix().character
+ + ")."
+ )
+ .displayTokens(this.tokenIndex, 0, false);
+ }
}
diff --git a/src/test/java/lanat/test/exampleTests/ExampleTest.java b/src/test/java/lanat/test/exampleTests/ExampleTest.java
index 32cbce9a..e3512d31 100644
--- a/src/test/java/lanat/test/exampleTests/ExampleTest.java
+++ b/src/test/java/lanat/test/exampleTests/ExampleTest.java
@@ -1,6 +1,7 @@
package lanat.test.exampleTests;
import lanat.*;
+import lanat.argumentTypes.CounterArgumentType;
import lanat.argumentTypes.IntegerArgumentType;
import lanat.argumentTypes.NumberRangeArgumentType;
import lanat.argumentTypes.StringArgumentType;
@@ -14,18 +15,18 @@ public final class ExampleTest {
public void main() {
Argument.PrefixChar.defaultPrefix = Argument.PrefixChar.MINUS;
// TextFormatter.enableSequences = false;
- var parsedArgs = new ArgumentParser("my-program") {{
+ new ArgumentParser("my-program") {{
this.setCallbackInvocationOption(CallbacksInvocationOption.NO_ERROR_IN_ARGUMENT);
this.addHelpArgument();
+ this.addArgument(Argument.create(new CounterArgumentType(), "counter", "c").onOk(System.out::println));
this.addArgument(Argument.create(new Example1Type(), "user", "u").required().positional());
+ this.addArgument(Argument.createOfBoolType("t").onOk(v -> System.out.println("present")));
this.addArgument(Argument.create(new NumberRangeArgumentType<>(0.0, 15.23), "number").onOk(System.out::println));
- this.addArgument(Argument.create(new StringArgumentType(), "string").onOk(System.out::println));
+ this.addArgument(Argument.create(new StringArgumentType(), "string", "s").onOk(System.out::println).withPrefix(Argument.PrefixChar.PLUS));
this.addArgument(Argument.create(new IntegerArgumentType(), "test").onOk(System.out::println).allowsUnique());
- }}.parse(CLInput.from("-h --number 3' --string 'hello world' -u ['[jo]"))
+ }}.parse(CLInput.from("-h --number 3' --c -c --c -cccelloc ++string hello -ccc"))
.printErrors()
.getParsedArguments();
-
- System.out.println(parsedArgs);
}
public static class Example1Type extends ArgumentType {
diff --git a/src/test/java/lanat/test/units/TestTerminalOutput.java b/src/test/java/lanat/test/units/TestTerminalOutput.java
index f03cb849..ddb3cbea 100644
--- a/src/test/java/lanat/test/units/TestTerminalOutput.java
+++ b/src/test/java/lanat/test/units/TestTerminalOutput.java
@@ -94,7 +94,7 @@ public void testUnmatchedToken() {
this.assertErrorOutput("[foo] --unknown", """
WARNING
Testing [ foo ] --unknown <-
- Token '--unknown' does not correspond with a valid argument, value, or command.""");
+ Token '--unknown' does not correspond with a valid argument, argument list, value, or command.""");
}
@Test
From 8c8764e8558533664b551396456d4bad175862a8 Mon Sep 17 00:00:00 2001
From: DarviL
Date: Sat, 4 Nov 2023 18:13:32 +0100
Subject: [PATCH 30/33] add: terminal output test for argument group
exclusivity
---
src/test/java/lanat/test/UnitTests.java | 8 +++++++-
src/test/java/lanat/test/units/TestTerminalOutput.java | 9 +++++++++
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/test/java/lanat/test/UnitTests.java b/src/test/java/lanat/test/UnitTests.java
index aed01e73..c07f2ba3 100644
--- a/src/test/java/lanat/test/UnitTests.java
+++ b/src/test/java/lanat/test/UnitTests.java
@@ -1,6 +1,7 @@
package lanat.test;
import lanat.Argument;
+import lanat.ArgumentGroup;
import lanat.ArgumentType;
import lanat.Command;
import lanat.argumentTypes.CounterArgumentType;
@@ -80,7 +81,12 @@ protected TestingParser setParser() {
this.addCommand(new Command("subCommand2") {{
this.setErrorCode(0b1000);
- this.addArgument(Argument.create(new IntegerArgumentType(), 'c').positional());
+
+ this.addGroup(new ArgumentGroup("exclusive-group") {{
+ this.setExclusive(true);
+ this.addArgument(Argument.createOfBoolType("extra"));
+ this.addArgument(Argument.create(new IntegerArgumentType(), 'c').positional());
+ }});
}});
}};
}
diff --git a/src/test/java/lanat/test/units/TestTerminalOutput.java b/src/test/java/lanat/test/units/TestTerminalOutput.java
index ddb3cbea..c4fefea7 100644
--- a/src/test/java/lanat/test/units/TestTerminalOutput.java
+++ b/src/test/java/lanat/test/units/TestTerminalOutput.java
@@ -112,4 +112,13 @@ public void testIncorrectUsageCount() {
Argument 'double-adder' was used an incorrect amount of times.
Expected from 2 to 4 usages, but was used 5 times.""");
}
+
+ @Test
+ @DisplayName("Test group exclusivity error")
+ public void testGroupExclusivityError() {
+ this.assertErrorOutput("foo subCommand2 --extra --c 5", """
+ ERROR
+ Testing foo subCommand2 --extra -> --c 5 <-
+ Multiple arguments in exclusive group 'exclusive-group' used.""");
+ }
}
\ No newline at end of file
From dd1ff3a708a903a70a8c4de73164855e2f46cde9 Mon Sep 17 00:00:00 2001
From: David L <48654552+DarviL82@users.noreply.github.com>
Date: Mon, 6 Nov 2023 01:45:33 +0100
Subject: [PATCH 31/33] add logo to README
---
README.md | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 658a36b7..cd25b992 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,16 @@
-# Lanat
+
+
+
+
+
+
+ A command line argument parser for Java 17 with
+ ease of use and high customization possibilities in mind.
+
+
+
+
-Lanat is a command line argument parser for Java 17 with ease of use and high customization
-possibilities in mind.
### Examples
Here is an example of a simple argument parser definition.
@@ -80,9 +89,9 @@ The package is currently only available on GitHub Packages.
```kotlin
implementation("darvil:lanat")
- ```
-
- Note that you may need to explicitly specify the version of the package you want to use. (e.g. `darvil:lanat:x.x.x`)
+ ```
+ > [!NOTE]
+ > You may need to explicitly specify the version of the package you want to use. (e.g. `darvil:lanat:x.x.x`).
This information is available at the [GitHub Packages documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package).
From c42b5a0612b5e221f9f2fdec96813a9119351be4 Mon Sep 17 00:00:00 2001
From: David L <48654552+DarviL82@users.noreply.github.com>
Date: Mon, 6 Nov 2023 01:51:09 +0100
Subject: [PATCH 32/33] update example in README
---
README.md | 79 +++++++++++++++++++++++++++++--------------------------
1 file changed, 42 insertions(+), 37 deletions(-)
diff --git a/README.md b/README.md
index cd25b992..06084cf1 100644
--- a/README.md
+++ b/README.md
@@ -12,51 +12,56 @@
-### Examples
-Here is an example of a simple argument parser definition.
-
-```java
-@Command.Define
-class MyProgram {
- @Argument.Define(required = true, positional = true, description = "The name of the user.")
- public String name;
-
- @Argument.Define(argType = StringArgumentType.class, description = "The surname of the user.")
- public Optional surname;
-
- @Argument.Define(names = {"age", "a"}, description = "The age of the user.", prefix = '+')
- public int age = 18;
+### Example
+- First, we define our Command by creating a *Command Template*.
- @InitDef
- public static void beforeInit(@NotNull CommandBuildHelper cmdBuildHelper) {
- // configure the argument "age" to have an argument type of
- // number range and set the range to 1-100
- cmdBuildHelper., Integer>getArgument("age")
- .withArgType(new NumberRangeArgumentType<>(1, 100))
- .onOk(v -> System.out.println("The age is valid!"));
- }
-}
-
-class Test {
- public static void main(String[] args) {
- // example: david +a20
- var myProgram = ArgumentParser.parseFromInto(MyProgram.class, CLInput.from(args));
+ ```java
+ @Command.Define
+ class MyProgram {
+ @Argument.Define(required = true, positional = true, description = "The name of the user.")
+ public String name;
+
+ @Argument.Define(argType = StringArgumentType.class, description = "The surname of the user.")
+ public Optional surname;
+
+ @Argument.Define(names = {"age", "a"}, description = "The age of the user.", prefix = '+')
+ public int age = 18;
- System.out.printf(
- "Welcome %s! You are %d years old.%n",
- myProgram.name, myProgram.age
- );
-
- // if no surname was specified, we'll show "none" instead
- System.out.printf("The surname of the user is %s.%n", myProgram.surname.orElse("none"));
+ @InitDef
+ public static void beforeInit(@NotNull CommandBuildHelper cmdBuildHelper) {
+ // configure the argument "age" to have an argument type of
+ // number range and set the range to 1-100
+ cmdBuildHelper., Integer>getArgument("age")
+ .withArgType(new NumberRangeArgumentType<>(1, 100))
+ .onOk(v -> System.out.println("The age is valid!"));
+ }
+ }
+ ```
+
+ - Then, let that class definition also serve as the container for the parsed values.
+ ```java
+ class Test {
+ public static void main(String[] args) {
+ // example: david +a20
+ var myProgram = ArgumentParser.parseFromInto(MyProgram.class, CLInput.from(args));
+
+ System.out.printf(
+ "Welcome %s! You are %d years old.%n",
+ myProgram.name, myProgram.age
+ );
+
+ // if no surname was specified, we'll show "none" instead
+ System.out.printf("The surname of the user is %s.%n", myProgram.surname.orElse("none"));
+ }
}
-}
-```
+ ```
## Documentation
Javadoc documentation for the latest stable version is available [here](https://darvil82.github.io/Lanat/).
+Deep documentation and tutorials comming soon.
+
## Installation
From 2798ed7bd9fe0ba243ee85701859458e217789e7 Mon Sep 17 00:00:00 2001
From: DarviL
Date: Mon, 6 Nov 2023 01:53:30 +0100
Subject: [PATCH 33/33] update: version number
---
build.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index 15dcd597..cc7ebef4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,7 +4,7 @@ plugins {
}
group = "darvil"
-version = "0.0.3"
+version = "0.1.0"
description = "Command line argument parser"
dependencies {