Skip to content
Petrus Pradella edited this page Jul 31, 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 @FlagArg pipeline a command author uses to bind it to a method parameter, and the lower-level manual API (MultiArgumentos.getFlags()/getFlag(...)) for commands that parse their own raw tokens. 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".

  • A multi-word value needs quotes, single or double. Without quotes, only the immediately following token is taken and the rest stays positional. 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 flag with nothing after it (or followed by another flag/--) is a presence flag: its value is the literal string "true". A Boolean-typed @FlagArg never looks past this - see below.

  • -- 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.

  • 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").

  • A flag token only counts after the command path is complete. 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. But 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 the flag before the path ends breaks the circle and keeps the traversal a single pass. 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 (no separating space, 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'. Always put a space before an opening quote.

An unrecognized flag on a command that declares @FlagArg parameters aborts the command with a message listing the flags that are available (filtered to the ones the sender has permission for):

Unknown flag [--froce]! Available flags: --force


@FlagArg - the declarative pipeline

Put @FlagArg 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.

public @interface FlagArg {
    String name();                          // 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)
    boolean showOnUsage() default true;      // false hides it from usage+hover; tab/dispatch unaffected
}

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(name = "<CooldownID>") String cooldownId,
                 @Arg(name = "<duration>") String duration,
                 @FlagArg(name = "--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. An unparseable def() errors at registration/first-use instead of silently becoming null - unlike @Arg.def(), whose internal ArgInfo is intentionally OPTIONAL (see Argument Parsing), a flag's internal ArgInfo is REQUIRED so a typo in def() is never swallowed.
  • 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.

Registration guards (fail fast, not at dispatch)

Five things fail the command's registration outright (ArgMountException), so a bad @FlagArg never ships silently:

  1. A primitive parameter type.
  2. name not in the long form --name (no spaces, non-empty after the dashes).
  3. 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.
  4. 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.
  5. Both @Arg and @FlagArg on the same parameter - a parameter is one or the other, never both.

A flag declared on a node

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

@FinalCMD.Node(subcmd = "duel")
public static class DuelNode {
    @FinalCMD.Capture
    public DuelArena capture(@Arg(name = "<arena>") DuelArena arena,
                             @FlagArg(name = "--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 4 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 the only place a player can discover a short alias at all:

[--network | -n]        <- usage line: name plus every alias, pipe-separated, when present
✯ [--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 three situations:

  • The word being typed looks like a flag (--, --ne, ...): suggests the declared long names, filtered by permission, prefix, and flags already used earlier on the line.
  • 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).
  • 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 token after a literal -- is never treated as a flag by tab-complete either, matching dispatch.


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

Some commands parse their own raw tokens instead of declaring @Arg/@FlagArg - the escape hatch is MultiArgumentos, injected as a contextual parameter. It understands the same --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.

@FinalCMD.SubCMD(subcmd = "set")
public void set(FCommandSender sender, MultiArgumentos argumentos, HelpLine helpLine) {
    if (argumentos.emptyArgs(0)) { helpLine.sendTo(sender); return; }

    FlagedArgumento force = argumentos.getFlag("force"); // same lookup as "-force"/"--force"
    if (force.isSet()) {
        // ...
    }
}

getFlags()/getFlag(name) trigger flagification lazily (and idempotently) the first time either is called - a plain positional-only command that never touches them 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 @FlagArg 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?

A token starting with - is only ever treated as a flag when:

  1. The method declares at least one @FlagArg parameter (the declarative pipeline), or
  2. The command calls MultiArgumentos.getFlags()/getFlag(...) itself (the manual path).

A method with @Arg parameters and no @FlagArg never enters flag mode at all - a token like --anything stays a plain positional argument, parsed like any other token would be. This is deliberate: adding a flag to one sub-command can never change how an unrelated sub-command interprets a dash-prefixed positional value.


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