A Kotlin-first DSL and structured execution framework built on top of Mojang's Brigadier command library.
brigadier-kt does not replace Brigadier — it enhances it by preserving Brigadier's dispatcher, parsing engine, node system, and execution model while transforming the development experience into a structured, middleware-enabled, Kotlin-native command framework.
- Introduction
- Getting Started
- Type-Safe Argument Handling
- Guards: The Middleware System
- Redirect, Fork and Forward
- Advanced Features
- Architecture
- Summary
Brigadier is a powerful, performant, tree-based command parsing library. However, its API is low-level and verbose when building complex command trees. brigadier-kt addresses this by providing a clean, idiomatic Kotlin interface while maintaining 100% compatibility with Brigadier's core functionality.
You still use Brigadier's dispatcher and nodes — brigadier-kt simply makes defining them clean, structured, and maintainable.
Problems with Pure Brigadier:
Working directly with Brigadier often leads to:
- Deep
.then()nesting that obscures command structure - Repetitive generic type declarations
- Manual
ctx.getArgument(...)calls scattered throughout code - Duplicated validation logic across command handlers
- No middleware pipeline for shared validation
- No structured approach to argument transformation
Example in Pure Brigadier:
dispatcher.register(
literal("group")
.then(argument("name", StringArgumentType.word())
.then(literal("info")
.executes(ctx -> {
String name = ctx.getArgument("name", String.class);
// resolve group...
return 1;
}))));This becomes increasingly difficult to read and maintain as command trees grow.
brigadier-kt provides:
- Clean Kotlin DSL — Visually structured command definitions
- Middleware Guards — Centralized validation and transformation pipeline
- Type-Safe Arguments — Reified generics remove boilerplate
- Argument References — Strongly typed, reusable argument bindings
- Transformation Pipeline — Convert parsed strings to domain objects
- Mutable Context — Safe argument injection and override system
- Redirect / Fork / Forward — Full support for Brigadier's node routing system
- Suggestions DSL — Simplified suggestion handling
- Full Brigadier Compatibility — Drop-in enhancement, not a replacement
Argument references (argRef() and createArgRef<T>()) rely on Kotlin's context parameters feature. You must enable
the following compiler option in your build configuration:
-Xcontext-parameters
Without this flag, any code using argRef() or createArgRef<T>() will fail to compile.
val node = command<CommandSource>("hello") {
execute {
println("Hello world!")
SINGLE_SUCCESS
}
}
dispatcher.register(node)This registers the /hello command.
The DSL mirrors your command structure visually, making the hierarchy immediately clear:
command<CommandSource>("group") {
literal("info") {
execute {
println("Showing group info")
SINGLE_SUCCESS
}
}
literal("list") {
execute {
println("Listing groups")
SINGLE_SUCCESS
}
}
literal("create") {
execute {
println("Creating group")
SINGLE_SUCCESS
}
}
}Benefits:
- Indentation reflects tree hierarchy
- No
.then()chains - Clear parent-child relationships
- Easy to navigate and modify
command<CommandSource>("user") {
argument("name", StringArgumentType.word()) {
execute {
val name = arg<String>("name")
println("User: $name")
SINGLE_SUCCESS
}
}
}What This Does:
- Registers
/user <name>command - Parses
nameargument - Provides type-safe access in execution block
Instead of Brigadier's verbose approach:
ctx.getArgument("name", String.class)Use brigadier-kt's type-safe methods:
arg<String>("name") // Required argument
optionalArg<String>("name") // Optional argumentBenefits:
- No explicit
.classparameter - Leverages Kotlin's reified generics
- Cleaner, more maintainable code
- Compile-time type safety
String-based argument access becomes fragile and error-prone in large command trees:
// Repeated throughout multiple handlers
arg<String>("group")
arg<String>("group")
arg<String>("group")Issues:
- Typos cause runtime errors
- No IDE refactoring support
- Hard to track usage
- Difficult to maintain
Bind once, reuse everywhere with strong typing. brigadier-kt provides two ways to create argument references:
argRef() creates a reference bound to the argument at the current DSL scope. The name and type are inferred
automatically from context, so no parameters are required.
argument("group", StringArgumentType.word()) {
val groupRawRef = argRef() // Bound to "group", type String
literal("info") {
execute {
val name = groupRawRef.get() // Returns String
println("Group: $name")
SINGLE_SUCCESS
}
}
}Use argRef() when you want a reference to the raw parsed value of the current argument node.
createArgRef<T>(name) creates a reference to any argument by name, with an explicitly specified type. This is
particularly useful when a Guard will transform the argument into a domain object — the ref can be typed to the
target type rather than the raw parsed type.
argument("group", StringArgumentType.word()) {
val groupRef = createArgRef<Group>("group") // Typed to Group, not String
guard {
val group = groupService.find(arg<String>("group"))
groupRef.set(group) // Inject the domain object
continueCommand()
}
literal("info") {
execute {
val group = groupRef.get() // Returns Group directly
println("Group: ${group.name}")
SINGLE_SUCCESS
}
}
}Use createArgRef<T>(name) when:
- You need a reference to an argument that will be transformed by a Guard
- The reference type differs from the originally parsed type
- You want strongly-typed access to a named argument from any scope
A reference created with createArgRef is mutable — you can write to it using ref.set(value), which is
equivalent to calling setArgument(name, value) directly on the context. This pairs naturally with Guards to inject
transformed domain objects.
guard {
val group = groupService.find(arg<String>("group"))
groupRef.set(group) // via reference
// equivalent to:
setArgument("group", group) // via context directly
continueCommand()
}Both approaches produce the same result. Prefer ref.set(...) when you have already declared a createArgRef for
the argument, as it avoids repeating the string key and keeps the type contract explicit.
| Feature | argRef() |
createArgRef<T>(name) |
|---|---|---|
| Bound to current argument node | ✓ | ✗ (explicit name) |
| Type inferred from context | ✓ (raw parsed type) | ✗ (you specify type) |
| Supports transformed types | ✗ | ✓ |
Mutable (ref.set(...)) |
✗ | ✓ |
| Use case | Raw parsed value access | Domain object after transform |
argument("group", StringArgumentType.word()) {
val groupRawRef = argRef() // String reference (raw parsed)
val groupRef = createArgRef<Group>("group") // Group reference (post-transform)
guard {
val name = groupRawRef.get()
val group = groupService.find(name)
if (group == null) {
println("Group not found: $name")
return@guard abort(NO_SUCCESS)
}
groupRef.set(group) // Inject transformed domain object
continueCommand()
}
literal("info") {
execute {
val group = groupRef.get() // Receives Group object
println("Group info: ${group.name} (${group.members.size} members)")
SINGLE_SUCCESS
}
}
literal("delete") {
execute {
val group = groupRef.get()
groupService.delete(group)
println("Deleted group: ${group.name}")
SINGLE_SUCCESS
}
}
literal("members") {
literal("list") {
execute {
val group = groupRef.get()
println("Members: ${group.members.joinToString()}")
SINGLE_SUCCESS
}
}
}
}Why This Matters:
- Strong Typing — Compile-time safety for both raw and transformed types
- No String Keys — Eliminate magic strings in execute blocks
- Refactoring Support — IDE can track all usages of a reference
- Guard Integration —
createArgRefandref.setwork naturally in the middleware pipeline
Guards are one of the core architectural features of brigadier-kt. They introduce a structured middleware pipeline between parsing and execution, enabling centralized validation and transformation.
In vanilla Brigadier:
- Validation logic must be duplicated in every execute block
- Argument transformation must be repeated across handlers
- No mechanism exists for shared subtree validation
- Business logic and validation become tightly coupled
- Domain object resolution happens repeatedly
Guards create a middleware layer that:
- Executes After Parsing — Work with validated command structure
- Executes Before Handlers — Intercept before business logic runs
- Execute Hierarchically — Run from root → leaf in tree order
- Can Abort Execution — Prevent handler execution with custom result codes
- Can Transform Arguments — Convert parsed strings to domain objects
- Can Inject Arguments — Add new values to context
- Can Override Values — Replace parsed arguments with transformed ones
command<CommandSource>("admin") {
guard {
if (!source.isAdmin()) {
println("Access denied")
abort() // Returns NO_SUCCESS by default
} else {
continueCommand()
}
}
literal("reload") {
execute {
println("Reloaded")
SINGLE_SUCCESS
}
}
literal("shutdown") {
execute {
println("Shutting down")
SINGLE_SUCCESS
}
}
}Flow:
- Command is parsed
- Guard checks if user is admin
- If not admin →
abort()stops execution - If admin →
continueCommand()allows execution to proceed - Execute block runs
Note: For simple permission checks, a requires predicate is often more appropriate as it affects node visibility.
Use guards when you need more complex logic or custom error handling.
fun <S> KtCommandBuilder<S, *>.guard(
runOnSameNode: Boolean = true,
guard: KtCommandGuard<S>
)By default, a Guard runs for every execute block in its subtree — including any execute block defined directly
on the same node where the Guard is declared.
command<Player>("test") {
guard { // runOnSameNode = true (default)
println("[Guard] Running")
continueCommand()
}
literal("hello") {
execute { /* Guard runs here */ SINGLE_SUCCESS }
}
execute { /* Guard also runs here */ SINGLE_SUCCESS }
}Running /test triggers the Guard before the execute block, just like running /test hello does.
When runOnSameNode = false, the Guard is skipped when the execute block on the same node is invoked.
It still runs normally for all child nodes.
command<Player>("test") {
guard(false) { // will NOT run when /test itself is executed
println("[Global Guard] Executing command as ${source.name}")
GuardResult.Continue
}
literal("hello") {
execute { /* Guard runs here */ SINGLE_SUCCESS }
}
argument("level", IntegerArgumentType.integer(0, 100)) {
execute { /* Guard runs here */ SINGLE_SUCCESS }
}
execute {
// Guard does NOT run here
SINGLE_SUCCESS
}
}Execution behaviour:
| Command | Guard runs? | Reason |
|---|---|---|
/test |
No | execute is on the same node; runOnSameNode = false |
/test hello |
Yes | Child node — runOnSameNode does not apply |
/test 42 |
Yes | Child node — runOnSameNode does not apply |
This option is useful when a Guard represents logic that is only meaningful for subcommands — for example, logging the specific subcommand used, resolving a resource that only subcommands need, or enforcing a requirement that does not apply to the base command itself.
command<Player>("stats") {
// Log which subcommand the player is using,
// but don't interfere when /stats is called with no arguments.
guard(false) {
println("Player ${source.name} used a stats subcommand")
continueCommand()
}
literal("kills") {
execute { /* Guard runs */ SINGLE_SUCCESS }
}
literal("deaths") {
execute { /* Guard runs */ SINGLE_SUCCESS }
}
execute {
// Guard does NOT run - shows summary without logging
println("Stats overview for ${source.name}")
SINGLE_SUCCESS
}
}This is where Guards become particularly powerful.
Command Structure:
/group <group> info
/group <group> delete
/group <group> members add <user>
/group <group> members remove <user>
Requirements:
- Validate that
<group>exists - Convert string → Group domain object
- Inject the resolved Group into execution context
- Avoid duplicate resolution in every handler
- Handle "not found" cases consistently
Every execute block must duplicate the resolution logic:
argument("group", StringArgumentType.word()) {
literal("info") {
execute {
val name = arg<String>("group")
val group = groupService.find(name)
if (group == null) {
println("Group not found: $name")
return@execute NO_SUCCESS
}
println("Group info: $group")
SINGLE_SUCCESS
}
}
literal("delete") {
execute {
val name = arg<String>("group")
val group = groupService.find(name)
if (group == null) {
println("Group not found: $name")
return@execute NO_SUCCESS
}
println("Deleting group: $group")
SINGLE_SUCCESS
}
}
// and so on...
}Problems:
- Resolution logic duplicated in every handler
- Inconsistent error messages
- Hard to modify validation rules
- Performance overhead from repeated lookups
- Business logic cluttered with validation
argument("group", StringArgumentType.word()) {
val groupRawRef = argRef()
val groupRef = createArgRef<Group>("group")
guard {
val name = groupRawRef.get()
val group = groupService.find(name)
if (group == null) {
println("Group not found: $name")
return@guard abort(NO_SUCCESS)
}
// Transform string to domain object — both approaches are equivalent
setArgument("group", group)
// or: groupRef.set(group)
continueCommand()
}
literal("info") {
execute {
val group = groupRef.get() // Receives Group object
println("Group info: ${group.name} (${group.members.size} members)")
SINGLE_SUCCESS
}
}
literal("delete") {
execute {
val group = groupRef.get()
groupService.delete(group)
println("Deleted group: ${group.name}")
SINGLE_SUCCESS
}
}
literal("members") {
// Nested commands also receive the transformed Group
literal("list") {
execute {
val group = groupRef.get()
println("Members: ${group.members.joinToString()}")
SINGLE_SUCCESS
}
}
}
}Benefits:
- ✅ Resolution happens exactly once
- ✅ Validation is centralized and consistent
- ✅ Execution logic works with domain objects
- ✅ Business logic is clean and focused
- ✅ Error handling is unified
- ✅ Performance improved (single lookup)
- ✅ Easy to modify validation rules
Now consider extending the existing structure with a new subcommand:
/group <group> create
This command should create a new group using the provided name.
Because the <group> argument already has a Guard attached (which:
- resolves the
Stringto aGroup - aborts if the group does not exist
- injects the resolved
Groupobject
), that Guard will also execute for create.
If we natively add:
literal("create") {
execute {
val name = arg<String>("group")
groupService.create(name)
println("Created group: $name")
SINGLE_SUCCESS
}
}the Guard will run first.
Since the group does not exist yet, the Guard will:
println("Group not found: $name")
abort(NO_SUCCESS)Execution never reaches the create block.
brigadier-kt allows you to explicitly disable the Guard pipeline for a specific execution block:
literal("create") {
execute(false) {
val name = arg<String>("group")
groupService.create(name)
println("Created group: $name")
SINGLE_SUCCESS
}
}Passing false to execute(...) means:
- Parent Guards will not run
- No validation is performed
- No argument transformation occurs
- Execution starts immediately after parsing
This isolates the create command from the existing middleware logic.
Because the Guard is skipped:
- The
String → Grouptransformation does not happen - No injected
Groupobject is available - Only the originally parsed value exists
Therefore you must access the raw argument:
val name = arg<String>("group")
// or: val name = groupRawRef.get()You cannot do:
groupRef.get() // throws — no Group was injected
arg<Group>("group") // throws — same reasonBecause no Group was injected into the context.
Disabling Guards is appropriate when:
- A command intentionally contradicts parent validation
- A creation/bootstrap command must bypass existence checks
- You need raw parsed values
- You want full manual control over execution
Guards operate on KtCommandContext, which extends Brigadier's CommandContext with safe mutation capabilities.
// Override or inject an argument (by key)
setArgument(name: String, value: Any)
// Override or inject an argument via a typed reference
ref.set(value)
// Remove an argument completely (further accessing will throw an exception)
removeArgument(name: String)
// Reset argument to original parsed value
resetArgument(name: String)Both setArgument(name, value) and ref.set(value) write to the same underlying mutable context and produce
identical results. The difference is stylistic:
| Approach | When to use |
|---|---|
setArgument(name, value) |
Quick one-off injection, no prior createArgRef declared |
ref.set(value) |
You already have a createArgRef — keeps the key out of strings |
Prefer ref.set(...) in transformation-heavy Guards where a createArgRef is already in scope, as it ties the write
directly to the typed reference and avoids repeating magic strings.
Overridden arguments always take precedence over parsed ones:
guard {
val raw = arg<String>("count") // "5"
val parsed = raw.toInt() // 5
setArgument("count", parsed) // Override with Integer
continueCommand()
}
execute {
val count = arg<Int>("count") // Receives Integer, not String
}This enables safe, predictable transformation pipelines.
Of course, you should use IntegerArgumentType instead of StringArgumentType for numeric arguments.
Brigadier's node system supports more than simple linear command trees. Using redirects and forks, a node can point execution to an entirely different part of the tree — enabling patterns like looping chains, context modifiers, and conditional branching. brigadier-kt exposes all three of Brigadier's routing mechanisms as clean DSL functions.
In a standard command tree, parsing always moves forward: each node consumes a token and descends into a child. A redirect breaks this rule — after a node is parsed, instead of looking for children on that node, Brigadier jumps to the children of a completely different target node and continues parsing from there.
This is how Minecraft's /execute as <target> run <command> works: after resolving <target>, execution doesn't
stop — it loops back to the execute node's children so more subcommands can follow.
There are three variants:
redirect— Routes to a target node, optionally transforming the source (1 source → 1 source)fork— Routes to a target node, producing multiple sources (1 source → n sources)forward— Low-level combined routing with explicit fork control
A redirect needs a reference to a CommandNode as its target. This is straightforward when redirecting to an
unrelated node. However, a common and important pattern requires a node to redirect back to itself — for example,
so that after processing one modifier, the full modifier chain can be used again.
The challenge is that a node does not exist yet while its DSL block is being defined. brigadier-kt solves this using Brigadier's own node merging behavior.
When dispatcher.register(node) is called, the following happens internally:
// CommandDispatcher.register:
public LiteralCommandNode<S> register(final LiteralArgumentBuilder<S> command) {
final LiteralCommandNode<S> build = command.build();
root.addChild(build);
return build;
}
// CommandNode.addChild — merges if a node with the same name already exists:
final CommandNode<S> child = children.get(node.getName());
if (child != null) {
// Merge: adopt all children from the new node onto the existing one
for (final CommandNode<S> grandchild : node.getChildren()) {
child.addChild(grandchild);
}
} else {
children.put(node.getName(), node);
}
// some code lines where removedThis means you can register the same command name twice. The first registration returns the node reference immediately; the second registration merges its children onto the already-existing node. The reference obtained from the first registration stays valid and can safely be used as a redirect target inside the second registration.
Pattern:
// Step 1: Register an empty shell to obtain the node reference
val executeNode = dispatcher.register(
command<CommandSourceStack>("execute") {
requires { hasPermission(LEVEL_GAMEMASTERS) }
// if you want to use requires,
// you MUST put it here in the first registration.
// If you put it in the second registration,
// it will not be merged and so no permission check will be performed.
}
)
// Step 2: Register the full tree — children are merged onto the existing node.
// executeNode is now a valid redirect target.
dispatcher.register(
command<CommandSourceStack>("execute") {
requires { hasPermission(LEVEL_GAMEMASTERS) }
literal("run") {
redirect(dispatcher.root) // redirect to the dispatcher root
}
literal("as") {
argument("targets", EntityArgument.entities()) {
fork(executeNode) { // redirect back to execute's children
val targets = ...
targets.map { context.source.withEntity(it) }
}
}
}
}
)This is the same technique Minecraft itself uses — the first dispatcher.register(Commands.literal("execute")...)
call in ExecuteCommand.register exists solely to capture the node reference before the full tree is built.
redirect routes execution to a target node after the current node is parsed. It accepts an optional
SingleRedirectModifier — a lambda that receives the current context and returns a single transformed source.
If no modifier is given, the source is forwarded unchanged.
fun <S> KtCommandBuilder<S, *>.redirect(target: CommandNode<S>, modifier: SingleRedirectModifier<S>? = null)The most basic use: after this node is parsed, continue with the children of target using the same source.
literal("run") {
redirect(dispatcher.root) // /execute run <any command>
}The modifier receives the current CommandContext and returns a new source. Parsing then continues at target
using that new source. This is how context modifiers like positioned, rotated, or in work.
literal("positioned") {
argument("pos", Vec3Argument.vec3()) {
redirect(executeNode) { context ->
context.source
.withPosition(/*new pos*/)
}
}
}
literal("in") {
argument("dimension", DimensionArgument.dimension()) {
redirect(executeNode) { context ->
context.source.withLevel(/*new level*/)
}
}
}After the argument is parsed, the source is replaced with a new source pointing to the new position or dimension —
then parsing loops back to executeNode, allowing further modifiers or a final run to follow.
fork routes execution to a target node, but unlike redirect it can produce multiple sources from a single
incoming source. Brigadier runs the remaining command tree once for each source in the returned collection.
fun <S> KtCommandBuilder<S, *>.fork(target: CommandNode<S>, modifier: RedirectModifier<S>)This is how /execute as <targets> works: one command execution fans out into one execution per matched entity.
literal("as") {
argument("targets", EntityArgument.entities()) {
fork(executeNode) { context ->
val targets = ...
targets.map { context.source.withEntity(it) }
}
}
}
literal("at") {
argument("targets", EntityArgument.entities()) {
fork(executeNode) { context ->
val targets = ...
targets.map { context.source.withPosition(it.position()) }
}
}
}If the modifier returns an empty list, execution is silently canceled — no children of the target node are
visited. This is how fork-based conditionals work: returning the source continues, returning empty aborts.
Returning the source or an empty list makes fork act as a conditional gate:
literal("if") {
literal("entity") {
argument("entities", EntityArgument.entities()) {
fork(executeNode) { context ->
val target = ...
val condition = ...
if (condition.matches(target)) {
listOf(context.source.withEntity(target))
} else {
emptyList()
}
}
}
}
}forward is the low-level primitive that both redirect and fork build on. It accepts a RedirectModifier and an
explicit fork boolean that controls how Brigadier treats the result.
fun <S> KtCommandBuilder<S, *>.forward(target: CommandNode<S>, fork: Boolean, modifier: RedirectModifier<S>)fork value |
Behaviour |
|---|---|
false |
Expects exactly one source — behaves like redirect with a full modifier |
true |
Expects zero or more sources — behaves like fork |
In most cases redirect and fork are the right choice. Use forward only when you need precise control over the
fork flag that the higher-level functions don't expose — for example when wrapping a custom RedirectModifier
implementation.
argument("targets", EntityArgument.entities()) {
forward(
target = executeNode,
fork = true
) { context ->
listOf(...)
}
}Guards and redirects operate at different phases of Brigadier's execution model, and their interaction has important consequences.
When a node redirects to a target, Guards attached to that redirect node are not executed. Execution arrives at the redirect node during parsing and immediately jumps to the target — there is no execution phase at the redirect node itself where Guards could run.
Guards only run during the execution phase, which begins at the node where the final command handler lives.
val executeNode = dispatcher.register(
command<CommandSourceStack>("execute") {
requires { hasPermission(LEVEL_GAMEMASTERS) }
}
)
dispatcher.register(
command<CommandSourceStack>("execute") {
requires { hasPermission(LEVEL_GAMEMASTERS) }
literal("as") {
argument("targets", EntityArgument.entities()) {
// A guard placed here will NEVER run —
// this node immediately redirects and has no execute handler.
guard {
continueCommand()
}
fork(executeNode) { context ->
...
}
}
}
literal("run") {
redirect(dispatcher.root)
}
// Guards placed here DO run — this is the execution entry point
// when the chain eventually reaches a concrete command after "run".
}
)When execution reaches the target of a redirect through normal command dispatch (i.e. when a command is typed that resolves through that node), Guards on the target node and its ancestors run as usual.
In the pattern above, after /execute as @a run say hello is fully parsed and say hello is dispatched, the Guards
on the say node run normally. The redirect only affected how the source was modified during parsing — not whether
Guards on the final command run.
Summary:
| Location | Guards run? |
|---|---|
Node that calls redirect(...) or fork(...) |
✗ Never — no execution phase here |
| Target node of the redirect | ✓ Yes — when reached via normal dispatch |
Nodes after run / dispatcher root |
✓ Yes — normal execution flow |
This means Guards are the right place to put validation that applies to the final command being run, not to the
modifier chain itself. Modifier validation (e.g. "does this entity exist?") belongs in the fork or redirect
lambda directly.
Brigadier suggestions normally require manual CompletableFuture<Suggestions> handling. brigadier-kt simplifies this
dramatically.
argument("color", StringArgumentType.word()) {
suggests {
suggest("red")
suggest("green")
suggest("blue")
}
}Notes:
suggestsis called every time the argument needs completion- You can use dynamic values based on current context
- Access
CommandContextviacontextproperty - Access native
SuggestionsBuilderviabuilderproperty - Guards do not run during suggestion — only during execution
argument("permission", StringArgumentType.word()) {
suggests {
suggest("admin", LiteralMessage("Full administrative access"))
suggest("moderator", LiteralMessage("Moderation permissions"))
suggest("user", LiteralMessage("Standard user permissions"))
}
}argument("player", StringArgumentType.word()) {
suggests {
val online = server.getOnlinePlayers()
online.forEach { player ->
suggest(player.name)
}
}
}For advanced cases where you need complete control:
argument("custom", StringArgumentType.word()) {
suggestsOverride { builder ->
// Full CompletableFuture access
builder.suggest("dynamicValue")
builder.buildFuture()
}
}These two features operate at different architectural layers and serve complementary purposes.
Purpose: Control structural accessibility during parsing
Characteristics:
- ✅ Runs during command parsing phase
- ✅ Controls node visibility
- ✅ Affects command tree structure
- ✅ Fast (pre-execution)
- ❌ Cannot access arguments
- ❌ Cannot modify context
- ❌ Cannot return custom result codes
- ❌ Boolean only (allow/deny)
Example:
command<CommandSource>("admin") {
requires { hasPermission("admin") }
literal("reload") {
execute {
println("Reloading...")
SINGLE_SUCCESS
}
}
}When to Use:
- Simple permission checks
- Static access control
- When node visibility matters
- When you need Brigadier's built-in permission system
Purpose: Validate, transform, and prepare for execution
Characteristics:
- ✅ Runs after successful parsing
- ✅ Full argument access
- ✅ Can mutate context
- ✅ Can inject new arguments
- ✅ Can transform arguments
- ✅ Can abort with custom result codes
- ✅ Shared subtree validation logic
- ✅ Domain object transformation
- ❌ Does not affect node visibility
- ❌ Runs after parsing
Example:
argument("group", StringArgumentType.word()) {
val groupRef = createArgRef<Group>("group")
guard {
val name = arg<String>("group")
val group = groupService.find(name)
if (group == null) {
println("Group not found: $name")
return@guard abort()
}
if (!group.hasPermission(source, "view")) {
println("Access denied")
return@guard abort(NO_SUCCESS)
}
groupRef.set(group)
continueCommand()
}
// Handlers receive transformed Group object via groupRef.get()
}When to Use:
- Argument validation
- Domain object resolution
- Complex permission checks requiring context
- Argument transformation
- Shared validation across subtree
- Custom error handling
| Feature | requires | Guard |
|---|---|---|
| Parsing phase | ✓ | ✗ |
| Post-parse validation | ✗ | ✓ |
| Access arguments | ✗ | ✓ |
| Modify context | ✗ | ✓ |
| Argument transformation | ✗ | ✓ |
| Argument injection | ✗ | ✓ |
| Custom result codes | ✗ | ✓ |
| Shared subtree logic | ✗ | ✓ |
| Affects node visibility | ✓ | ✗ |
| Domain object resolution | ✗ | ✓ |
command<CommandSource>("group-admin") {
requires { hasPermission("admin.access") } // Node-level access control
argument("group", StringArgumentType.word()) {
val groupRef = createArgRef<Group>("group")
guard {
val name = arg<String>("group")
val group = groupService.find(name)
if (group == null) {
println("Group not found: $name")
return@guard abort(NO_SUCCESS)
}
// Check if admin has permission for THIS specific group
if (!group.allowsAdmin(source)) {
println("Access denied")
return@guard abort(NO_SUCCESS)
}
groupRef.set(group)
continueCommand()
}
literal("delete") {
requires { hasPermission("admin.group.delete") } // More specific permission
execute {
val group = groupRef.get()
groupService.delete(group)
SINGLE_SUCCESS
}
}
}
}Summary: They complement each other, not replace each other. Use requires for static access control and node
visibility, use Guards for dynamic validation and transformation.
When a command is executed, the following happens in order:
1. User Input
↓
2. Brigadier Parsing
↓
3. requires Predicates (if any)
↓
4. Redirect / Fork resolution (if any)
│ Guards do NOT run here
↓
5. Guards Execute (root → leaf order)
│ runOnSameNode = false guards are skipped
│ if the execute block is on their own node
↓
├─ Guard 1 at root
├─ Guard 2 at intermediate node
└─ Guard 3 at leaf node
↓
6. All Guards returned GuardResult.Continue?
├─ YES → Execute Handler
└─ NO → Stop (return Guard's abort result)
↓
7. Return Result
Clear Separation of Concerns:
- Parsing → Brigadier handles syntax and structure
- Access Control →
requireshandles node-level permissions - Routing →
redirect,fork,forwardhandle execution flow - Validation & Transformation → Guards handle business validation
- Business Logic →
executehandles core functionality
brigadier-kt is built on strict architectural decisions:
-
Non-Invasive Architecture
- Brigadier remains completely untouched
- No modifications to core parsing logic
- Full backward compatibility
-
Middleware-First Execution Model
- Guards create a structured pipeline
- Clear separation between validation and execution
- Predictable execution order
-
Explicit Control Flow
- All Guard results are explicit (
abort()orcontinueCommand()) - No implicit behavior or magic
- Clear intent in code
- All Guard results are explicit (
-
Domain-Driven Transformation
- Support converting parsed primitives to domain objects
- Centralized transformation logic
- Type-safe argument access
-
Kotlin-Native DSL
- Idiomatic Kotlin syntax
- Leverage language features (reified generics, lambdas, etc.)
- Clean, readable command definitions
-
No Hidden Magic
- All behavior is explicit
- No reflection-based surprises
- What you see is what you get
-
Full Brigadier Compatibility
- Works with existing Brigadier code
- Can be adopted incrementally
- Integrates with Brigadier ecosystem
brigadier-kt transforms Brigadier from a low-level parsing API into a structured, middleware-enabled command framework that embraces Kotlin idioms while preserving Brigadier's power and performance.
✅ Clean DSL Structure — Command trees that match your mental model
✅ Type-Safe Arguments — Reified generics eliminate boilerplate
✅ Argument References — argRef() for raw access, createArgRef<T>() for typed domain objects
✅ Middleware Guards — Centralized validation and transformation
✅ runOnSameNode Control — Fine-grained Guard scoping per node
✅ Domain Transformation — Work with domain objects, not primitives
✅ Mutable Context — Safe argument injection via setArgument or ref.set
✅ Redirect / Fork / Forward — Full Brigadier routing support with clean DSL
✅ Node Merging — Self-reference pattern for looping command chains
✅ Suggestions DSL — Simplified completion handling
✅ Full Compatibility — Drop-in enhancement for Brigadier
Author: Fantamomo
License: MIT