();
- final var charArray = str.substring(1).toCharArray();
-
- for (final char argName : charArray) {
- if (!this.runForArgument(argName, a -> possiblePrefixes.add(a.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));
}
/**
@@ -276,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.
@@ -290,9 +331,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);
}
/**
@@ -329,4 +369,25 @@ private Command getSubCommandByName(@NotNull String name) {
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/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..ee0ba55b 100644
--- a/src/main/java/lanat/parsing/errors/ParseError.java
+++ b/src/main/java/lanat/parsing/errors/ParseError.java
@@ -9,23 +9,23 @@
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;
+ 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;
@@ -51,37 +51,55 @@ 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;
+ }
+
+ /**
+ * 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;
+ }
- 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")
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(
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);
+ );
+
+ 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
+ );
}
@Handler("ARG_INCORRECT_USAGES_COUNT")
@@ -95,7 +113,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")
@@ -106,27 +124,40 @@ 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 + 1);
+ .displayTokens(this.tokenIndex);
}
@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, this.valueCount, false);
+ .displayTokens(this.tokenIndex, 0, false);
}
@Handler("MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED")
protected void handleMultipleArgsInExclusiveGroupUsed() {
this.fmt()
- .setContent("Multiple arguments in exclusive group '%s' used."
- .formatted(this.argumentGroup.getName())
- )
- .displayTokens(this.tokenIndex, this.valueCount, false);
+ .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 9122be83..cbb07091 100644
--- a/src/main/java/lanat/parsing/errors/TokenizeError.java
+++ b/src/main/java/lanat/parsing/errors/TokenizeError.java
@@ -1,32 +1,48 @@
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")
protected void handleTupleAlreadyOpen() {
this.fmt()
.setContent("Tuple already open.")
- .displayTokens(this.tokenIndex + 1);
+ .displayTokens(this.tokenIndex, 0, false);
}
@Handler("TUPLE_NOT_CLOSED")
@@ -40,7 +56,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")
@@ -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/main/java/lanat/utils/ErrorsContainerImpl.java b/src/main/java/lanat/utils/ErrorsContainerImpl.java
index fb9c6be9..cae4c1f0 100644
--- a/src/main/java/lanat/utils/ErrorsContainerImpl.java
+++ b/src/main/java/lanat/utils/ErrorsContainerImpl.java
@@ -13,11 +13,14 @@
* @param The type of the errors to store.
*/
public abstract class ErrorsContainerImpl implements ErrorsContainer {
- private @NotNull ModifyRecord minimumExitErrorLevel = ModifyRecord.of(ErrorLevel.ERROR);
- private @NotNull ModifyRecord minimumDisplayErrorLevel = ModifyRecord.of(ErrorLevel.INFO);
+ private final ModifyRecord minimumExitErrorLevel;
+ private final ModifyRecord minimumDisplayErrorLevel;
private final @NotNull List errors = new ArrayList<>();
- public ErrorsContainerImpl() {}
+ public ErrorsContainerImpl() {
+ // default values
+ this(ModifyRecord.of(ErrorLevel.ERROR), ModifyRecord.of(ErrorLevel.INFO));
+ }
public ErrorsContainerImpl(
@NotNull ModifyRecord minimumExitErrorLevelRecord,
@@ -28,11 +31,6 @@ public ErrorsContainerImpl(
this.minimumDisplayErrorLevel = minimumDisplayErrorLevelRecord;
}
- public ErrorsContainerImpl(@NotNull ErrorLevel minimumExitErrorLevel, @NotNull ErrorLevel minimumDisplayErrorLevel) {
- this.minimumExitErrorLevel.set(minimumExitErrorLevel);
- this.minimumDisplayErrorLevel.set(minimumDisplayErrorLevel);
- }
-
/**
* Adds an error to the list of errors.
*
@@ -68,7 +66,7 @@ public boolean hasDisplayErrors() {
}
private boolean errorIsInMinimumLevel(@NotNull TErr error, boolean isDisplayError) {
- return error.getErrorLevel().isInErrorMinimum((
+ return error.getErrorLevel().isInMinimum((
isDisplayError
? this.minimumDisplayErrorLevel
: this.minimumExitErrorLevel
diff --git a/src/main/java/lanat/utils/ModifyRecord.java b/src/main/java/lanat/utils/ModifyRecord.java
index a3b406d7..c49d5332 100644
--- a/src/main/java/lanat/utils/ModifyRecord.java
+++ b/src/main/java/lanat/utils/ModifyRecord.java
@@ -77,9 +77,7 @@ public void setIfNotModified(T value) {
* @param value The value to set.
*/
public void setIfNotModified(@NotNull ModifyRecord value) {
- if (!this.modified) {
- this.set(value);
- }
+ this.setIfNotModified(value.value);
}
/**
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 0965714c..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("");
}
/**
@@ -252,4 +252,21 @@ public static boolean isNullOrEmpty(@Nullable String str) {
public static @NotNull String @NotNull [] split(@NotNull String str, char splitter) {
return UtlString.split(str, String.valueOf(splitter), -1);
}
+
+ /**
+ * Returns a pair of strings. The first string is the leading whitespace of the string given, and the second string
+ * is the string given without the leading whitespace.
+ * @param str the string to split
+ * @return a pair of strings
+ */
+ public static @NotNull Pair<@NotNull String, @NotNull String> splitAtLeadingWhitespace(@NotNull String str) {
+ final var buffWhitespace = new StringBuilder();
+
+ for (char chr : str.toCharArray()) {
+ if (!Character.isWhitespace(chr)) break;
+ buffWhitespace.append(chr);
+ }
+
+ return new Pair<>(buffWhitespace.toString(), str.substring(buffWhitespace.length()));
+ }
}
diff --git a/src/main/java/lanat/utils/displayFormatter/Color.java b/src/main/java/lanat/utils/displayFormatter/Color.java
index c5d01501..9c36e2d3 100644
--- a/src/main/java/lanat/utils/displayFormatter/Color.java
+++ b/src/main/java/lanat/utils/displayFormatter/Color.java
@@ -2,8 +2,6 @@
import org.jetbrains.annotations.NotNull;
-import java.util.List;
-
/**
* Enumerates the ANSI color codes that a terminal can normally display.
*/
@@ -60,7 +58,7 @@ public String toString() {
/**
* Immutable list of all the dark colors.
*/
- public static final @NotNull List BRIGHT_COLORS = List.of(
+ public static final @NotNull Color[] BRIGHT_COLORS = {
BRIGHT_RED,
BRIGHT_GREEN,
BRIGHT_YELLOW,
@@ -68,12 +66,12 @@ public String toString() {
BRIGHT_MAGENTA,
BRIGHT_CYAN,
BRIGHT_WHITE
- );
+ };
/**
* Immutable list of all the bright colors.
*/
- public static final @NotNull List DARK_COLORS = List.of(
+ public static final @NotNull Color[] DARK_COLORS = {
RED,
GREEN,
YELLOW,
@@ -81,5 +79,5 @@ public String toString() {
MAGENTA,
CYAN,
WHITE
- );
+ };
}
\ No newline at end of file
diff --git a/src/main/java/lanat/utils/displayFormatter/FormatOption.java b/src/main/java/lanat/utils/displayFormatter/FormatOption.java
index 4dd14069..450c284f 100644
--- a/src/main/java/lanat/utils/displayFormatter/FormatOption.java
+++ b/src/main/java/lanat/utils/displayFormatter/FormatOption.java
@@ -12,9 +12,9 @@ public enum FormatOption {
/** Resets all formatting. Including colors. */
RESET_ALL(0),
BOLD(1),
- ITALIC(3),
/** Makes the text dimmer. */
DIM(2),
+ ITALIC(3),
UNDERLINE(4),
/** Makes the text blink. */
BLINK(5),
@@ -54,6 +54,10 @@ public enum FormatOption {
*/
public @NotNull String reset() {
// for some reason, bold is 21 instead of 20
- return TextFormatter.getSequence(this.value + 20 + (this == BOLD ? 1 : 0));
+ return TextFormatter.getSequence(
+ this == RESET_ALL
+ ? this.value // RESET_ALL should be the same when resetting
+ : this.value + 20 + (this == BOLD ? 1 : 0)
+ );
}
}
\ No newline at end of file
diff --git a/src/main/java/lanat/utils/displayFormatter/TextFormatter.java b/src/main/java/lanat/utils/displayFormatter/TextFormatter.java
index 5f8958af..4ca9b48f 100644
--- a/src/main/java/lanat/utils/displayFormatter/TextFormatter.java
+++ b/src/main/java/lanat/utils/displayFormatter/TextFormatter.java
@@ -1,5 +1,6 @@
package lanat.utils.displayFormatter;
+import lanat.utils.UtlString;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -23,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;
@@ -73,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.
@@ -144,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!
}
/**
@@ -169,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)
@@ -185,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) {
@@ -245,22 +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();
- buffer.append(this.getStartSequences());
- buffer.append(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);
+ }
+ // then do the same thing for the concatenated formatters
for (TextFormatter subFormatter : this.concatList) {
- buffer.append(subFormatter);
+ buff.append(subFormatter);
}
- buffer.append(this.getEndSequences());
+ buff.append(this.getEndSequences());
- return buffer.toString();
+ return buff.toString();
+ }
+
+ /**
+ * 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();
+
+ // start by adding the leading whitespace
+ buff.append(split.first());
+
+ // then add the start sequences
+ buff.append(startSequences);
+
+ char[] charArray = split.second().toCharArray();
+ for (int i = 0; i < charArray.length; i++) {
+ var chr = charArray[i];
+
+ // 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 */
@@ -280,7 +321,7 @@ public boolean formattingNotDefined() {
*/
static @NotNull String getSequence(int code) {
if (TextFormatter.debug)
- return "ESC[" + code;
+ return "ESC[" + code + "]";
return "" + ESC + '[' + code + 'm';
}
diff --git a/src/test/java/lanat/test/UnitTests.java b/src/test/java/lanat/test/UnitTests.java
index 65f3c366..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;
@@ -51,6 +52,9 @@ public class UnitTests {
static {
HelpFormatter.lineWrapMax = 1000; // just so we don't have to worry about line wrapping
TextFormatter.enableSequences = false; // just so we don't have to worry about color codes
+
+ // prefix char is set to auto by default (make sure tests run in windows too)
+ Argument.PrefixChar.defaultPrefix = Argument.PrefixChar.MINUS;
}
protected TestingParser setParser() {
@@ -77,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/manualTests/CommandTemplateExample.java b/src/test/java/lanat/test/exampleTests/CommandTemplateExample.java
similarity index 98%
rename from src/test/java/lanat/test/manualTests/CommandTemplateExample.java
rename to src/test/java/lanat/test/exampleTests/CommandTemplateExample.java
index c91fde58..0e9b275e 100644
--- a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java
+++ b/src/test/java/lanat/test/exampleTests/CommandTemplateExample.java
@@ -1,4 +1,4 @@
-package lanat.test.manualTests;
+package lanat.test.exampleTests;
import lanat.Argument;
import lanat.ArgumentGroup;
diff --git a/src/test/java/lanat/test/exampleTests/ExampleTest.java b/src/test/java/lanat/test/exampleTests/ExampleTest.java
new file mode 100644
index 00000000..e3512d31
--- /dev/null
+++ b/src/test/java/lanat/test/exampleTests/ExampleTest.java
@@ -0,0 +1,48 @@
+package lanat.test.exampleTests;
+
+import lanat.*;
+import lanat.argumentTypes.CounterArgumentType;
+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;
+import org.junit.jupiter.api.Test;
+
+public final class ExampleTest {
+ @Test
+ public void main() {
+ Argument.PrefixChar.defaultPrefix = Argument.PrefixChar.MINUS;
+// TextFormatter.enableSequences = false;
+ 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", "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' --c -c --c -cccelloc ++string hello -ccc"))
+ .printErrors()
+ .getParsedArguments();
+ }
+
+ 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 @NotNull Range getRequiredArgValueCount() {
+ return Range.from(2).toInfinity();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java
deleted file mode 100644
index 54516626..00000000
--- a/src/test/java/lanat/test/manualTests/ManualTests.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package lanat.test.manualTests;
-
-import lanat.ArgumentParser;
-import lanat.CLInput;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.util.Arrays;
-
-public final class ManualTests {
- @Test
- public void main() {
- String input = " ";
-
- // write some stuff to stdin
- System.setIn(new ByteArrayInputStream("hello world\ngoodbye".getBytes()));
-
- var parsed = ArgumentParser.parseFromInto(
- CommandTemplateExample.class,
- CLInput.from(input),
- o -> o.exitIfErrors()
- .printErrors()
- .printHelpIfNoInput()
- );
-
- parsed.string
- .ifPresentOrElse(
- s -> System.out.println("String is present: " + s),
- () -> System.out.println("String is not present")
- );
-
- System.out.println(parsed.number);
- System.out.println(parsed.subCommand.counter);
- System.out.println(parsed.subCommand.anotherSubCommand.counter);
- System.out.println(parsed.stdin);
- System.out.println(Arrays.toString(parsed.bytes));
- }
-}
\ 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..c4fefea7 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
@@ -40,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() {
@@ -74,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.""");
}
@@ -84,7 +84,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,8 +93,8 @@ public void testInvalidArgumentTypeValue() {
public void testUnmatchedToken() {
this.assertErrorOutput("[foo] --unknown", """
WARNING
- Testing [ foo ] --unknown
- Token '--unknown' does not correspond with a valid argument, value, or command.""");
+ Testing [ foo ] --unknown <-
+ Token '--unknown' does not correspond with a valid argument, argument list, value, or command.""");
}
@Test
@@ -102,14 +102,23 @@ 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.""");
}
+
+ @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
diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java
index 8f9b6c4f..5eb3e780 100644
--- a/src/test/java/module-info.java
+++ b/src/test/java/module-info.java
@@ -4,7 +4,7 @@
requires org.jetbrains.annotations;
exports lanat.test to org.junit.platform.commons, lanat;
- exports lanat.test.manualTests to org.junit.platform.commons, lanat;
+ exports lanat.test.exampleTests to org.junit.platform.commons, lanat;
exports lanat.test.units to lanat, org.junit.platform.commons;
exports lanat.test.units.commandTemplates to lanat, org.junit.platform.commons;
}
\ No newline at end of file