-
Notifications
You must be signed in to change notification settings - Fork 7
Argument Parsing
What this page covers: how a command method's parameters become typed, validated arguments. Where a value comes from is decided by its annotation, never by syntax buried in a string. This page is the companion to Command Framework.
@FinalCMD.SubCMD(subcmd = "coins")
public void coins(FCommandSender sender, @Arg(name = "<amount>") Integer amount) { ... }| Form | Provenance |
|---|---|
| no annotation | the command context - the caller (sender, label, raw args, ...). It never consumes a typed token. |
@Arg |
a token of the line, in this executable's window |
@FlagArg |
a --name value flag, anywhere after the command path - see Flags
|
@FinalCMD.Captured |
what an ancestor node captured - see Command Framework |
So an unannotated FPlayer is always whoever typed the command, and a target is always annotated. That
is what spares the framework a wrapper type whose only job would be telling the two apart.
public @interface Arg {
String name(); // display name incl. brackets, e.g. "<amount>"
String context() default ""; // parser-specific constraint (see below)
boolean fromSender() default false; // resolve from the sender when no token was typed
Class<? extends ArgParser> parser() default ArgParser.class; // override the type-based parser
FCLocale[] locales() default {}; // localized error text for this arg
String def() default ""; // declarative default for an OPTIONAL arg (see below)
}The brackets around the name decide whether the argument is required. This is parsed from the
name string itself, and there are exactly two forms:
| Form | Meaning | If the player omits it |
|---|---|---|
<name> |
Required | Parsing fails, the arg's error message is shown, the command aborts. |
[name] |
Nullable / optional | The parameter is injected as null; your method handles it. |
// Required: "/heal <target>" - target must be given.
public void heal(FCommandSender s, @Arg(name = "<target>") FPlayer target) { ... }
// Optional: "/heal [target]" - null target means "heal myself".
public void heal(FCommandSender s, @Arg(name = "[target]") FPlayer target) {
FPlayer who = target != null ? target : (FPlayer) s;
// ...
}
⚠️ Gotcha - an optional[name]argument is injected asnullwhen omitted. Always null-check optional arguments before dereferencing them.
Where a value comes from is always an annotation, never syntax buried in the displayed name.
fromSender = true lets the parser answer from the sender's own state when the position was left
empty, so one method serves both /arena enable true (the arena the admin is standing in) and
/arena enable true myarena (the named one):
public void enable(FCommandSender s,
@Arg(name = "<enabled>") Boolean enabled,
@Arg(name = "<arena>", fromSender = true) Arena arena) { ... }Two rules follow from it:
- the parser has to override
fromSender(ParseCall). One that does not is refused at registration - otherwise the argument would silently eat the next token instead of being inferred; - it fires only when that position is empty, so an inferred argument has to be the last positional (or optional). It does not hand the token back to the argument after it.
A ... suffix inside the brackets makes the argument the tail: it takes every token left. It has to
be the method's last @Arg, and there is at most one. Four parameter types are accepted, and they are
four ways of handing over the same tokens:
| Type | What you get |
|---|---|
String |
the tokens joined by a single space |
String[] |
one entry per token |
Argumento |
the joined text as the raw token type |
MultiArgumentos |
the tokens as an argument list |
@FinalCMD.SubCMD(subcmd = "broadcast")
public void broadcast(FCommandSender s, @Arg(name = "<message...>") String message) { ... }A required tail with zero tokens sends the help line; an optional [rest...] with zero tokens hands over
an empty value of its type. The tail never goes through an ArgParser - choosing among four shapes of
the same tokens is the framework's job - but it still shows on the usage line and still tab-completes
(the tail's parser keeps answering for every position it covers).
def() is only legal on an [optional] argument - declaring it on a <required> one fails at
registration, not at dispatch. When the player
omits the argument (including typing an explicit empty string), the def() text is parsed by the
exact same ArgParser the typed value would have gone through - context bounds and choices apply
to it too:
// Omitted -> parsed as if the player had typed "1" - so it still respects the [1:*] bound.
@Arg(name = "[page]", context = "[1:*]", def = "1") Integer pageA value the player actually types always wins over def(). A def() that is not parseable for the
argument's type behaves exactly like a bad optional value would: every built-in ArgParser only
raises its error message when argInfo.isRequired() is true, and def() is exclusive to optional
arguments, so an unparseable def() silently resolves to null instead of aborting - EXCEPT for a
context bound/choice check, which always rejects regardless of required/optional (so a def()
outside a declared numeric range still errors and aborts dispatch, same as a bad typed value would).
def() also supports the %placeholder% mechanism ICustomFinalCMD.customize() resolves for the
rest of ArgData (name, context, locales).
context narrows what the parser accepts, and its meaning is parser-specific. Every built-in parser
shares the same small mini-DSL:
| Type | Form | Meaning |
|---|---|---|
Integer/Float/Double
|
"min:max" (e.g. "1:10", "[1:*]") |
An inclusive numeric range. * on either side means unbounded in that direction. Wrapping the whole thing in the arg's own <...>/[...] brackets is conventional but not required - the parser strips a leading/trailing bracket pair if present, and reads the value unchanged either way. |
Integer/Float/Double
|
"a|b|c" (e.g. "1|5|10") |
A fixed set of accepted numeric values. |
String |
"a|b|c" (e.g. "add|remove|list") |
A fixed, case-insensitive option set, also used for tab-complete. Empty context falls back to the arg's own name (so a plain <mode> with no context just means "any single token"). |
Enum (any) |
"A|B|C" |
A subset of the enum's constants (matched case-insensitively by their name()). Empty context means every constant (capped at 50 - ArgParserEnum throws past that, by design: that many choices belongs in a different UI). |
Boolean |
"yes|no" (exactly 2 options) |
A custom true/false pair instead of the default true/false tokens (first option = true, second = false). A custom-context Boolean must have exactly 2 options - it fails at registration otherwise. |
// A fixed option set (String parser): only these three tokens accepted, and tab-completed.
@Arg(name = "<mode>", context = "add|remove|list") String mode
// A numeric range (Number parser): page >= 1.
@Arg(name = "[page]", context = "[1:*]") Integer pageThe same DSL applies to a @FlagArg's context(), applied to the flag's value parser - see
Flags.
These types resolve out of the box - just declare the parameter type (positional needs @Arg):
| Type | Notes |
|---|---|
String |
Free text, or a fixed option set via context. |
Integer, Float, Double
|
Numbers, with optional [min:max] range context. |
Boolean |
true/false (and locale-aware variants). |
Enum (any) |
Any enum; defaults to all constants as options. |
UUID |
Parsed UUID. |
FPlayer |
Portable online player (both platforms). |
IPlayerData |
Resolves a player's data by name. |
NumberWrapper |
EverNifeCore's arbitrary-precision number. |
FCTimeFrame |
A duration like 10m, 2h, 3d. |
Argumento |
The raw single token, unparsed. |
PageVisualization |
A page number for a FancyText PageViewer. |
On the Minecraft platform these are registered additionally (lazily, see below): Player and
World, plus OreDictEntry (on modded servers) and FCWorldGuardRegion (when WorldGuard is present).
A parameter with no @Arg is filled from the command context by an ArgParserContextual. The
framework knows these types out of the box:
| Type | Injected value |
|---|---|
FCommandSender |
The command sender (portable). |
FPlayer |
The sender as a player (command becomes player-only). |
String |
The command label the player typed. |
MultiArgumentos |
The raw arguments of THIS method's window (see below). |
HelpLine |
This sub-command's help line, already rendered for the path typed, to sendTo(sender) on bad input. |
HelpContext |
The command's full help context. |
CommandPath |
The concrete path this dispatch walked - label, literals and captured tokens. |
PlayerData |
The sender's player data. |
PDSection |
A specific PlayerData section of the sender. |
On Minecraft you can additionally inject the native CommandSender, Player, and ItemStack (the
item in hand).
⚠️ HelpLineis one render of aHelpLineTemplate- the template is the shared, path-less object the framework builds at registration, andsendToonly exists on the render. InjectHelpLine.
MultiArgumentos is the escape hatch when you want to parse the raw tokens yourself. It is the
window this executable owns: index 0 is the first token after the command path, not after the label.
For a leaf three levels down, /mycmd user Steve set a b, getStringArg(0) is a:
@FinalCMD.SubCMD(subcmd = "set", usage = "<PluginName> <LocaleName>")
public void set(FCommandSender sender, MultiArgumentos argumentos, HelpLine helpLine) {
if (argumentos.emptyArgs(0, 1)) { helpLine.sendTo(sender); return; }
String pluginName = argumentos.getStringArg(0);
// ...
}The same holds for ArgInfo.getIndex() inside a parser: the index is local to the window, so a parser
never has to know how deep its command sits.
To make a parameter of your own type resolvable, extend ArgParser<T>. It needs an (ArgInfo)
constructor and one method per job - the framework decides which one runs, and when:
| Method | Answers | Called when |
|---|---|---|
parse(ParseCall) |
the converted value, or null for "not recognized" |
there is a token |
reject(ParseCall) |
the message for a token parse did not recognize |
parse returned null AND the argument was required |
absent(ParseCall) |
the value of an optional argument nobody typed | no token, optional, no def()
|
fromSender(ParseCall) |
the value read off the sender | no token, @Arg(fromSender = true)
|
Only parse is abstract. The point of the split is that what a failure costs is the framework's
decision, and only the text of it is the parser's - a parser that had to ask argInfo.isRequired()
was rewriting the framework's decision tree in every implementation.
public class ArgParserTemplateKit extends ArgParser<TemplateKit> {
@FCLocale(lang = LocaleType.EN_US, text = "&c There is no kit named &e${name}&c.")
@FCLocale(lang = LocaleType.PT_BR, text = "&c Não existe nenhum kit chamado &e${name}&c.")
public static LocaleMessage KIT_NOT_FOUND;
public ArgParserTemplateKit(ArgInfo argInfo) {
super(argInfo);
}
@Override
public TemplateKit parse(ParseCall call) {
return TemplateKit.getByName(call.argumento().toString()); // null = not recognized
}
@Override
public ArgParseException reject(ParseCall call) {
return deny(call, KIT_NOT_FOUND.addPlaceholder("name", call.argumento()));
}
@Override
public List<String> tabComplete(TabContext tabContext) {
String lastWord = tabContext.getLastWord().toLowerCase();
return TemplateKit.getAll().stream()
.map(TemplateKit::getName)
.filter(name -> name.toLowerCase().startsWith(lastWord))
.sorted(String.CASE_INSENSITIVE_ORDER)
.collect(Collectors.toList());
}
}📌 Never check
isRequired()to decide whether to complain.parsereturnsnulland stops there; if that null was fatal, the framework asksrejectfor the message. Skippingrejectaltogether is fine - the default sends a generic "invalid argument".
There is a third case the old single method could not name: a value that converted perfectly and is
refused anyway - a domain rule, not a parse failure. That one is always fatal, and it goes out through
deny, which sends the message and hands back the exception to throw. This is ArgParserNumber
enforcing a context = "[1:*]" bound on a number that parsed just fine:
throw deny(call, FCMessageUtil.NOT_BOUNDED_LOWER
.addPlaceholder("number", value)
.addPlaceholder("min", NumberWrapper.of(boundaries.getLeft())));Both deny(call, MESSAGE) and deny(call, MESSAGE.addPlaceholder(...)) are the same call, because
addPlaceholder hands back a new message instead of mutating the shared static field - two senders
at once never read each other's text.
ParseCall is one object instead of three parameters, so giving parsers more context later is an
addition rather than a signature break in every third-party parser in existence. It carries
sender(), argumento() (EMPTY when the parser is answering without a token), argInfo(), path(),
previouslyParsed(Class) - a value an earlier @Arg of the same method produced - and
captured(nodePath, Class), which reads what an ancestor node captured, keyed by node path because
a path may capture the same type twice.
The same ArgParser class resolves both a positional @Arg and a @FlagArg's value - there is no
separate parser type for flags. ParseCall.isFlagValue() tells you which one you are in (also true
while resolving a flag's def()), if your parser needs to behave differently for the two - see
Flags.
For a contextual type, extend ArgParserContextual<T> instead: it takes an (ArgContextualInfo)
constructor, a parserArgument(ctx, sender) returning the injected value, and
requiresToBeAPlayer(). It gets no token and has no failure policy, so it kept the single method.
tabComplete(TabContext) runs once per keystroke, and the traversal that leads to it counts tokens
without resolving any. So TabContext hands over facts, not values:
| Call | What it gives |
|---|---|
getLastWord() |
the word being completed |
getIndex() |
where it sits in the raw args
|
getLocalIndex() |
where it sits once the command path is sliced off - 0 is the first token after the path, flags included |
getPath() |
the segments already consumed, as typed |
getCaptureToken(nodePath) |
what an ancestor node captured, raw: never parsed, never validated |
@Override
public List<String> tabComplete(TabContext ctx) {
String userToken = ctx.getCaptureToken("user"); // exactly as typed, maybe not a real player
return homeCache.namesOf(userToken);
}Resolving that token is your business, with your own cache. The framework does not pay, on every keystroke, for a value the traversal never needed - and when it cannot answer a position, the answer is an empty list, never a list of online player names dressed up as a real suggestion.
Three ways, from most local to most global:
// 1. Per parameter - only this argument uses it.
public void kit(FCommandSender s, @Arg(name = "<kit>", parser = ArgParserTemplateKit.class) TemplateKit kit) { }
// 2. Per plugin - every TemplateKit parameter in THIS plugin's commands resolves through it.
ArgParserManager.addPluginParser(ecPluginData, TemplateKit.class, ArgParserTemplateKit.class);
// 3. Global - every ECPlugin can resolve this type.
ArgParserManager.addGlobalParser(TemplateKit.class, ArgParserTemplateKit.class);Register the parser before the command that uses it, so the framework already knows how to resolve the type at registration time:
public static void registerCommands(ECPluginData ecPluginData) {
ArgParserManager.addPluginParser(ecPluginData, TemplateKit.class, ArgParserTemplateKit.class);
FinalCMDManager.registerCommand(ecPluginData, TemplateCommand.class);
}Lookup order is plugin parsers first, then global, matching by exact class or assignability
(isAssignableFrom). Contextual parsers use addPluginContextualParser / addGlobalContextualParser.
The platform-specific parsers (Bukkit Player/World/ItemStack, Hytale types) are registered the
first time the command framework initializes, through IPlatform.registerArgParsers(). On Bukkit that
runs MinecraftArgParsers.initialize() exactly once (it's guarded to be idempotent), which is what
adds Player, World, and the conditional OreDictEntry/FCWorldGuardRegion parsers. You don't call
this yourself; it's part of the Platform Abstraction wiring.
-
Command Framework -
@FinalCMD, sub-commands, help, permissions, registration. -
Flags -
@FlagArg, the--name valuesyntax, and the manualMultiArgumentosflag API. -
Localization - the
@FCLocaleerror messages a parser sends. -
PlayerData and PDSections - the
PlayerData/PDSectiontypes you can inject. -
Platform Abstraction -
IPlatform.registerArgParsers()and theFPlayer/FCommandSendertypes.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference