Skip to content
Petrus Pradella edited this page Aug 3, 2026 · 9 revisions

Flags

What this page covers: the --name value flag syntax on FinalCMD commands - the syntax a player or admin types, the declarative @Arg.Flag pipeline a command author uses to bind it to a method parameter, and the lower-level manual API (MultiArgumentos.getFlags()/getFlag(...)) for tokens no declaration covers. Companion pages: Command Framework (the command itself) and Argument Parsing (@Arg, the context mini-DSL, custom ArgParsers - flags reuse all of it).


The syntax, for whoever types the command

A flag is a token starting with one or more -, followed by a name: --force, --page, -n. There is no other supported form - the old -name:value syntax is gone.

  • A value flag consumes exactly the next token: --page 2 sets page to "2".

  • --name=value is the same thing, spelled with the value glued on: --page=2 and --page 2 are the identical flag with the identical value. The split happens at the FIRST =, so --title=a=b is the flag title with value a=b.

  • A multi-word value needs quotes, single or double. Without quotes, only the immediately following token is taken and the rest stays positional. Quoting works the same on both spellings, so --title='Title Message' is one value. These two cases come straight from the tests that pin the tokenizer (MultiArgumentosSystemTest):

    Case A (quoted):    /dbroad Teste My Friend --title 'Title Message'
                         -> flag title = "Title Message"
    
    Case B (unquoted):  /dbroad Teste My Friend --title Title Message
                         -> flag title = "Title"   (only the next token)
                         -> "Message" remains a positional argument
    
  • A declared value flag never degrades to presence. Left with nothing to take - end of the line, another flag marker, the bare --, or an = with nothing after it - it aborts the command naming itself, instead of handing its parser the invented token "true":

    The flag [--page] needs a value: --page <value>

    Conversely, a declared presence flag (a Boolean parameter) takes no value at all, so --force=false is refused rather than read as "force is false":

    The flag [--force] takes no value - write just --force

    The lower-level sniffed mode (MultiArgumentos.getFlags(), below) still degrades to the literal string "true" in these cases: there is no declaration there to hold the spelling to.

  • -- alone ends flag scanning. It is removed from the input, and every token after it stays positional literally, even one that looks like a flag: set -- --force treats --force as a plain positional argument, not a flag. This holds on every command, including one that declares no flag at all - the escape is not something a declaration switches on. Inside a variadic tail it is not needed and not consumed: the scan already stopped there, so a bare -- is text like everything else.

  • A negative number is never a flag. -5 and --5 are left as positional arguments (the check is "dash(es) followed by a non-digit").

  • There is no short-flag stacking. -fv is one flag named fv, not -f plus -v. Grouping would collide with the single-dash multi-character aliases that already exist (-fo), and in a chat box the saving is a keystroke - so this is settled, not pending.

  • A flag token only counts once something that reads it has been reached. Anywhere in the executable's own window is fine - before, between or after the positional arguments, and they are stripped out before the positional parser ever sees the line. A flag written while the path is still being walked is an error that says where the flag belongs:

    Valid:  /home Steve info myhouse --details
    Error:  /home --details Steve info myhouse
    

    The reason is mechanical, not stylistic: finding the leaf would need to know the arity of each flag, and knowing the arity needs the leaf. Refusing a flag nothing has claimed yet breaks the circle and keeps the traversal a single pass.

    The exception is the node standing right there: if the node the traversal has reached already declares the flag - through its @FinalCMD.Execute, or through an ancestor's capture - the path is over and the token belongs to that node's window. That is what makes a flag on a node with children typeable at all: /lp --verbose reaches the root's own method, /lp admin --details reaches admin's @Execute. Declaring is a different matter - a node may declare flags, and the whole subtree below it recognizes them (see below).

  • Lookup is dash-count- and case-insensitive: force, -force, --FORCE and --Force all resolve the same flag.

  • A quote glued directly onto the name with no = (e.g. --title'X') is part of the flag's name, not the start of a quoted value - it is a flag literally called title'X'. Put a space or an = before an opening quote.

An unrecognized flag aborts the command. The message lists the flags that are available - all of the mistyped markers at once, so one round of corrections is enough - filtered to the ones the sender could have discovered anywhere else (permission granted, showOnUsage not turned off):

