diff --git a/README.md b/README.md index 1d470a1a..1a83a354 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The package is currently only available on GitHub Packages. implementation("darvil:lanat") ``` - Note that you may need to explicitly specify the version of the package you want to use. (e.g. `darvil:lanat:0.0.1`) + Note that 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). diff --git a/build.gradle.kts b/build.gradle.kts index b631a142..15dcd597 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "darvil" -version = "0.0.2" +version = "0.0.3" description = "Command line argument parser" dependencies { diff --git a/src/main/java/lanat/Argument.java b/src/main/java/lanat/Argument.java index ded72661..a5cf726b 100644 --- a/src/main/java/lanat/Argument.java +++ b/src/main/java/lanat/Argument.java @@ -476,7 +476,7 @@ public void parseValues(short tokenIndex, @NotNull String... values) { */ public @Nullable TInner finishParsing() { final TInner finalValue = this.argType.getFinalValue(); - final TInner defaultValue = this.defaultValue == null ? this.argType.getInitialValue() : this.defaultValue; + final TInner defaultValue = UtlMisc.nonNullOrElse(this.defaultValue, this.argType.getInitialValue()); /* no, | is not a typo. We don't want the OR operator to short-circuit, we want all of them to be evaluated * because the methods have side effects (they add errors to the parser) */ @@ -597,6 +597,7 @@ void invokeCallbacks(@Nullable Object okValue) { */ @Override public boolean equals(@NotNull Object obj) { + if (obj == this) return true; if (obj instanceof Argument arg) return UtlMisc.equalsByNamesAndParentCmd(this, arg); return false; diff --git a/src/main/java/lanat/ArgumentAdder.java b/src/main/java/lanat/ArgumentAdder.java index dcfc52bd..2f799e54 100644 --- a/src/main/java/lanat/ArgumentAdder.java +++ b/src/main/java/lanat/ArgumentAdder.java @@ -39,7 +39,7 @@ void addArgument(@NotNull ArgumentBuilder argument) { @NotNull List<@NotNull Argument> getArguments(); /** - * Checks that all the arguments in this container have unique names. + * Checks that all the arguments in this container are unique. * @throws ArgumentAlreadyExistsException if there are two arguments with the same name */ default void checkUniqueArguments() { diff --git a/src/main/java/lanat/ArgumentBuilder.java b/src/main/java/lanat/ArgumentBuilder.java index 09ee1f15..5c20e4ea 100644 --- a/src/main/java/lanat/ArgumentBuilder.java +++ b/src/main/java/lanat/ArgumentBuilder.java @@ -1,6 +1,7 @@ package lanat; import lanat.argumentTypes.DummyArgumentType; +import lanat.exceptions.ArgumentTypeInferException; import lanat.utils.UtlReflection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -47,16 +48,17 @@ public class ArgumentBuilder, TInner> { if (annotation.argType() != DummyArgumentType.class) return UtlReflection.instantiate(annotation.argType()); - // try to infer the type from the field type - var argTypeMap = ArgumentType.getTypeInfer(field.getType()); - - // if the type was not found, return null - return argTypeMap == null ? null : UtlReflection.instantiate(argTypeMap); + // try to infer the type from the field type. If it can't be inferred, return null + try { + return ArgumentTypeInfer.get(field.getType()); + } catch (ArgumentTypeInferException e) { + return null; + } } /** * Builds an {@link Argument} from the specified field annotated with {@link Argument.Define}. - * Note that this doesn't set the argument type. + * Note that this doesn't set the argument type. Use {@link #setArgTypeFromField(Field)} for that. * * @param field the field that will be used to build the argument * @param the {@link ArgumentType} subclass that will parse the value passed to the argument diff --git a/src/main/java/lanat/ArgumentGroup.java b/src/main/java/lanat/ArgumentGroup.java index fcc230a8..9053157e 100644 --- a/src/main/java/lanat/ArgumentGroup.java +++ b/src/main/java/lanat/ArgumentGroup.java @@ -75,8 +75,8 @@ public class ArgumentGroup * each one added to this group is because at parsing, we might need to know which arguments were used in this * group. *

- * Sure, we could just use {@link Command#arguments}, but that would mean that we would have to iterate through all - * the arguments in there for filtering ours, which is probably worse. + * Sure, we could just use {@link Command#getArguments()}, but that would mean that we would have + * to iterate through all the arguments in there for filtering ours, which is probably worse. */ private final @NotNull List<@NotNull Argument> arguments = new ArrayList<>(); @@ -274,6 +274,7 @@ public ArgumentGroup getParent() { @Override public boolean equals(@NotNull Object obj) { + if (obj == this) return true; if (obj instanceof ArgumentGroup group) return this.parentCommand == group.parentCommand && this.name.equals(group.name); return false; diff --git a/src/main/java/lanat/ArgumentGroupAdder.java b/src/main/java/lanat/ArgumentGroupAdder.java index f9cda9d8..6f568dda 100644 --- a/src/main/java/lanat/ArgumentGroupAdder.java +++ b/src/main/java/lanat/ArgumentGroupAdder.java @@ -24,7 +24,7 @@ public interface ArgumentGroupAdder extends NamedWithDescription { @NotNull List<@NotNull ArgumentGroup> getGroups(); /** - * Checks that all the argument groups in this container have unique names. + * Checks that all the argument groups in this container are unique. * @throws ArgumentGroupAlreadyExistsException if there are two argument groups with the same name */ default void checkUniqueGroups() { diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index 14d0368c..25ff4314 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -4,10 +4,12 @@ import lanat.exceptions.IncompatibleCommandTemplateType; import lanat.parsing.TokenType; import lanat.parsing.errors.ErrorHandler; +import lanat.utils.UtlMisc; import lanat.utils.UtlReflection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -131,7 +133,11 @@ public static ArgumentParser from(@NotNull Class temp */ public static @NotNull T parseFromInto(@NotNull Class templateClass, @NotNull CLInput input) { - return ArgumentParser.parseFromInto(templateClass, input, opts -> opts.printErrors().exitIfErrors()); + return ArgumentParser.parseFromInto( + templateClass, + input, + opts -> opts.printErrors().exitIfErrors().printHelpIfNoInput().exitIfNoInput() + ); } /** @@ -191,7 +197,7 @@ public static ArgumentParser from(@NotNull Class temp return new ParsedArgumentsRoot( this, this.getParser().getParsedArgumentsHashMap(), - this.subCommands.stream().map(Command::getParsedArguments).toList(), + this.getCommands().stream().map(Command::getParsedArguments).toList(), this.getForwardValue() ); } @@ -239,7 +245,9 @@ public void setVersion(@NotNull String version) { */ public void addVersionArgument() { this.addArgument(Argument.createOfBoolType("version") - .onOk(t -> System.out.println("Version: " + this.getVersion())) + .onOk(t -> + System.out.println("Version: " + UtlMisc.nonNullOrElse(this.getVersion(), "unknown")) + ) .withDescription("Shows the version of this program.") .allowsUnique() ); @@ -366,7 +374,7 @@ private static T into( // 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); + 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) @@ -379,7 +387,7 @@ private static T into( instance, f.getType().isAssignableFrom(Optional.class) ? parsedValue - : parsedValue.orElse(null) + : AfterParseOptions.into$getNewFieldValue(f, parsedValue) ); } catch (IllegalArgumentException e) { if (parsedValue.isEmpty()) @@ -390,7 +398,7 @@ private static T into( throw new IncompatibleCommandTemplateType( "Field '" + f.getName() + "' of type '" + f.getType().getSimpleName() + "' is not " - + "compatible with the type (" + parsedValue.getClass().getSimpleName() + ") of the " + + "compatible with the type (" + parsedValue.get().getClass().getSimpleName() + ") of the " + "parsed argument '" + argName + "'" ); @@ -456,5 +464,47 @@ private static T into( throw new RuntimeException(e); } } + + /** + * {@link #into(Class)} helper method. Returns the new value for the given field based on the parsed value. + * If the parsed value is {@code null}, this method will return {@code null} as well. + * If both the field and the parsed value are arrays, this method will return a new array with the same type. + * @param commandAccesorField The field to get the new value for. + * @param parsedValue The parsed value to get the new value from. + * @return The new value for the given field based on the parsed value. This will be {@code null} if the parsed + * value is {@code null}. + */ + private static Object into$getNewFieldValue( + @NotNull Field commandAccesorField, + @NotNull Optional parsedValue + ) { + if (parsedValue.isEmpty()) + return null; + + final Object value = parsedValue.get(); + + if (!(commandAccesorField.getType().isArray() && value.getClass().isArray())) + return value; + + + // handle array types + final var fieldType = commandAccesorField.getType().getComponentType(); + final var originalArray = (Object[])value; // to get rid of warnings + + try { + // create a new array of the same type as the field. + var newArray = (Object[])Array.newInstance(fieldType, Array.getLength(originalArray)); + + // copy the values from the original array to the new array + System.arraycopy(originalArray, 0, newArray, 0, originalArray.length); + + return newArray; + } catch (ClassCastException e) { + throw new IncompatibleCommandTemplateType( + "Field '" + commandAccesorField.getName() + "' of type '" + commandAccesorField.getType().getSimpleName() + + "' is not compatible with the type (" + fieldType.arrayType() + ") of the parsed argument" + ); + } + } } } \ No newline at end of file diff --git a/src/main/java/lanat/ArgumentType.java b/src/main/java/lanat/ArgumentType.java index 2f05f4d6..49e9c910 100644 --- a/src/main/java/lanat/ArgumentType.java +++ b/src/main/java/lanat/ArgumentType.java @@ -9,7 +9,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.HashMap; import java.util.function.Consumer; /** @@ -78,9 +77,6 @@ public abstract class ArgumentType private @Nullable ArgumentType parentArgType; private final @NotNull ArrayList<@NotNull ArgumentType> subTypes = new ArrayList<>(); - /** Mapping of types to their corresponding argument types. Used for inferring. */ - private static final HashMap, Class>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>(); - /** * Constructs a new argument type with the specified initial value. @@ -92,7 +88,7 @@ public ArgumentType(@NotNull T initialValue) { } /** - * Constructs a new argument type with no initial value. + * Constructs a new argument type. */ public ArgumentType() { if (this.getRequiredUsageCount().start() == 0) { @@ -298,39 +294,4 @@ public void resetState() { public @Nullable ArgumentType getParent() { return this.parentArgType; } - - - /** - * Registers an argument type to be inferred for the specified type/s. - * @param type The argument type to infer. - * @param infer The types to infer the argument type for. - */ - public static void registerTypeInfer(@NotNull Class> type, @NotNull Class... infer) { - for (Class clazz : infer) { - ArgumentType.INFER_ARGUMENT_TYPES_MAP.put(clazz, type); - } - } - - /** - * Returns the argument type that should be inferred for the specified type. - * @param clazz The type to infer the argument type for. - * @return The argument type that should be inferred for the specified type. Returns {@code null} if no - * valid argument type was found. - */ - public static Class> getTypeInfer(@NotNull Class clazz) { - return ArgumentType.INFER_ARGUMENT_TYPES_MAP.get(clazz); - } - - // add some default argument types. - static { - // we need to also specify the primitives... wish there was a better way to do this. - ArgumentType.registerTypeInfer(StringArgumentType.class, String.class); - ArgumentType.registerTypeInfer(IntegerArgumentType.class, int.class, Integer.class); - ArgumentType.registerTypeInfer(BooleanArgumentType.class, boolean.class, Boolean.class); - ArgumentType.registerTypeInfer(FloatArgumentType.class, float.class, Float.class); - ArgumentType.registerTypeInfer(DoubleArgumentType.class, double.class, Double.class); - ArgumentType.registerTypeInfer(LongArgumentType.class, long.class, Long.class); - ArgumentType.registerTypeInfer(ShortArgumentType.class, short.class, Short.class); - ArgumentType.registerTypeInfer(ByteArgumentType.class, byte.class, Byte.class); - } } \ No newline at end of file diff --git a/src/main/java/lanat/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java new file mode 100644 index 00000000..cb9d88b4 --- /dev/null +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -0,0 +1,98 @@ +package lanat; + +import lanat.argumentTypes.*; +import lanat.exceptions.ArgumentTypeInferException; +import lanat.utils.Range; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.HashMap; +import java.util.function.Supplier; + +public class ArgumentTypeInfer { + /** + * Mapping of types to their corresponding argument types. Used for inferring. + * Argument types are stored as suppliers so that we have no shared references. + * */ + private static final HashMap, Supplier>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>(); + public static final Range DEFAULT_TYPE_RANGE = Range.AT_LEAST_ONE; + + /** + * Registers an argument type to be inferred for the specified type/s. + * @param type The argument type to infer. + * @param infer The types to infer the argument type for. + */ + public static void register(@NotNull Supplier> type, @NotNull Class... infer) { + if (infer.length == 0) + throw new IllegalArgumentException("Must specify at least one type to infer the argument type for."); + + for (Class clazz : infer) { + if (clazz.isArray() && clazz.getComponentType().isPrimitive()) + throw new IllegalArgumentException("Cannot infer argument type for primitive array type: " + clazz.getName()); + + if (ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.containsKey(clazz)) + throw new IllegalArgumentException("Argument type already registered for type: " + clazz.getName()); + + ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.put(clazz, type); + } + } + + /** + * Returns a new argument type instance for the specified type. + * @param clazz The type to infer the argument type for. + * @return The argument type that should be inferred for the specified type. + * @throws ArgumentTypeInferException If no argument type is found for the specified type. + */ + public static ArgumentType get(@NotNull Class clazz) { + var result = ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.get(clazz); + + if (result == null) + throw new ArgumentTypeInferException(clazz); + + return result.get(); + } + + /** + * Registers a numeric argument type with the specified tuple type as well. + * Note that for arrays, only the non-primitive types are inferred. + * @param type The type of the numeric argument type. + * @param array The default value of the numeric argument type. + * @param inferPrimitive The non-array types to infer the argument type for. + * @param The type of the numeric type. + * @param The type of the tuple argument type. + */ + private static > + void registerNumericWithTuple( + @NotNull Supplier type, + @NotNull Ti[] array, + @NotNull Class inferPrimitive, + @NotNull Class infer + ) { + assert !infer.isPrimitive() && inferPrimitive.isPrimitive() + : "Infer must be a non-primitive type and inferPrimitive must be a primitive type."; + + // register both the primitive and non-primitive types + ArgumentTypeInfer.register(type, inferPrimitive, infer); + + // register the array type (only the non-primitive type) + ArgumentTypeInfer.register(() -> new MultipleNumbersArgumentType<>(DEFAULT_TYPE_RANGE, array), infer.arrayType()); + } + + // add some default argument types. + static { + register(StringArgumentType::new, String.class); + register(() -> new MultipleStringsArgumentType(DEFAULT_TYPE_RANGE), String[].class); + + register(BooleanArgumentType::new, boolean.class, Boolean.class); + + register(() -> new FileArgumentType(false), File.class); + + // we need to specify the primitives as well... wish there was a better way to do this. + registerNumericWithTuple(IntegerArgumentType::new, new Integer[] {}, int.class, Integer.class); + registerNumericWithTuple(FloatArgumentType::new, new Float[] {}, float.class, Float.class); + registerNumericWithTuple(DoubleArgumentType::new, new Double[] {}, double.class, Double.class); + registerNumericWithTuple(LongArgumentType::new, new Long[] {}, long.class, Long.class); + registerNumericWithTuple(ShortArgumentType::new, new Short[] {}, short.class, Short.class); + registerNumericWithTuple(ByteArgumentType::new, new Byte[] {}, byte.class, Byte.class); + } +} diff --git a/src/main/java/lanat/CLInput.java b/src/main/java/lanat/CLInput.java index f5e2ce63..a72e0cce 100644 --- a/src/main/java/lanat/CLInput.java +++ b/src/main/java/lanat/CLInput.java @@ -12,7 +12,7 @@ public final class CLInput { public final @NotNull String args; private CLInput(@NotNull String args) { - this.args = args; + this.args = args.trim(); } /** diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java index da9611e4..7894d4fc 100644 --- a/src/main/java/lanat/Command.java +++ b/src/main/java/lanat/Command.java @@ -43,10 +43,10 @@ public class Command { private final @NotNull List<@NotNull String> names = new ArrayList<>(); private @Nullable String description; - final @NotNull ArrayList<@NotNull Argument> arguments = new ArrayList<>(); - final @NotNull ArrayList<@NotNull Command> subCommands = new ArrayList<>(); + private final @NotNull ArrayList<@NotNull Argument> arguments = new ArrayList<>(); + private final @NotNull ArrayList<@NotNull Command> subCommands = new ArrayList<>(); private Command parentCommand; - final @NotNull ArrayList<@NotNull ArgumentGroup> argumentGroups = new ArrayList<>(); + private final @NotNull ArrayList<@NotNull ArgumentGroup> argumentGroups = new ArrayList<>(); private final @NotNull ModifyRecord<@NotNull TupleCharacter> tupleChars = ModifyRecord.of(TupleCharacter.SQUARE_BRACKETS); private final @NotNull ModifyRecord<@NotNull Integer> errorCode = ModifyRecord.of(1); @@ -199,7 +199,7 @@ public void addNames(@NotNull String... names) { .map(UtlString::requireValidName) .peek(newName -> { if (this.hasName(newName)) - throw new IllegalArgumentException("Name " + UtlString.surround(newName) + " is already used by this command."); + throw new IllegalArgumentException("Name '" + newName + "' is already used by this command."); }) .forEach(this.names::add); @@ -268,7 +268,7 @@ 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. */ - public boolean uniqueArgumentReceivedValue() { + boolean uniqueArgumentReceivedValue() { return this.arguments.stream().anyMatch(a -> a.getUsageCount() >= 1 && a.isUniqueAllowed()) || this.subCommands.stream().anyMatch(Command::uniqueArgumentReceivedValue); } @@ -362,6 +362,12 @@ private void inheritProperties(@NotNull Command parent) { this.from$invokeAfterInitMethod(cmdTemplate); } + /** + * Invokes the {@link CommandTemplate#beforeInit(CommandTemplate.CommandBuildHelper)} method of the given command + * template class, if it exists. + * @param cmdTemplate The command template class to invoke the method from. + * @param argumentBuilders The argument builders that will be passed to the method. + */ private void from$invokeBeforeInitMethod( @NotNull Class cmdTemplate, @NotNull List> argumentBuilders @@ -382,6 +388,10 @@ private void inheritProperties(@NotNull Command parent) { }); } + /** + * Invokes the {@link CommandTemplate#afterInit(Command)} method of the given command template class, if it exists. + * @param cmdTemplate The command template class to invoke the method from. + */ private void from$invokeAfterInitMethod(@NotNull Class cmdTemplate) { Stream.of(cmdTemplate.getDeclaredMethods()) .filter(m -> UtlReflection.hasParameters(m, Command.class)) @@ -397,6 +407,10 @@ private void inheritProperties(@NotNull Command parent) { }); } + /** + * Passes certain properties to all the Sub-Commands of this command. + * @see #inheritProperties(Command) + */ void passPropertiesToChildren() { this.subCommands.forEach(c -> c.inheritProperties(this)); } @@ -412,11 +426,16 @@ void passPropertiesToChildren() { */ @Override public boolean equals(@NotNull Object obj) { + if (obj == this) return true; if (obj instanceof Command cmd) return UtlMisc.equalsByNamesAndParentCmd(this, cmd); return false; } + /** + * Checks that all the sub-commands in this container are unique. + * @throws CommandAlreadyExistsException if there are two commands with the same name + */ void checkUniqueSubCommands() { UtlMisc.requireUniqueElements(this.subCommands, c -> new CommandAlreadyExistsException(c, this)); } @@ -443,8 +462,6 @@ public void setOnOkCallback(@Nullable Consumer<@NotNull ParsedArguments> callbac @Override public void invokeCallbacks() { - this.subCommands.forEach(Command::invokeCallbacks); - if (this.shouldExecuteCorrectCallback()) { if (this.onCorrectCallback != null) this.onCorrectCallback.accept(this.getParsedArguments()); } else { @@ -456,8 +473,14 @@ public void invokeCallbacks() { .stream() .sorted((x, y) -> Argument.compareByPriority(x.getKey(), y.getKey())) // sort by priority when invoking callbacks! .forEach(e -> e.getKey().invokeCallbacks(e.getValue())); + + // invoke the callbacks of the Sub-Commands recursively + this.subCommands.forEach(Command::invokeCallbacks); } + /** + * Returns {@code true} if the {@link #onCorrectCallback} should be executed. + */ boolean shouldExecuteCorrectCallback() { return switch (this.getCallbackInvocationOption()) { case NO_ERROR_IN_COMMAND -> !this.hasExitErrorsNotIncludingSubCommands(); @@ -544,7 +567,7 @@ void tokenize(@NotNull String input) { } void parseTokens() { - // first we need to set the tokens of all tokenized subCommands + // first, we need to set the tokens of all tokenized subCommands Command cmd = this; do { cmd.parser.setTokens(cmd.tokenizer.getFinalTokens()); diff --git a/src/main/java/lanat/CommandTemplate.java b/src/main/java/lanat/CommandTemplate.java index 52f18967..bb4df964 100644 --- a/src/main/java/lanat/CommandTemplate.java +++ b/src/main/java/lanat/CommandTemplate.java @@ -51,7 +51,7 @@ *

* If no Argument Type is specified on the annotation, the Argument Type will be attempted to be inferred from the * field type if possible, which is the case for some built-in types, such as - * {@link String}, {@link Integer}, {@link Double}, etc. + * {@link String}, {@link Integer}, {@link java.io.File}, etc. *

* * Example: @@ -124,6 +124,14 @@ public abstract class CommandTemplate { * to get the argument builder corresponding to an argument with a given name. */ public record CommandBuildHelper(@NotNull Command cmd, @NotNull List> args) { + /** + * Returns the argument builder corresponding to the argument with the given name. + * This is a helper method to get the argument builder from the list of argument builders ({@link #args}). + * @param name The name of the argument. + * @return The argument builder corresponding to the argument with the given name. + * @param The type of the argument. + * @param The type of the value passed to the argument. + */ @SuppressWarnings("unchecked") public , TInner> ArgumentBuilder arg(@NotNull String name) { diff --git a/src/main/java/lanat/argumentTypes/BooleanArgumentType.java b/src/main/java/lanat/argumentTypes/BooleanArgumentType.java index ec676c09..b80800cb 100644 --- a/src/main/java/lanat/argumentTypes/BooleanArgumentType.java +++ b/src/main/java/lanat/argumentTypes/BooleanArgumentType.java @@ -8,6 +8,7 @@ /** * An argument type that is set in a true state if the argument was used. + * @see Boolean */ public class BooleanArgumentType extends ArgumentType { public BooleanArgumentType() { diff --git a/src/main/java/lanat/argumentTypes/ByteArgumentType.java b/src/main/java/lanat/argumentTypes/ByteArgumentType.java index c92c3ac4..ef93a9ef 100644 --- a/src/main/java/lanat/argumentTypes/ByteArgumentType.java +++ b/src/main/java/lanat/argumentTypes/ByteArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a byte value. + * @see Byte */ public class ByteArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/DoubleArgumentType.java b/src/main/java/lanat/argumentTypes/DoubleArgumentType.java index 89825e46..8b169465 100644 --- a/src/main/java/lanat/argumentTypes/DoubleArgumentType.java +++ b/src/main/java/lanat/argumentTypes/DoubleArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a double precision floating point number. + * @see Double */ public class DoubleArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/EnumArgumentType.java b/src/main/java/lanat/argumentTypes/EnumArgumentType.java index 31e6d793..cb90d941 100644 --- a/src/main/java/lanat/argumentTypes/EnumArgumentType.java +++ b/src/main/java/lanat/argumentTypes/EnumArgumentType.java @@ -20,6 +20,10 @@ public class EnumArgumentType> extends ArgumentType { private final @NotNull T @NotNull [] values; + /** + * Creates a new enum argument type. + * @param defaultValue The default value of the enum type. This is also used to infer the type of the enum. + */ public EnumArgumentType(@NotNull T defaultValue) { super(defaultValue); this.values = defaultValue.getDeclaringClass().getEnumConstants(); diff --git a/src/main/java/lanat/argumentTypes/FileArgumentType.java b/src/main/java/lanat/argumentTypes/FileArgumentType.java index afcab7c2..f4941037 100644 --- a/src/main/java/lanat/argumentTypes/FileArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FileArgumentType.java @@ -1,28 +1,96 @@ package lanat.argumentTypes; import lanat.ArgumentType; +import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; /** - * An argument type that takes a file path, and returns a {@link File} instance. - * If the file could not be found, an error is added. + * An argument type that takes a file path and returns a {@link File} instance representing it. + * This argument type can also check if the file exists and if it is a regular file or a directory. + * @see File */ public class FileArgumentType extends ArgumentType { + public enum FileType { + REGULAR_FILE, + DIRECTORY, + ANY; + + private @NotNull String getName(boolean shortName) { + return switch (this) { + case REGULAR_FILE -> "file"; + case DIRECTORY -> "directory"; + case ANY -> shortName ? "(file/dir)" : "file or directory"; + }; + } + } + + private final boolean mustExist; + private final @NotNull FileType fileType; + + + /** + * Creates a new file argument type. + * @param mustExist whether the file must exist or not + * @param fileType the type of the file (regular file, directory, or any) + */ + public FileArgumentType(boolean mustExist, @NotNull FileType fileType) { + this.mustExist = mustExist; + this.fileType = fileType; + } + + /** + * Creates a new file argument type which accepts any kind of file. + * @param mustExist whether the file must exist or not + */ + public FileArgumentType(boolean mustExist) { + this(mustExist, FileType.ANY); + } + + /** + * Checks if the file is valid. This method may add errors to the type. + * @param file the file to check + * @return whether the file is valid or not + */ + protected boolean checkFile(@NotNull File file) { + if (this.mustExist && !file.exists()) { + this.addError("File does not exist."); + return false; + } + + if (this.fileType == FileType.REGULAR_FILE && !file.isFile()) { + this.addError("File is not a regular file."); + return false; + } + + if (this.fileType == FileType.DIRECTORY && !file.isDirectory()) { + this.addError("File is not a directory."); + return false; + } + + return true; + } + @Override public File parseValues(@NotNull String @NotNull [] args) { - try { - return new File(args[0]); - } catch (Exception e) { - this.addError("File not found: '" + args[0] + "'."); - return null; - } + File file = new File(args[0]); + return this.checkFile(file) ? file : null; } @Override public @Nullable String getDescription() { - return "A file path."; + return "A file path of" + + (this.mustExist ? " an existing " : " a ") + + this.fileType.getName(false) + + "."; + } + + @Override + public @Nullable TextFormatter getRepresentation() { + return new TextFormatter( + "path" + File.separator + "to" + File.separator + this.fileType.getName(true) + ); } } diff --git a/src/main/java/lanat/argumentTypes/FloatArgumentType.java b/src/main/java/lanat/argumentTypes/FloatArgumentType.java index 7a240f85..fa987fbd 100644 --- a/src/main/java/lanat/argumentTypes/FloatArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FloatArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a floating point number. + * @see Float */ public class FloatArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java b/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java index 0c41a446..cd15a13a 100644 --- a/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java @@ -11,6 +11,7 @@ * method returns {@code null}, an error is added. The error message can be specified in the constructor. * @param The {@link Parseable} type. * @param The type of the value returned by the {@link Parseable#parseValues(String[])} method. + * @see Parseable */ public class FromParseableArgumentType, TInner> extends ArgumentType { private final @NotNull T parseable; diff --git a/src/main/java/lanat/argumentTypes/IntegerArgumentType.java b/src/main/java/lanat/argumentTypes/IntegerArgumentType.java index 52637a41..4736711b 100644 --- a/src/main/java/lanat/argumentTypes/IntegerArgumentType.java +++ b/src/main/java/lanat/argumentTypes/IntegerArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes an integer number. + * @see Integer */ public class IntegerArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java b/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java index bcb650ea..caf095c8 100644 --- a/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java +++ b/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java @@ -18,6 +18,7 @@ *

* @param The type of the argument type used to parse the values. * @param The type of the values. + * @see HashMap */ public class KeyValuesArgumentType, Ts> extends ArgumentType> { private final @NotNull ArgumentType valueType; diff --git a/src/main/java/lanat/argumentTypes/LongArgumentType.java b/src/main/java/lanat/argumentTypes/LongArgumentType.java index bef388e8..25e4cc23 100644 --- a/src/main/java/lanat/argumentTypes/LongArgumentType.java +++ b/src/main/java/lanat/argumentTypes/LongArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a long integer number. + * @see Long */ public class LongArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java b/src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java new file mode 100644 index 00000000..cafe72de --- /dev/null +++ b/src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java @@ -0,0 +1,28 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import lanat.ArgumentTypeInfer; +import lanat.utils.Range; +import org.jetbrains.annotations.NotNull; + +/** + * An argument type that takes multiple numbers. + * @param The type of number that this argument type is. + */ +public class MultipleNumbersArgumentType extends TupleArgumentType { + /** + * Creates a new {@link TupleArgumentType} with the specified range of values that the argument will take. + * @param range The range of values that the argument will take. + * @param defaultValue The default value of the argument. This will be used if no values are provided. + * @throws lanat.exceptions.ArgumentTypeInferException If the type of the default value is not supported. + */ + @SuppressWarnings("unchecked") + public MultipleNumbersArgumentType(@NotNull Range range, @NotNull Ti[] defaultValue) { + super( + range, + // we can infer the type of the argument type from the default value + (ArgumentType)ArgumentTypeInfer.get(defaultValue.getClass().getComponentType()), + defaultValue + ); + } +} diff --git a/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java b/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java index 291c46b4..c19af4c5 100644 --- a/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java +++ b/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java @@ -1,26 +1,23 @@ package lanat.argumentTypes; -import lanat.ArgumentType; import lanat.utils.Range; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * An argument type that takes multiple strings. */ -public class MultipleStringsArgumentType extends ArgumentType { - @Override - public @NotNull Range getRequiredArgValueCount() { - return Range.AT_LEAST_ONE; +public class MultipleStringsArgumentType extends TupleArgumentType { + /** + * Creates a new {@link TupleArgumentType} with the specified range of values that the argument will take. + * @param range The range of values that the argument will take. + */ + public MultipleStringsArgumentType(@NotNull Range range) { + super(range, new StringArgumentType(), new String[0]); } + // no need for anything fancy here, simply return the args @Override public @NotNull String[] parseValues(@NotNull String @NotNull [] args) { return args; } - - @Override - public @Nullable String getDescription() { - return "Accepts multiple strings."; - } } diff --git a/src/main/java/lanat/argumentTypes/NumberArgumentType.java b/src/main/java/lanat/argumentTypes/NumberArgumentType.java index 0445ff3a..7f887f3d 100644 --- a/src/main/java/lanat/argumentTypes/NumberArgumentType.java +++ b/src/main/java/lanat/argumentTypes/NumberArgumentType.java @@ -10,6 +10,7 @@ * of {@link #parseValues(String[])} that will parse the first argument as a number using the * function returned by {@link #getParseFunction()}. * @param The type of number that this argument type is. + * @see Number */ public abstract class NumberArgumentType extends ArgumentType { /** diff --git a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java index 78cca4ee..1c876e19 100644 --- a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java +++ b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java @@ -1,7 +1,7 @@ package lanat.argumentTypes; import lanat.ArgumentType; -import lanat.utils.UtlReflection; +import lanat.ArgumentTypeInfer; import lanat.utils.displayFormatter.Color; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; @@ -17,19 +17,19 @@ public class NumberRangeArgumentType> extends A private final ArgumentType argumentType; private final T min, max; + /** + * Creates a new number range argument type. + * @param min The minimum value. + * @param max The maximum value. + * @throws lanat.exceptions.ArgumentTypeInferException If the type of the default value is not supported. + */ @SuppressWarnings("unchecked") public NumberRangeArgumentType(@NotNull T min, @NotNull T max) { if (min.compareTo(max) > 0) { throw new IllegalArgumentException("min must be less than or equal to max"); } - final var typeInferred = ArgumentType.getTypeInfer(min.getClass()); - - if (typeInferred == null) { - throw new IllegalArgumentException("Unsupported type: " + min.getClass().getName()); - } - - this.argumentType = (ArgumentType)UtlReflection.instantiate(typeInferred); + this.argumentType = (ArgumentType)ArgumentTypeInfer.get(min.getClass()); this.registerSubType(this.argumentType); this.min = min; diff --git a/src/main/java/lanat/argumentTypes/ShortArgumentType.java b/src/main/java/lanat/argumentTypes/ShortArgumentType.java index 529d419c..4ed67dda 100644 --- a/src/main/java/lanat/argumentTypes/ShortArgumentType.java +++ b/src/main/java/lanat/argumentTypes/ShortArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a short integer number. + * @see Short */ public class ShortArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/StringArgumentType.java b/src/main/java/lanat/argumentTypes/StringArgumentType.java index 1f04a3c0..957a0b13 100644 --- a/src/main/java/lanat/argumentTypes/StringArgumentType.java +++ b/src/main/java/lanat/argumentTypes/StringArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a string of characters. + * @see String */ public class StringArgumentType extends ArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/TupleArgumentType.java b/src/main/java/lanat/argumentTypes/TupleArgumentType.java index 4b95155a..73d52b51 100644 --- a/src/main/java/lanat/argumentTypes/TupleArgumentType.java +++ b/src/main/java/lanat/argumentTypes/TupleArgumentType.java @@ -12,12 +12,32 @@ * Shows a properly formatted description and representation. * @param the type of the value that the argument will take */ -public abstract class TupleArgumentType extends ArgumentType { +public abstract class TupleArgumentType extends ArgumentType { private final @NotNull Range argCount; + private final @NotNull ArgumentType argumentType; - public TupleArgumentType(@NotNull Range range, @NotNull T initialValue) { - super(initialValue); + /** + * Creates a new {@link TupleArgumentType} with the specified range and argument type. + * @param range The range of values that the argument will take. + * @param argumentType The argument type that will be used to parse the values. + * @param defaultValue The default value of the argument. This will be used if no values are provided. + */ + public TupleArgumentType(@NotNull Range range, @NotNull ArgumentType argumentType, @NotNull T[] defaultValue) { + super(defaultValue); this.argCount = range; + this.registerSubType(this.argumentType = argumentType); + } + + @SuppressWarnings("unchecked") + @Override + public T @Nullable [] parseValues(@NotNull String... args) { + var result = new Object[args.length]; + + for (int i = 0; i < args.length; i++) { + result[i] = this.argumentType.parseValues(args[i]); + } + + return (T[])result; } @Override @@ -26,14 +46,18 @@ public TupleArgumentType(@NotNull Range range, @NotNull T initialValue) { } @Override - public @NotNull TextFormatter getRepresentation() { - return new TextFormatter(this.getValue().getClass().getSimpleName()) + public @Nullable TextFormatter getRepresentation() { + var argTypeRepr = this.argumentType.getRepresentation(); + if (argTypeRepr == null) + return null; + + return argTypeRepr .concat(new TextFormatter(this.argCount.getRegexRange()).withForegroundColor(Color.BRIGHT_YELLOW)); } @Override public @Nullable String getDescription() { return "Takes " + this.argCount.getMessage("value") - + " of type " + this.getInitialValue().getClass().getSimpleName() + "."; + + " of type " + this.argumentType.getRepresentation() + "."; } } \ No newline at end of file diff --git a/src/main/java/lanat/exceptions/ArgumentTypeInferException.java b/src/main/java/lanat/exceptions/ArgumentTypeInferException.java new file mode 100644 index 00000000..9e67177f --- /dev/null +++ b/src/main/java/lanat/exceptions/ArgumentTypeInferException.java @@ -0,0 +1,7 @@ +package lanat.exceptions; + +public class ArgumentTypeInferException extends ArgumentTypeException { + public ArgumentTypeInferException(Class clazz) { + super("No argument type found for type: " + clazz.getName()); + } +} diff --git a/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java b/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java index b115b2a8..8eda309c 100644 --- a/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java +++ b/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java @@ -2,7 +2,6 @@ import lanat.NamedWithDescription; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** @@ -17,12 +16,9 @@ public ObjectAlreadyExistsException( { super( typeName - + " " - + UtlString.surround(obj.getName()) - + " already exists in " + + " '" + obj.getName() + "' already exists in " + UtlReflection.getSimpleName(container.getClass()) - + " " - + UtlString.surround(container.getName()) + + " '" + container.getName() + "'" ); } } diff --git a/src/main/java/lanat/exceptions/ObjectNotFoundException.java b/src/main/java/lanat/exceptions/ObjectNotFoundException.java index 8b888995..b0bc84a1 100644 --- a/src/main/java/lanat/exceptions/ObjectNotFoundException.java +++ b/src/main/java/lanat/exceptions/ObjectNotFoundException.java @@ -2,7 +2,6 @@ import lanat.NamedWithDescription; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** @@ -17,12 +16,9 @@ public ObjectNotFoundException( { super( typeName - + " " - + UtlString.surround(obj.getName()) - + " was not found in " + + " '" + obj.getName() + "' was not found in " + UtlReflection.getSimpleName(container.getClass()) - + " " - + UtlString.surround(container.getName()) + + " '" + container.getName() + "'" ); } @@ -31,8 +27,6 @@ public ObjectNotFoundException(@NotNull String typeName, @NotNull NamedWithDescr } public ObjectNotFoundException(@NotNull String typeName, @NotNull String name) { - super( - typeName + " " + UtlString.surround(name) + " was not found" - ); + super(typeName + " '" + name + "' was not found"); } } diff --git a/src/main/java/lanat/helpRepresentation/ArgumentRepr.java b/src/main/java/lanat/helpRepresentation/ArgumentRepr.java index a8bd1325..f348d4af 100644 --- a/src/main/java/lanat/helpRepresentation/ArgumentRepr.java +++ b/src/main/java/lanat/helpRepresentation/ArgumentRepr.java @@ -2,6 +2,7 @@ import lanat.Argument; import lanat.helpRepresentation.descriptions.DescriptionFormatter; +import lanat.utils.UtlMisc; import lanat.utils.displayFormatter.FormatOption; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; @@ -64,12 +65,10 @@ private ArgumentRepr() {} * @return the representation and description of the argument */ public static @Nullable String getDescription(@NotNull Argument arg) { - final String desc = DescriptionFormatter.parse(arg); - - if (desc == null) - return null; - - return ArgumentRepr.getRepresentation(arg) + ":\n" + HelpFormatter.indent(desc, arg); + return UtlMisc.nullOrElse( + DescriptionFormatter.parse(arg), + desc -> ArgumentRepr.getRepresentation(arg) + ":\n" + HelpFormatter.indent(desc, arg) + ); } /** diff --git a/src/main/java/lanat/helpRepresentation/CommandRepr.java b/src/main/java/lanat/helpRepresentation/CommandRepr.java index 5d2d31b4..4cdcb7da 100644 --- a/src/main/java/lanat/helpRepresentation/CommandRepr.java +++ b/src/main/java/lanat/helpRepresentation/CommandRepr.java @@ -2,6 +2,7 @@ import lanat.Command; import lanat.helpRepresentation.descriptions.DescriptionFormatter; +import lanat.utils.UtlMisc; import lanat.utils.displayFormatter.FormatOption; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; @@ -52,11 +53,10 @@ private CommandRepr() {} * @return the parsed description of the command */ public static @Nullable String getDescription(@NotNull Command cmd) { - final String desc = DescriptionFormatter.parse(cmd); - - return desc == null - ? null - : CommandRepr.getRepresentation(cmd) + ":\n" + HelpFormatter.indent(desc, cmd); + return UtlMisc.nullOrElse( + DescriptionFormatter.parse(cmd), + desc -> CommandRepr.getRepresentation(cmd) + ":\n" + HelpFormatter.indent(desc, cmd) + ); } /** diff --git a/src/main/java/lanat/helpRepresentation/HelpFormatter.java b/src/main/java/lanat/helpRepresentation/HelpFormatter.java index 72ea7dd7..77691117 100644 --- a/src/main/java/lanat/helpRepresentation/HelpFormatter.java +++ b/src/main/java/lanat/helpRepresentation/HelpFormatter.java @@ -20,7 +20,7 @@ * returns a string. *

*

- * To generate the help message, use {@link #generate(Command)} ()}. + * To generate the help message, use {@link #generate(Command)}. *

* * @see LayoutItem @@ -32,6 +32,7 @@ public class HelpFormatter { public static boolean debugLayout = false; static { + // register the default tags before we start parsing descriptions Tag.initTags(); } diff --git a/src/main/java/lanat/helpRepresentation/LayoutGenerators.java b/src/main/java/lanat/helpRepresentation/LayoutGenerators.java index bd3678df..f1951a64 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutGenerators.java +++ b/src/main/java/lanat/helpRepresentation/LayoutGenerators.java @@ -77,7 +77,7 @@ private LayoutGenerators() {} } if (!cmd.getCommands().isEmpty()) - buffer.append(' ').append(CommandRepr.getSubCommandsRepresentation(cmd)); + buffer.append(CommandRepr.getSubCommandsRepresentation(cmd)); return buffer.toString(); } diff --git a/src/main/java/lanat/helpRepresentation/LayoutItem.java b/src/main/java/lanat/helpRepresentation/LayoutItem.java index 97bf1df8..9abef01e 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutItem.java +++ b/src/main/java/lanat/helpRepresentation/LayoutItem.java @@ -10,7 +10,7 @@ /** * Represents a layout item in the help message generated by {@link HelpFormatter}. This class is essentially just a - * builder with some helper commands for setting a {@link Function} that generates a {@link String} for a given + * builder with some helper utilities for setting a {@link Function} that generates a {@link String} for a given * {@link Command}. * * @see HelpFormatter @@ -107,9 +107,10 @@ public LayoutItem margin(int margin) { *

* It is shown as: *

-	 * <title<:
+	 * <title>:
 	 *    <content>
 	 * 
+ * If no content is generated, the title is not shown. * * @param title the title of the layout item */ diff --git a/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java b/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java index 5c784b2c..03221581 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java @@ -3,6 +3,7 @@ import lanat.CommandUser; import lanat.NamedWithDescription; import lanat.helpRepresentation.descriptions.exceptions.MalformedTagException; +import lanat.utils.UtlMisc; import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -70,11 +71,10 @@ private DescriptionFormatter() {} */ public static @Nullable String parse(@NotNull T element) { - final var desc = element.getDescription(); - if (desc == null) - return null; - - return DescriptionFormatter.parse(element, desc); + return UtlMisc.nullOrElse( + element.getDescription(), + desc -> DescriptionFormatter.parse(element, desc) + ); } /** diff --git a/src/main/java/lanat/helpRepresentation/descriptions/Tag.java b/src/main/java/lanat/helpRepresentation/descriptions/Tag.java index 212e6c03..80e94596 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/Tag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/Tag.java @@ -7,7 +7,6 @@ import lanat.helpRepresentation.descriptions.tags.FormatTag; import lanat.helpRepresentation.descriptions.tags.LinkTag; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -53,7 +52,7 @@ public static String getTagNameFromTagClass(Class tagClass) { .findFirst() .map(Map.Entry::getKey) .orElseThrow(() -> - new IllegalStateException("Tag class " + UtlString.surround(tagClass.getName()) + " is not registered") + new IllegalStateException("Tag class '" + tagClass.getName() + "' is not registered") ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java index 42adf705..f594f2db 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java @@ -3,7 +3,6 @@ import lanat.NamedWithDescription; import lanat.exceptions.LanatException; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -15,8 +14,8 @@ public InvalidRouteException(@NotNull NamedWithDescription user, @Nullable Strin public InvalidRouteException(@NotNull NamedWithDescription user, @Nullable String value, @Nullable String message) { super( - "invalid route value " + UtlString.surround(value) - + " for " + UtlReflection.getSimpleName(user.getClass()) + " " + UtlString.surround(user.getName()) + "invalid route value '" + value + "' for " + + UtlReflection.getSimpleName(user.getClass()) + " '" + user.getName() + "'" + (message == null ? "" : ": " + message) ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java index cd8064c3..bd8105e5 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java @@ -2,7 +2,6 @@ import lanat.exceptions.LanatException; import lanat.helpRepresentation.descriptions.Tag; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,9 +9,7 @@ public class MalformedTagException extends LanatException { public MalformedTagException(@NotNull Class tagClass, @Nullable String reason) { super( - "Tag " - + UtlString.surround(Tag.getTagNameFromTagClass(tagClass)) - + " is malformed" + "Tag '" + Tag.getTagNameFromTagClass(tagClass) + "' is malformed" + (reason == null ? "" : ": " + reason) ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java index afdb1d00..a0188ba2 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java @@ -3,7 +3,6 @@ import lanat.NamedWithDescription; import lanat.exceptions.LanatException; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** Thrown when a description was not defined for an object. */ @@ -11,7 +10,7 @@ public class NoDescriptionDefinedException extends LanatException { public NoDescriptionDefinedException(@NotNull NamedWithDescription user) { super( "No description defined for " - + UtlReflection.getSimpleName(user.getClass()) + " " + UtlString.surround(user.getName()) + + UtlReflection.getSimpleName(user.getClass()) + " '" + user.getName() + "'" ); } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java index ee6173fb..0773ccc4 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java @@ -1,12 +1,11 @@ package lanat.helpRepresentation.descriptions.exceptions; import lanat.exceptions.LanatException; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** Thrown when a tag with an unknown name is attempted to be used. */ public class UnknownTagException extends LanatException { public UnknownTagException(@NotNull String tagName) { - super("tag " + UtlString.surround(tagName) + " does not exist"); + super("tag '" + tagName + "' does not exist"); } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java index df96a573..5089aa62 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java @@ -55,8 +55,7 @@ public class ColorTag extends Tag { final String[] split = UtlString.split(value, ':'); if (split.length != 2) throw new MalformedTagException( - ColorTag.class, "invalid color format " + UtlString.surround(value) - + " (expected format: 'foreground:background')" + ColorTag.class, "invalid color format '" + value + "' (expected format: 'foreground:background')" ); return ColorTag.getColor(split[0]).fg() + ColorTag.getColor(split[1]).bg(); @@ -80,7 +79,7 @@ private static Color getColor(@NotNull String colorName) { case "dark magenta", "dm" -> Color.MAGENTA; case "dark cyan", "dc" -> Color.CYAN; case "dark white", "dw" -> Color.WHITE; - default -> throw new MalformedTagException(ColorTag.class, "unknown color name " + UtlString.surround(colorName)); + default -> throw new MalformedTagException(ColorTag.class, "unknown color name '" + colorName + "'"); }; } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java index 0674a667..01068569 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java @@ -62,7 +62,7 @@ private static FormatOption getFormat(@NotNull String formatName) { case "hidden", "h" -> FormatOption.HIDDEN; case "strike", "s" -> FormatOption.STRIKE_THROUGH; default -> - throw new MalformedTagException(FormatTag.class, "unknown format name " + UtlString.surround(formatName)); + throw new MalformedTagException(FormatTag.class, "unknown format name '" + formatName + "'"); }; } } diff --git a/src/main/java/lanat/parsing/Parser.java b/src/main/java/lanat/parsing/Parser.java index d7a0a7f8..ea05bc96 100644 --- a/src/main/java/lanat/parsing/Parser.java +++ b/src/main/java/lanat/parsing/Parser.java @@ -81,8 +81,12 @@ public void setTokens(@NotNull List<@NotNull Token> tokens) { this.tokens = tokens; } + /** + * Parses the tokens that have been set. Delegates parsing of argument values to the {@link ArgumentType} of the + * argument that is being parsed. + */ public void parseTokens() { - assert this.tokens != null : "Tokens have not been set yet"; + assert this.tokens != null : "Tokens have not been set yet."; assert !this.hasFinished : "This parser has already finished parsing."; // number of positional arguments that have been parsed. diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java index 0e4726e8..0397ac40 100644 --- a/src/main/java/lanat/parsing/Tokenizer.java +++ b/src/main/java/lanat/parsing/Tokenizer.java @@ -54,10 +54,16 @@ private void setInputString(@NotNull String inputString) { * {@link Tokenizer#getFinalTokens()} */ public void tokenize(@NotNull String input) { - assert !this.hasFinished : "Tokenizer has already finished tokenizing"; + assert !this.hasFinished : "Tokenizer has already finished tokenizing."; this.setInputString(input); + // nothing to tokenize. Just finish + if (input.isEmpty()) { + this.hasFinished = true; + return; + } + char currentStringChar = 0; // the character that opened the string TokenizeError.TokenizeErrorType errorType = null; diff --git a/src/main/java/lanat/utils/UtlMisc.java b/src/main/java/lanat/utils/UtlMisc.java index c7af99f7..f6775495 100644 --- a/src/main/java/lanat/utils/UtlMisc.java +++ b/src/main/java/lanat/utils/UtlMisc.java @@ -2,8 +2,8 @@ import lanat.CommandUser; import lanat.MultipleNamesAndDescription; -import lanat.exceptions.ObjectAlreadyExistsException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.function.Function; @@ -19,7 +19,7 @@ private UtlMisc() {} */ public static void requireUniqueElements( @NotNull List list, - @NotNull Function exceptionSupplier + @NotNull Function exceptionSupplier ) { for (int i = 0; i < list.size(); i++) { final var el = list.get(i); @@ -44,4 +44,27 @@ public static void requireUniqueElements( boolean equalsByNamesAndParentCmd(@NotNull T a, @NotNull T b) { return a.getParentCommand() == b.getParentCommand() && a.getNames().stream().anyMatch(b::hasName); } + + /** + * Returns {@code obj} if it is not {@code null}, otherwise returns {@code defaultObj}. + * @param obj The object to check + * @param defaultObj The object to return if {@code obj} is {@code null} + * @return {@code obj} if it is not {@code null}, otherwise returns {@code defaultObj} + * @param The type of the objects + */ + public static @NotNull T nonNullOrElse(@Nullable T obj, @NotNull T defaultObj) { + return obj == null ? defaultObj : obj; + } + + /** + * Returns {@code null} if {@code obj} is {@code null}, otherwise returns the result of the given function. + * @param obj The object to check + * @param defaultObj The function to apply to {@code obj} if it is not {@code null} + * @return {@code null} if {@code obj} is {@code null}, otherwise returns the result of the given function + * @param The type of the objects + * @param The type of the result of the function + */ + public static @Nullable R nullOrElse(@Nullable T obj, @NotNull Function<@NotNull T, @NotNull R> defaultObj) { + return obj == null ? null : defaultObj.apply(obj); + } } diff --git a/src/main/java/lanat/utils/UtlReflection.java b/src/main/java/lanat/utils/UtlReflection.java index a1e8714e..bc6800df 100644 --- a/src/main/java/lanat/utils/UtlReflection.java +++ b/src/main/java/lanat/utils/UtlReflection.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.stream.Stream; @@ -48,7 +49,24 @@ public static boolean hasParameters(Method method, Class... parameters) { public static T instantiate(Class clazz, Object... args) { try { return clazz.getDeclaredConstructor().newInstance(args); - } catch (ReflectiveOperationException e) { + } 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)""" + ); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Unable to gain access to the class '" + clazz.getName() + + "'. Please, make sure this class is visible to Lanat." + ); + } catch (InstantiationException e) { + throw new RuntimeException( + "Unable to instantiate the class '" + clazz.getName() + + "'. Please, make sure this class is not abstract." + ); + } catch (InvocationTargetException e) { throw new RuntimeException(e); } } @@ -64,5 +82,4 @@ public static Stream getMethods(Class clazz) { return UtlReflection.getMethods(clazz.getSuperclass()); return Stream.of(clazz.getDeclaredMethods()); } - } diff --git a/src/main/java/lanat/utils/UtlString.java b/src/main/java/lanat/utils/UtlString.java index ab91ece4..0965714c 100644 --- a/src/main/java/lanat/utils/UtlString.java +++ b/src/main/java/lanat/utils/UtlString.java @@ -72,7 +72,7 @@ private UtlString() {} final var indentBuff = new StringBuilder(); // buffer for the current indentation that will be added to the beginning of each line if needed int lineWidth = 0; // the current line width - boolean jumped = true; // true if a newline was added. starts off as true in case the string with indentation + boolean jumped = true; // true if a newline was added. starts off as true in case the string starts with indentation for (char chr : str.toCharArray()) { if (chr == ' ' || chr == '\t') { @@ -89,7 +89,7 @@ private UtlString() {} } endBuffer.append(wordBuff).append(chr); // make sure to not count escape sequences on the length! - lineWidth += UtlString.removeSequences(wordBuff.toString()).length() + 1; + lineWidth += UtlString.removeSequences(wordBuff.toString()).length() + 1; // +1 for the char we just added wordBuff.setLength(0); } diff --git a/src/test/java/lanat/test/TestingParser.java b/src/test/java/lanat/test/TestingParser.java index c0f70263..eb5a3cfb 100644 --- a/src/test/java/lanat/test/TestingParser.java +++ b/src/test/java/lanat/test/TestingParser.java @@ -28,7 +28,7 @@ public List parseGetErrors(String args) { } public @NotNull ParsedArgumentsRoot parseGetValues(@NotNull String args) { - var res = this.parse(CLInput.from(args)).getParsedArguments(); + var res = this.parse(CLInput.from(args)).printErrors().getParsedArguments(); assertNotNull(res, "The result of the parsing was null (Arguments have failed)"); return res; } diff --git a/src/test/java/lanat/test/UnitTests.java b/src/test/java/lanat/test/UnitTests.java index 4f4c308c..65f3c366 100644 --- a/src/test/java/lanat/test/UnitTests.java +++ b/src/test/java/lanat/test/UnitTests.java @@ -6,7 +6,6 @@ import lanat.argumentTypes.CounterArgumentType; import lanat.argumentTypes.IntegerArgumentType; import lanat.argumentTypes.StringArgumentType; -import lanat.argumentTypes.TupleArgumentType; import lanat.helpRepresentation.HelpFormatter; import lanat.utils.Range; import lanat.utils.displayFormatter.TextFormatter; @@ -17,9 +16,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -class StringJoiner extends TupleArgumentType { - public StringJoiner() { - super(Range.from(1).to(3), ""); +class StringJoiner extends ArgumentType { + @Override + public @NotNull Range getRequiredArgValueCount() { + return Range.from(1).to(3); } @Override diff --git a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java index e3114f89..c91fde58 100644 --- a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java +++ b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java @@ -4,12 +4,10 @@ import lanat.ArgumentGroup; import lanat.Command; import lanat.CommandTemplate; -import lanat.argumentTypes.CounterArgumentType; -import lanat.argumentTypes.NumberRangeArgumentType; -import lanat.argumentTypes.StdinArgumentType; -import lanat.argumentTypes.StringArgumentType; +import lanat.argumentTypes.*; import org.jetbrains.annotations.NotNull; +import java.io.File; import java.util.Optional; @Command.Define(names = "my-program", description = "This is a test program.") @@ -28,10 +26,14 @@ public CommandTemplateExample() {} @Argument.Define(names = "arg1", argType = StringArgumentType.class) public String arg1; + @Argument.Define(description = "") + public File file; @Argument.Define(names = "arg1a", argType = StringArgumentType.class) public String arg1copy; + @Argument.Define + public Byte[] bytes; @CommandAccessor public MySubCommand subCommand; @@ -40,11 +42,26 @@ public CommandTemplateExample() {} public static void beforeInit(@NotNull CommandBuildHelper helper) { helper., Double>arg("number") .withArgType(new NumberRangeArgumentType<>(5.5, 15.89)); + helper.arg("file") + .withArgType(new FileArgumentType(true, FileArgumentType.FileType.REGULAR_FILE) { + @Override + protected boolean checkFile(@NotNull File file) { + if (!super.checkFile(file)) return false; + + if (!file.canExecute()) { + this.addError("File is not executable."); + return false; + } + + return true; + } + }); } @InitDef public static void afterInit(@NotNull Command cmd) { cmd.addGroup(new ArgumentGroup("test-group") {{ + this.setExclusive(true); this.addArgument(cmd.getArgument("string")); this.addArgument(cmd.getArgument("number")); }}); @@ -52,7 +69,7 @@ public static void afterInit(@NotNull Command cmd) { @Command.Define(names = "sub-command", description = "This is a sub-command.") - public static class MySubCommand extends CommandTemplate { + public static class MySubCommand extends CommandTemplate.Default { public MySubCommand() {} @Argument.Define(argType = CounterArgumentType.class, description = "This is a counter", names = "c") diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java index bf66bdc9..54516626 100644 --- a/src/test/java/lanat/test/manualTests/ManualTests.java +++ b/src/test/java/lanat/test/manualTests/ManualTests.java @@ -2,28 +2,26 @@ import lanat.ArgumentParser; import lanat.CLInput; -import lanat.Command; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; +import java.util.Arrays; public final class ManualTests { @Test public void main() { - String input = ""; + String input = " "; // write some stuff to stdin System.setIn(new ByteArrayInputStream("hello world\ngoodbye".getBytes())); - var parsed = new ArgumentParser(CommandTemplateExample.class) {{ - this.addCommand(new Command(CommandTemplateExample.MySubCommand.class) {{ - this.addCommand(new Command(CommandTemplateExample.MySubCommand.AnotherSubCommand.class)); - }}); - }} - .parse(CLInput.from(input)) - .printErrors() - .printHelpIfNoInput() - .into(CommandTemplateExample.class); + var parsed = ArgumentParser.parseFromInto( + CommandTemplateExample.class, + CLInput.from(input), + o -> o.exitIfErrors() + .printErrors() + .printHelpIfNoInput() + ); parsed.string .ifPresentOrElse( @@ -35,5 +33,6 @@ public void main() { 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/TestArgumentTypes.java b/src/test/java/lanat/test/units/TestArgumentTypes.java index 5846d49f..c04e4373 100644 --- a/src/test/java/lanat/test/units/TestArgumentTypes.java +++ b/src/test/java/lanat/test/units/TestArgumentTypes.java @@ -4,6 +4,7 @@ import lanat.argumentTypes.*; import lanat.test.TestingParser; import lanat.test.UnitTests; +import lanat.utils.Range; import org.junit.jupiter.api.Test; import java.io.File; @@ -24,8 +25,11 @@ protected TestingParser setParser() { this.addArgument(Argument.create(new IntegerArgumentType(), "integer")); this.addArgument(Argument.create(new FloatArgumentType(), "float")); this.addArgument(Argument.create(new StringArgumentType(), "string")); - this.addArgument(Argument.create(new MultipleStringsArgumentType(), "multiple-strings")); - this.addArgument(Argument.create(new FileArgumentType(), "file")); + this.addArgument(Argument.create(new MultipleStringsArgumentType(Range.AT_LEAST_ONE), "multiple-strings")); + this.addArgument(Argument.create(new MultipleNumbersArgumentType<>( + Range.AT_LEAST_ONE, new Integer[] { 10101 }), "multiple-ints") + ); + this.addArgument(Argument.create(new FileArgumentType(true), "file")); this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum.TWO), "enum")); this.addArgument(Argument.create(new KeyValuesArgumentType<>(new IntegerArgumentType()), "key-value")); this.addArgument(Argument.create(new NumberRangeArgumentType<>(3, 10), "int-range")); @@ -75,10 +79,16 @@ public void testStrings() { assertArrayEquals(new String[] { "hello world" }, this.parseArg("multiple-strings", "'hello world'")); } + @Test + public void testNumbers() { + assertArrayEquals(new Integer[] { 4 }, this.parseArg("multiple-ints", "4")); + assertArrayEquals(new Integer[] { 4, 5, 6 }, this.parseArg("multiple-ints", "4 5 6")); + assertArrayEquals(new Integer[] { 10101 }, this.parseArg("multiple-ints", "")); + } + @Test public void testFile() { - assertEquals("hello.txt", this.parseArg("file", "hello.txt").getName()); - this.assertNotPresent("file"); + assertNull(this.parseArg("file", "hello.txt")); } @Test diff --git a/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java b/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java index 8d8a9b50..aa4d2578 100644 --- a/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java +++ b/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java @@ -82,5 +82,8 @@ public static class CmdTemplate4 extends CommandTemplate { @Argument.Define public Double number2; + + @Argument.Define + public Byte[] bytes; } } diff --git a/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java b/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java index e3dd43de..6458b9f6 100644 --- a/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java +++ b/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java @@ -3,10 +3,7 @@ import lanat.ArgumentParser; import lanat.CLInput; import lanat.CommandTemplate; -import lanat.argumentTypes.BooleanArgumentType; -import lanat.argumentTypes.DoubleArgumentType; -import lanat.argumentTypes.IntegerArgumentType; -import lanat.argumentTypes.StringArgumentType; +import lanat.argumentTypes.*; import lanat.exceptions.CommandTemplateException; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; @@ -57,7 +54,6 @@ public void testNestedCommands() { assertEquals(52, result.cmd2.cmd3.number); } - @Test @DisplayName("test type inference for fields argument types") public void testTypeInference() { @@ -67,5 +63,17 @@ public void testTypeInference() { assertTrue(result.getArgument("text").argType instanceof StringArgumentType); assertTrue(result.getArgument("flag").argType instanceof BooleanArgumentType); assertTrue(result.getArgument("number2").argType instanceof DoubleArgumentType); + assertTrue(result.getArgument("bytes").argType instanceof MultipleNumbersArgumentType); + } + + @Test + @DisplayName("test array parsed values are properly converted") + public void testArrayParsedValues() { + final var result = ArgumentParser.parseFromInto( + CmdTemplates.CmdTemplate4.class, + CLInput.from("--bytes 5 12 89") + ); + + assertArrayEquals(new Byte[] {5, 12, 89}, result.bytes); } } \ No newline at end of file