Arguments
A command takes input three ways, and they are not interchangeable.
| Required argument | Optional argument | Flag | |
|---|---|---|---|
| Looks like | /heal Notch | /heal [Notch] | /heal --silent |
| Declared with | addRequiredArgument | addOptionalArgument | addFlag |
| Position | positional, in order | positional, after required | last, after everything |
| Order between siblings | matters | matters | does not matter |
| Can be omitted | no | yes | yes |
| Read with | getArgument | getOptionalArgument | getFlagValue / hasFlag |
| Brigadier cost | one node | one branch | one node per ordered subset |
The short version: positional data is an argument, a modifier is a flag. If leaving it out should still run the command, it is optional or a flag.
Required arguments
The command only executes once every required argument is supplied. Brigadier will not match the path otherwise, so there is no "missing argument" branch to handle.
public class BanCommand extends BaseCommand<MyPlugin> {
public BanCommand(MyPlugin plugin) {
super(plugin, "ban");
this.addRequiredArgument("target", ArgumentTypes.player());
this.addRequiredArgument("reason", StringArgumentType.greedyString());
}
@Override
protected CommandResultType perform(CommandDispatch<MyPlugin> dispatch) {
// Guaranteed present: the command would not have matched otherwise.
Player target = dispatch.resolvePlayer("target").orElseThrow();
String reason = dispatch.getArgument("reason", String.class);
...
}
}
/ban alone does not run this — it is not a failure you handle, the path simply
does not exist.
Optional arguments
An optional argument makes the command executable both with and without it, so
perform runs in either case and has to cope with absence.
this.addOptionalArgument("target", ArgumentTypes.player());
Player target = dispatch.resolvePlayer("target") // the ARGUMENT
.orElse(dispatch.getSenderAsPlayer()); // fall back to the SENDER
Both /heal and /heal Notch work. This is the usual "act on me, or on someone
I name" shape.
Reading them back
| Call | Returns |
|---|---|
getArgument(name, Type.class) | The value. Use for required arguments. |
getArgument(name, Type.class, fallback) | The value, or fallback if absent. |
getOptionalArgument(name, Type.class) | Optional, empty if absent. |
hasArgument(name) | Presence only. |
For Paper's own types there are typed resolvers, which save you the
ArgumentTypes round-trip: resolvePlayer, resolvePlayers, resolveEntity,
resolveEntities, resolveBlockPosition, resolveFinePosition,
resolvePlayerProfiles.
resolvePlayer and getSenderAsPlayer both return a Player but are
unrelated. resolvePlayer("target") looks up the argument named target;
getSenderAsPlayer() returns whoever typed the command. Mixing them up is
the most common mistake with this API — see
Core concepts.
Why hasArgument exists
Brigadier throws the same IllegalArgumentException for "no such argument"
and "argument is a different type", so catching it around a typed lookup cannot
tell an absent argument from your own type mistake. hasArgument probes with
Object.class, which is assignable from anything, so it can only fail when the
argument is genuinely absent.
That is also why getOptionalArgument throws rather than returning empty when
the type is wrong: silently swallowing a type error would hide a bug in your
code.
Ordering
Declare in this order, because that is the order Brigadier parses:
/command <required…> [optional…] [--flags…]
- Required before optional. An optional argument followed by a required one cannot be expressed — there would be no way to tell which value you meant.
- Flags last, always. Flag nodes branch only into other flag nodes, never
back into arguments or sub-commands.
/cmd sub --verboseparses;/cmd --verbose subdoes not. - Sub-commands and positional arguments compete. A sub-command named
listand a string argument in the same position are ambiguous; Brigadier resolves literals first, so the sub-command wins and the argument becomes unreachable for that input.
Two traps
Reserved names
Value flags generate synthetic argument names internally, all sharing a reserved
prefix. Adding an argument whose name starts with that prefix throws
IllegalArgumentException at construction, rather than producing a tree where
your argument and a flag's value collide.
If you change the prefix with setFlagValuePrefix, do it before adding any
arguments — it throws if arguments already exist, because a late change would
invalidate names that already passed validation.
A value that looks like a flag
getOptionalArgument returns empty if the argument is absent or if its value
matches a registered flag name. That is deliberate: for /heal --silent,
silent must not be captured as the optional target.
The consequence is that a player genuinely named like one of your flags cannot
be passed positionally. dispatch.isFlagLikeValue(...) exposes the same check
if you need to explain it to the user.
Cost
A required or optional argument adds roughly one node to the command tree. A
flag adds one node per ordered subset of the whole flag set — growth faster
than n!, and the tree is sent to every client on join. See
Flags for the measured numbers.
If you are choosing between a fourth boolean flag and an optional argument, the argument is dramatically cheaper.