Unknown flag [--froce, --paeg]! Available flags: --force, --page

The marker syntax belongs to the line, not to the declaration. The scan runs on every command, including one that declares no @Arg.Flag at all - so -- is always consumed there too, and a marker nothing reads is answered instead of quietly becoming a positional argument:

The flag [--qualquer] means nothing here - write -- before it to keep it as plain text

That is what the escape is for: /note -- --qualquer hands --qualquer to the positional argument verbatim, whether or not the command has flags.


@Arg.Flag - the declarative pipeline

Put @Arg.Flag on a method parameter exactly like @Arg, except a flag parameter is never positional - it can appear anywhere on the command line, and its presence is always optional. It nests under @Arg because every source of a parameter's value does; a flag really does eat a word off the line, and its value goes through the very same ArgParser a positional would.

public @interface Flag {                     // nested: br...annotations.Arg.Flag
    String value();                          // canonical spelling, always long form: "--force"
    String[] aliases() default {};           // extra spellings, short or long: {"-f"}
    String context() default "";             // same mini-DSL as @Arg, applied to the VALUE parser
    Class<? extends ArgParser> parser() default ArgParser.class;
    FCLocale[] locales() default {};         // hover description, same mechanism as @Arg
    String def() default "";                 // parsed by the value parser when the flag is absent
    String permission() default "";          // required to USE the flag (not to see the command)
    String usageName() default "";           // which declared spelling shows on the usage line
    boolean showOnUsage() default true;      // false hides it from usage+hover; tab/dispatch unaffected
}

value() names the flag, so @Arg.Flag("--force") is the short spelling. Java allows it only while value is the only element - add aliases, def or anything else and it becomes @Arg.Flag(value = "--force", ...).

usageName() picks which of the declared spellings renders on the usage line; empty means the long value(). The hover always lists every spelling, so the long form stays discoverable even when the line shows a short one.

The real example: CMDECCooldown

This is the shipped code (common/src/main/java/.../commands/misc/CMDECCooldown.java) - the fire-test that collapsed the old 4-command matrix (set/setnetwork/setplayer/setplayernetwork) down to two:

@FinalCMD.SubCMD(subcmd = "set")
public void set(FCommandSender sender,
                 @Arg("<CooldownID>") String cooldownId,
                 @Arg("<duration>") String duration,
                 @Arg.Flag(value = "--network", aliases = "-n", def = "false", locales = {
                         @FCLocale(lang = LocaleType.EN_US, text = "Apply network-wide (follows the account across servers)."),
                         @FCLocale(lang = LocaleType.PT_BR, text = "Aplica na rede inteira (segue a conta entre servidores).")
                 })
                 Boolean network) {

    Cooldown cooldown = network ? Cooldown.network(cooldownId) : Cooldown.of(cooldownId);
    // ...
}

/eccooldown set myid 5m sets a local cooldown; /eccooldown set myid 5m --network (or -n) sets a network one. setPlayer carries the same --network/-n flag alongside its own <player> argument.

Typing rules

  • A flag parameter is never a primitive. boolean force fails registration; use Boolean so an absent flag can be null. This is checked fail-fast when the command registers, not at dispatch time.

  • Boolean is the presence type: aridade 0, it never consumes the next token. Present -> TRUE. Absent -> null, unless def() says otherwise (def = "false" makes absence FALSE instead of null - see CMDECCooldown above).

  • Every other type consumes exactly one token (or one quoted group): the flag's value is parsed by the same ArgParser a positional @Arg of that type would use - built-in or your own parser = MyParser.class override, including the context mini-DSL (bounds, choices - see Argument Parsing).

  • A flag is never required. There is no required attribute - a mandatory flag is a positional argument in disguise. Handle absence yourself, typically by checking for null (or relying on def()).

  • def() is parsed like a typed value, not stored raw, and it is parsed as a default: the text belongs to the command author, so a def() the value parser cannot read is reported as the command's own bug (logged, with the generic "not valid here" message naming the flag) instead of telling the sender that a word they never typed is invalid. This is the same failedOnItsOwnDefault protection @Arg.def() gets - see Argument Parsing.

  • A value the player actually typed always aborts the command on a parse failure, exactly like a bad value on a required positional - the command never runs with a bogus flag value. If the parser answers that the value is missing (an explicitly empty --title '', say), the answer is the command's own usage line, exactly as for a positional - never a silent abort.

  • A flag written twice is refused, not resolved. A flag holds one value, so --page 1 --page 2 is two answers to one question - and an alias counts as the same flag, so --page 1 -p 2 is too. Neither value is taken:

    The flag [--page] was written twice - it holds one value, so keep the one you meant

  • The scan stops where a variadic tail begins. Everything from the tail's first token on is text - a flag marker inside it is the sender's own word, and so is the bare --:

    /report Steve --force he said --force and left
                   ^^^^^^^ the flag    ^^^^^^^ part of the reason
    

    So a flag written for a command that ends in <reason...> goes before the reason, exactly as in every CLI with trailing text. The upside is that the tail is handed to the method exactly as typed: nothing is ever removed from it, so nothing in it ever needs escaping.

