diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 00000000..9ee09a4c --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,19 @@ +name: Deploy Javadoc + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Deploy JavaDoc + uses: MathieuSoysal/Javadoc-publisher.yml@v2.4.0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + javadoc-branch: javadoc + java-version: 17 + project: gradle \ No newline at end of file diff --git a/README.md b/README.md index daaf990c..5757d432 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,3 @@ -|
* An Argument specifies a value that the user can introduce to the command. This value will be parsed by the specified * {@link ArgumentType} each time the Argument is used. Once finished parsing, the value may be retrieved by using * {@link ParsedArguments#get(String)} on the {@link ParsedArguments} object returned by - * {@link ArgumentParser#parseArgs(String[])}. + * {@link ArgumentParser#parse(CLInput)}. * *
- * An Argument can be created using the factory methods available, like {@link Argument#create(String...)}. + * An Argument can be created using the factory methods available, like {@link Argument#createOfBoolType(String...)}. *
*- * An Argument with the names "name" and "n" that will parse an integer value. There are several ways to create this - * argument. + * An Argument with the names "name" and 'n' that will parse an integer value. In order to create an Argument, you need + * to call any of the static factory methods available, like {@link Argument#createOfBoolType(String...)}. These methods + * will return an {@link ArgumentBuilder} object, which can be used to specify the Argument's properties easily. *
- ** {@code - * Argument.create(ArgumentType.INTEGER(), "name", "n"); - * Argument.create("name", ArgumentType.INTEGER()) + * Argument.create('n', "name", new IntegerArgumentType()); + * Argument.create("name", new IntegerArgumentType()) * .addNames("n"); * } ** - *
- * {@code - * new Argument<>(ArgumentType.INTEGER(), "name", "n"); - * new Argument<>("name", ArgumentType.INTEGER()) - * .addNames("n"); - * } - ** *
* The {@link PrefixChar#AUTO} prefix will be automatically set depending on the Operating System. *
+ * * @see PrefixChar#AUTO - * */ + */ public static class PrefixChar { public static final PrefixChar MINUS = new PrefixChar('-'); public static final PrefixChar PLUS = new PrefixChar('+'); @@ -133,9 +133,9 @@ public static class PrefixChar { public static final PrefixChar COLON = new PrefixChar(':'); /** - * This prefix will be automatically set depending on the Operating System. - * On Linux, it will be {@link PrefixChar#MINUS}, and on Windows, it will be {@link PrefixChar#SLASH}. - * */ + * This prefix will be automatically set depending on the Operating System. On Linux, it will be + * {@link PrefixChar#MINUS}, and on Windows, it will be {@link PrefixChar#SLASH}. + */ public static final PrefixChar AUTO = System.getProperty("os.name").toLowerCase().contains("win") ? SLASH : MINUS; @@ -150,9 +150,11 @@ private PrefixChar(char character) { * Creates a new PrefixChar with the specified non-whitespace character. *
* NOTE:
- * The constant fields of this class should be used instead of this method. Other characters
- * could break compatibility with shells using special characters as prefixes, such as the |
or ;
characters.
+ * The constant fields of this class should be used instead of this method. Other characters could break
+ * compatibility with shells using special characters as prefixes, such as the |
or ;
+ * characters.
*
* This is equivalent to calling
{@code Argument.create(charName, argType).addNames(fullName)}* * @param charName the single character name of the argument. * @param fullName the full name of the argument. * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this - * */ + */ public static
true
if this argument is the help argument of its parent command.
- * This just checks if the argument's name is "help" and if it is marked with {@link #allowUnique()}.
- * @return true
if this argument is the help argument of its parent command.
+ * Returns {@code true} if this argument is the help argument of its parent command. This just checks if the
+ * argument's name is "help" and if it is marked with {@link #setAllowUnique(boolean)}.
+ *
+ * @return {@code true} if this argument is the help argument of its parent command.
*/
public boolean isHelpArgument() {
return this.getName().equals("help") && this.isUniqueAllowed();
}
- /**
- * Specify a function that will be called with the value introduced by the user.
- * - * By default this callback is called only if all commands succeed, but you can change this behavior with - * {@link Command#invokeCallbacksWhen(CallbacksInvocationOption)} - *
- * @param callback the function that will be called with the value introduced by the user. - */ - public Argument- * Note that this callback is only called if the error was dispatched by this argument's type. That - * is, - * if the argument, for example, is obligatory, and the user does not specify a value, an error will be thrown, but - * this callback will not be called, as the error was not dispatched by this argument's type. - *
- * @param callback the function that will be called if an error occurs when parsing this argument. - */ - public Argumenttrue
if the argument was used the correct amount of times.
+ *
+ * @return {@code true} if the argument was used the correct amount of times.
*/
- private boolean finishParsingCheckUsageCount() {
+ private boolean finishParsing$checkUsageCount() {
if (this.getUsageCount() == 0) {
if (this.obligatory && !this.parentCommand.uniqueArgumentReceivedValue()) {
this.parentCommand.getParser().addError(
@@ -517,7 +504,7 @@ private boolean finishParsingCheckUsageCount() {
);
return false;
}
- // make sure that the argument was used the minimum amount of times specified
+ // make sure that the argument was used the minimum amount of times specified
} else if (this.argType.usageCount < this.argType.getRequiredUsageCount().min()) {
this.parentCommand.getParser()
.addError(ParseError.ParseErrorType.ARG_INCORRECT_USAGES_COUNT, this, 0);
@@ -529,9 +516,10 @@ private boolean finishParsingCheckUsageCount() {
/**
* Checks if the argument is part of an exclusive group, and if so, checks if there is any violation of exclusivity
* in the group hierarchy.
- * @return true
if there is no violation of exclusivity in the group hierarchy.
+ *
+ * @return {@code true} if there is no violation of exclusivity in the group hierarchy.
*/
- private boolean finishParsingCheckExclusivity() {
+ private boolean finishParsing$checkExclusivity() {
// check if the parent group of this argument is exclusive, and if so, check if any other argument in it has been used
if (this.parentGroup == null || this.getUsageCount() == 0) return true;
@@ -542,7 +530,7 @@ private boolean finishParsingCheckExclusivity() {
new ParseError(
ParseError.ParseErrorType.MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED,
this.argType.getLastTokenIndex(),
- this, this.argType.getLastReceivedValueCount()
+ this, this.argType.getLastReceivedValuesNum()
)
{{
this.setArgumentGroup(exclusivityResult);
@@ -554,11 +542,12 @@ private boolean finishParsingCheckExclusivity() {
/**
* Checks if this argument matches the given name, including the prefix.
*
- * For example, if the prefix is '-'
and the argument has the name "help"
, this method will
- * return true
if the name is "--help"
.
+ * For example, if the prefix is '-'
and the argument has the name "help"
, this method
+ * will return {@code true} if the name is "--help"
.
*
true
if the name matches, false
otherwise.
+ * @return {@code true} if the name matches, {@code false} otherwise.
*/
public boolean checkMatch(@NotNull String name) {
final char prefixChar = this.getPrefix().character;
@@ -568,9 +557,10 @@ public boolean checkMatch(@NotNull String name) {
/**
* Checks if this argument matches the given single character name.
- * @see #checkMatch(String)
+ *
* @param name the name to check
- * @return true
if the name matches, false
otherwise.
+ * @return {@code true} if the name matches, {@code false} otherwise.
+ * @see #checkMatch(String)
*/
public boolean checkMatch(char name) {
return this.hasName(Character.toString(name));
@@ -597,22 +587,26 @@ void invokeCallbacks(@Nullable Object okValue) {
}
/**
- * Returns true
if the argument specified by the given name is equal to this argument.
+ * Returns {@code true} if the argument specified by the given name is equal to this argument.
* * Equality is determined by the argument's name and the command it belongs to. *
+ * * @param obj the argument to compare to - * @returntrue
if the argument specified by the given name is equal to this argument
+ * @return {@code true} if the argument specified by the given name is equal to this argument
*/
- public boolean equals(@NotNull Argument, ?> obj) {
- return Command.equalsByNamesAndParentCmd(this, obj);
+ @Override
+ public boolean equals(@NotNull Object obj) {
+ if (obj instanceof Argument, ?> arg)
+ return UtlMisc.equalsByNamesAndParentCmd(this, arg);
+ return false;
}
/**
* Compares two arguments by the synopsis view priority order.
* * Order: - * Positional > Obligatory > Optional. + * Allows Unique > Positional > Obligatory > Optional. *
* * @param first the first argument to compare @@ -621,7 +615,8 @@ public boolean equals(@NotNull Argument, ?> obj) { * before the first. */ public static int compareByPriority(@NotNull Argument, ?> first, @NotNull Argument, ?> second) { - return new Comparator+ * Note that this callback is only called if the error was dispatched by this argument's type. + * That + * is, if the argument, for example, is obligatory, and the user does not specify a value, an error will be + * thrown, but this callback will not be called, as the error was not dispatched by this argument's type. + *
+ * + * @param callback the function that will be called if an error occurs when parsing this argument. + */ @Override - public void setOnErrorCallback(@NotNull Consumer<@NotNull Argument+ * By default this callback is called only if all commands succeed, but you can change this behavior with + * {@link Command#setCallbackInvocationOption(CallbacksInvocationOption)} + *
+ * + * @param callback the function that will be called with the value introduced by the user. + */ @Override - public void setOnCorrectCallback(@NotNull Consumer<@NotNull TInner> callback) { + public void setOnOkCallback(@Nullable Consumer<@NotNull TInner> callback) { this.onCorrectCallback = callback; } diff --git a/src/main/java/lanat/ArgumentAdder.java b/src/main/java/lanat/ArgumentAdder.java index 830ba7c6..dcfc52bd 100644 --- a/src/main/java/lanat/ArgumentAdder.java +++ b/src/main/java/lanat/ArgumentAdder.java @@ -1,16 +1,77 @@ package lanat; +import lanat.exceptions.ArgumentAlreadyExistsException; +import lanat.exceptions.ArgumentNotFoundException; +import lanat.utils.UtlMisc; import org.jetbrains.annotations.NotNull; import java.util.List; -public interface ArgumentAdder { +/** + * An interface for objects that can add {@link Argument}s to themselves. + */ +public interface ArgumentAdder extends NamedWithDescription { /** - * Inserts an argument for this command to be parsed. + * Inserts an argument into this container. * * @param argument the argument to be inserted + * @param+ * Represents a group of arguments. This is used to group arguments together, and to set exclusivity between them. + * When a group is exclusive, it means that only one argument in it can be used at a time. + *
+ * Groups can also be used to simply indicate arguments that are related to each other, and to set a description + * to this relation. This is useful for the help message representation. + *
+ * Groups can be nested, meaning that a group can contain other groups. This is useful for setting exclusivity between + * arguments that are in different groups. For example, given the following group tree: + *
+ * +-----------------------+ + * | Group 1 (exclusive) | + * |-----------------------| + * | Argument 1 | + * +-----------------------+ + * | + * +---------------------------+ + * | | + * +---------------+ +-------------------------+ + * | Group 2 | | Group 3 (exclusive) | + * |---------------| |-------------------------| + * | Argument 2.1 | | Argument 3.1 | + * | Argument 2.2 | | Argument 3.2 | + * +---------------+ +-------------------------+ + *+ *
true
, indicates that one argument in this group has been used. This is used when later checking for
- * exclusivity in the groups tree at {@link ArgumentGroup#checkExclusivity(ArgumentGroup)}
+ * When set to {@code true}, indicates that one argument in this group has been used. This is used when later
+ * checking for exclusivity in the groups tree at {@link ArgumentGroup#checkExclusivity(ArgumentGroup)}
*/
private boolean argumentUsed = false;
+ /**
+ * Creates a new Argument Group with the given name and description.
+ * The name and descriptions are basically only used for the help message.
+ * @param name The name of the group. Must be a unique name among all groups in the same command.
+ * @param description The description of the group.
+ */
public ArgumentGroup(@NotNull String name, @Nullable String description) {
- this.name = UtlString.sanitizeName(name);
+ this.name = UtlString.requireValidName(name);
this.description = description;
}
+ /**
+ * Creates a new Argument Group with the given name and no description.
+ * @param name The name of the group. Must be a unique name among all groups in the same command.
+ */
public ArgumentGroup(@NotNull String name) {
this(name, null);
}
@@ -60,8 +117,9 @@ public ArgumentGroup(@NotNull String name) {
@Override
public null
if this is the
- * first call to this method.
- * @return The group that caused the violation, or null
if there is no violation.
+ * method, because it is the one that is being checked for exclusivity. This can be {@code null} if this is
+ * the first call to this method.
+ * @return The group that caused the violation, or {@code null} if there is no violation.
*/
@Nullable ArgumentGroup checkExclusivity(@Nullable ArgumentGroup childCallee) {
if (
@@ -143,15 +224,24 @@ void registerGroup(@NotNull Command parentCommand) {
return null;
}
+ /**
+ * Returns {@code true} if this group has no arguments and no subgroups.
+ * @return {@code true} if this group has no arguments and no subgroups.
+ */
public boolean isEmpty() {
return this.arguments.isEmpty() && this.subGroups.isEmpty();
}
+ /**
+ * Marks that an argument in this group has been used. This is used to later check for exclusivity.
+ * This also marks the parent group as used, and so on until reaching the root of the groups tree, thus marking the
+ * path of the used argument.
+ */
void setArgUsed() {
this.argumentUsed = true;
- // set argUsed to true
on all parents until reaching the groups root
+ // set argUsed to {@code true} on all parents until reaching the groups root
if (this.parentGroup != null)
this.parentGroup.setArgUsed();
}
@@ -171,7 +261,7 @@ public void resetState() {
public void setDescription(@NotNull String description) {
this.description = description;
}
-
+
@Override
public @Nullable String getDescription() {
return this.description;
@@ -181,6 +271,13 @@ public void setDescription(@NotNull String description) {
public ArgumentGroup getParent() {
return this.parentGroup;
}
+
+ @Override
+ public boolean equals(@NotNull Object obj) {
+ 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 8e813186..f9cda9d8 100644
--- a/src/main/java/lanat/ArgumentGroupAdder.java
+++ b/src/main/java/lanat/ArgumentGroupAdder.java
@@ -1,14 +1,62 @@
package lanat;
+import lanat.exceptions.ArgumentGroupAlreadyExistsException;
+import lanat.exceptions.ArgumentGroupNotFoundException;
+import lanat.utils.UtlMisc;
import org.jetbrains.annotations.NotNull;
import java.util.List;
-public interface ArgumentGroupAdder {
+/**
+ * An interface for objects that can add {@link ArgumentGroup}s to themselves.
+ */
+public interface ArgumentGroupAdder extends NamedWithDescription {
/**
- * Adds an argument group to this element.
+ * Adds an argument group to this container.
+ * @param group the argument group to be added
*/
void addGroup(@NotNull ArgumentGroup group);
- @NotNull List<@NotNull ArgumentGroup> getSubGroups();
+ /**
+ * Returns a list of the argument groups in this container.
+ * @return an immutable list of the argument groups in this container
+ */
+ @NotNull List<@NotNull ArgumentGroup> getGroups();
+
+ /**
+ * Checks that all the argument groups in this container have unique names.
+ * @throws ArgumentGroupAlreadyExistsException if there are two argument groups with the same name
+ */
+ default void checkUniqueGroups() {
+ UtlMisc.requireUniqueElements(this.getGroups(), g -> new ArgumentGroupAlreadyExistsException(g, this));
+ }
+
+ /**
+ * Checks if this container has an argument group with the given name.
+ * @param name the name of the argument group
+ * @return {@code true} if this container has an argument group with the given name, {@code false} otherwise
+ */
+ default boolean hasGroup(@NotNull String name) {
+ for (final var group : this.getGroups()) {
+ if (group.getName().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the argument group with the given name.
+ * @param name the name of the argument group
+ * @return the argument group with the given name
+ * @throws ArgumentGroupNotFoundException if there is no argument group with the given name
+ */
+ default @NotNull ArgumentGroup getGroup(@NotNull String name) {
+ for (final var group : this.getGroups()) {
+ if (group.getName().equals(name)) {
+ return group;
+ }
+ }
+ throw new ArgumentGroupNotFoundException(name);
+ }
}
diff --git a/src/main/java/lanat/ArgumentGroupUser.java b/src/main/java/lanat/ArgumentGroupUser.java
new file mode 100644
index 00000000..d80474de
--- /dev/null
+++ b/src/main/java/lanat/ArgumentGroupUser.java
@@ -0,0 +1,21 @@
+package lanat;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This interface is used for objects that belong to an {@link ArgumentGroup}.
+ */
+public interface ArgumentGroupUser {
+ /**
+ * Gets the {@link ArgumentGroup} object that this object belongs to.
+ *
+ * @return The parent group of this object.
+ */
+ ArgumentGroup getParentGroup();
+
+ /**
+ * Sets the parent group of this object.
+ * @param parentGroup the parent group to set
+ */
+ void registerToGroup(@NotNull ArgumentGroup parentGroup);
+}
diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java
index 583e76e1..94083be7 100644
--- a/src/main/java/lanat/ArgumentParser.java
+++ b/src/main/java/lanat/ArgumentParser.java
@@ -1,70 +1,172 @@
package lanat;
-
+import lanat.exceptions.CommandTemplateException;
+import lanat.exceptions.IncompatibleCommandTemplateType;
import lanat.parsing.TokenType;
import lanat.parsing.errors.ErrorHandler;
-import lanat.utils.Pair;
+import lanat.utils.UtlReflection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+/**
+ * + * Provides the ability to parse a command line input and later gather the values of the parsed arguments. + */ public class ArgumentParser extends Command { private boolean isParsed = false; private @Nullable String license; private @Nullable String version; + /** + * Creates a new command with the given name and description. + * @param programName The name of the command. This is the name the user will use to indicate that they want to use this + * command. + * @param description The description of the command. + * @see #setDescription(String) + */ public ArgumentParser(@NotNull String programName, @Nullable String description) { super(programName, description); } + /** + * Creates a new command with the given name and no description. This is the name the user will use to + * indicate that they want to use this command. + * @param programName The name of the command. + */ public ArgumentParser(@NotNull String programName) { this(programName, null); } + /** + * Creates a new command based on the given {@link CommandTemplate}. This does not take Sub-Commands into account. + * If you want to add Sub-Commands, use {@link #from(Class)} instead. + * @param templateClass The class of the template to use. + */ + public ArgumentParser(@NotNull Class extends CommandTemplate> templateClass) { + super(templateClass); + } /** - * {@link ArgumentParser#parseArgs(String)} + * Constructs a new {@link ArgumentParser} based on the given {@link CommandTemplate}, taking Sub-Commands into + * account. + * @param templateClass The class of the {@link CommandTemplate} to use. + * @return A new {@link ArgumentParser} based on the given {@link CommandTemplate}. + * @see CommandTemplate */ - public @NotNull ParsedArgumentsRoot parseArgs(@NotNull String @NotNull [] args) { - // if we receive the classic args array, just join it back - return this.parseArgs(String.join(" ", args)); + public static ArgumentParser from(@NotNull Class extends CommandTemplate> templateClass) { + final var argParser = new ArgumentParser(templateClass); + + // add all commands recursively + ArgumentParser.from$setCommands(templateClass, argParser); + + return argParser; } /** - * Parses the given command line arguments and returns a {@link ParsedArguments} object. + * Constructs a new {@link ArgumentParser} based on the given {@link CommandTemplate}, parses the given input, and + * populates the template with the parsed values. + *
+ * This is basically a shortcut for the following code: + *
{@code + * new ArgumentParser(clazz).parse(input).into(clazz); + * }+ *
{@code + * MyTemplate parsed = new ArgumentParser(MyTemplate.class) {{ + * addCommand(new Command(MyTemplate.SubTemplate.class)); + * }} + * .parse(input) + * .printErrors() + * .exitIfErrors() + * .into(MyTemplate.class); + * }+ *
+ * Is equivalent to this code: + *
{@code + * MyTemplate parsed = ArgumentParser.parseFromInto(MyTemplate.class, input); + * } + ** - * @param args The command line arguments to parse. + * @param templateClass The class to use as a template. + * @param input The input to parse. + * @param options A consumer that can be used for configuring the parsing process. + * @param
sun.java.command
system property.
+ * Adds all commands defined with {@link Command.Define} in the given class to the given parent command. This method
+ * is recursive and will add all sub-commands of the given class.
+ *
+ * @param templateClass The class to search for commands in.
+ * @param parentCommand The command to add the found commands to.
+ * @param