Registration guards (fail fast, not at dispatch)

These fail the command's registration outright (ArgMountException), so a bad @Arg.Flag never ships silently:

  1. A primitive parameter type.
  2. value not in the long form --name (no spaces, non-empty after the dashes).
  3. An alias that is not a token the tokenizer would ever recognize as a flag marker: it needs one or more leading -, a name after them that does not start with a digit, and no whitespace. "-" (dashes alone), "-5" (a negative number) and "-a b" all used to register a binding nothing could ever match.
  4. A usageName that is neither the flag's own name nor one of its aliases - the usage line can only show a spelling the sender may actually type.
  5. The same normalized spelling (name or alias, dashes stripped, case-insensitive) claimed twice on the same method - by two different flags, or a flag whose alias collides with another flag's name.
  6. The same normalized spelling claimed twice along one path - a leaf and one of its ancestor nodes. The whole path shares one namespace and one extraction pass, so the deeper declaration would quietly win; the error names both declarations instead.
  7. Two of the four @Arg... families on the same parameter - @Arg, @Arg.Flag, @Arg.Contextual and @Arg.NodeCaptured each name a different place the value comes from, and a parameter has one source. Every pair is refused, not just @Arg + @Arg.Flag.
  8. The declared name claimed twice on one method, across the three naming families (@Arg, @Arg.Flag, @Arg.Contextual). A name is how a value is addressed once resolved, so two parameters cannot answer to one. Names are compared exactly as written, which is why --amount and <amount> never collide - nobody would type one meaning the other.

A flag declared on a node

A @FinalCMD.Capture may declare @Arg.Flag parameters, and every leaf below that node recognizes them:

@FinalCMD.Node(subcmd = "duel")
public static class DuelNode {
    @FinalCMD.Capture
    public DuelArena capture(@Arg("<arena>") DuelArena arena,
                             @Arg.Flag(value = "--dry", aliases = "-d", def = "false") Boolean dry) { ... }
}
// /arena duel Colosseum spawn set 2 --dry   -> the capture receives dry = true

One extraction pass over the tail feeds the whole path, root first, so a flag declared four levels up costs the leaf nothing and shows up on its usage line and tab-complete like its own. That single namespace is exactly why guard 6 above exists.

Permission per flag

permission() gates using the flag, not seeing the command: absent, it is never checked; present without the permission, the sender gets the standard permission message and the command aborts (the value, if any, is never parsed). Tab-complete additionally filters out flags the sender cannot use.

isFlagValue() on the parse call

ParseCall.isFlagValue() tells a custom ArgParser whether it is currently resolving a flag's value (true, including its def()) or a positional value (false) - the same parser class serves both call sites, so this is the hook to branch on if a parser needs to behave differently for the two. It is fixed for the whole call and never changes mid-parse.


Help, hover and tab-complete

A visible flag (showOnUsage() == true, the default) renders as a compact [--name] token on the usage line, in declaration order, after the positional arguments - the value's shape (required-vs-not, bounds) never appears there, only in the hover.

Unlike a plain @Arg, a flag with no locales() still gets its own hover block - the title alone (with its aliases) is where a player reads every spelling at once:

[--network]             <- usage line: ONE spelling - usageName(), or the long name
✯ [--network | -n]      <- hover block title (always present for a visible flag)
● Apply network-wide (follows the account across servers).   <- only when locales() is set

showOnUsage = false removes the flag from both the usage line and the hover block entirely, but it keeps working normally: it still tab-completes and still dispatches. Use permission(), not showOnUsage, to hide a flag from players who should not even know it exists (a showOnUsage=false flag with no permission() is still suggested to everyone on tab).

Tab-complete understands flags in four situations:

  • The word being typed looks like a flag (--, --ne, ...): suggests the declared names, filtered by permission, prefix, and flags already used earlier on the line. Each flag answers with ONE spelling: its long name when that matches what was typed, otherwise the first alias that does - so -n<TAB> completes to -n, which is the only moment a short alias can be discovered by typing.
  • The word being typed already carries an = (--page=<TAB>): the flag's own value parser answers, exactly as it would for the spaced form, and every suggestion comes back with the marker glued in front (--page=1). A completion replaces the whole word, so a bare value would have replaced the flag with it.
  • The previous token is a declared value-flag: delegates to that flag's own value parser (so --page <TAB> suggests that argument's own choices, exactly like a positional would). A marker that already took its value inline (--page=3) is not that: what follows it is a positional.
  • Otherwise: the raw token index is corrected for whatever flags (and their consumed values) sit earlier on the line, then falls through to the normal positional tab-complete - so a flag typed before a positional argument never throws off which positional is being completed. A marker the extraction could not take (a name nobody declared, a value flag with nothing to consume) stays on the line but is never counted as a positional either.

A token after a literal -- is never treated as a flag by tab-complete either, matching dispatch - and so is every word from a variadic tail's first token on: once the tail has opened, flag names stop being offered, because the dispatch is about to hand them over as plain text.


The manual path: MultiArgumentos.getFlags() / getFlag(...)

MultiArgumentos carries a flag API of its own, for tokens no declaration covers. It understands the same --name value and --name=value syntax, but in sniffed mode: there is no declared arity, so a value token that itself looks like a flag (or is --) makes the marker degrade to a presence flag instead of consuming it, and nothing is ever reported back as an error - there is no declaration to check the spelling against.

The window injected into a @FinalCMD method is not in that mode. The dispatch runs the declared extraction over it on every command, so it is already flagified when the method receives it and getFlags()/getFlag(...) answer exactly what the path declared - nothing more, because a marker nobody declared never got that far: it was refused, with the -- escape offered. Sniffed mode is what an instance nothing has scanned does - one you build yourself, the one sliceFrom(...) hands back, and the one a variadic <rest...> delivers, since the scan stops where the tail begins and those tokens arrive exactly as typed:

@FinalCMD.SubCMD(subcmd = "broadcast")
public void broadcast(FCommandSender sender, @Arg("<rest...>") MultiArgumentos rest) {
    FlagedArgumento force = rest.getFlag("force"); // same lookup as "-force"/"--force"
    if (force.isSet()) {
        // ...
    }
}

getFlags()/getFlag(name) trigger flagification lazily (and idempotently) the first time either is called - an instance nobody ever asks pays nothing for it. A missing flag returns FlagedArgumento.EMPTY_ARG (isSet() == false), never null.

A single MultiArgumentos instance is flagified in exactly one mode: calling the declarative extractDeclaredFlags(...) (what @Arg.Flag uses internally) on an instance and then calling getFlags()/getFlag(...) on the SAME instance sees exactly what was already extracted - the two modes are mutually exclusive per instance, not something you mix by hand.


Anti-false-positive: when does --x become a flag at all?

The shape of the token decides it, and nothing else: one or more leading -, then a name whose first character is not a digit. So -5 and --5 are numbers, and a token of dashes alone names nothing at all (the bare -- is the escape, not a flag). No declaration switches that recognition on or off.

What the marker then means is the path's business: a spelling something between the root and the target declared becomes that flag, and any other marker is refused. So an @Arg.Flag added to one sub-command still cannot change how a sibling reads a dash-prefixed word - only a flag declared on a node reaches further, and it reaches exactly its own subtree.


See also

  • Command Framework - @FinalCMD, sub-commands, the command registry, help and permissions.
  • Argument Parsing - @Arg, the context mini-DSL, def(), writing a custom ArgParser.

Clone this wiki locally