Skip to content

Parsing

Three independent modules, reached through Operator.Parsing. Each is pure Luau with no Roblox API calls, so they run outside Studio. The client and the server run exactly this code, which is how the console validates inline without a round trip.

luau
local Operator = require(game:GetService("ServerStorage").Operator)
local Tokenizer, Parser, Binder =
	Operator.Parsing.Tokenizer, Operator.Parsing.Parser, Operator.Parsing.Binder

Tokenizer

luau
local Tokenizer = Operator.Parsing.Tokenizer

Tokenizer.tokenize

luau
Tokenizer.tokenize(source: string): TokenizeResult
luau
local result = Tokenizer.tokenize([[kick bob "being rude"]])

for _, token in result.tokens do
	print(token.value, token.raw, token.start, token.finish, token.quoted, token.terminated)
end

Tokenizer.MaxInputLength

luau
Tokenizer.MaxInputLength: number  --> 2000

Input longer than this is refused outright rather than parsed. That is far above any real command, while still bounding the work a hostile client can cause.

Token

FieldMeaning
valueunescaped and unquoted
rawthe exact source slice
start / finish1-based source positions, which the console uses to underline
quotedwhether a quoted section was used
terminatedwhether its quote was closed

Tokenizer.Diagnostics

UnterminatedQuote · TrailingEscape · InputTooLong

Parser

luau
local Parser = Operator.Parsing.Parser

Parser.parse

luau
Parser.parse(tokens: { Token }): ParsedCommand

Splits tokens into positional words and named flags.

luau
local parsed = Parser.parse(Tokenizer.tokenize([[ban bob --reason="spam bot" --silent]]).tokens)

print(parsed.words[1].value)          --> ban
print(parsed.flags.reason.value)      --> spam bot
print(parsed.flags.silent.value)      --> nil, a presence flag
print(parsed.flagOrder)               --> { "reason", "silent" }

Parser.Diagnostics

MalformedFlag · DuplicateFlag

Binder

luau
local Binder = Operator.Parsing.Binder

Binder.bind

luau
Binder.bind(options: BindOptions): BindResult

Binds words to named arguments. The caller strips the command path from words first, so the binder knows nothing about the registry.

luau
local result = Binder.bind({
	words = words,
	flags = parsed.flags,
	flagOrder = parsed.flagOrder,
	argSpecs = command.argSpecs,
	flagSpecs = command.flagSpecs,
	inputLength = #source,
})

A finished command already carries matching argSpecs and flagSpecs, built once at :run(), so the dispatcher does not rebuild them per call.

Binder.validateSpecs

luau
Binder.validateSpecs(argSpecs: { ArgSpec }): (boolean, string?)

Deliberately separate from bind. The command builder calls it at :run() so a malformed spec fails at load rather than on first use.

ArgSpec

FieldEffect
namethe key the value is bound to
optionalabsence is not an error; must not precede a required argument
variadiccollects every remaining word into a list; must be last
restjoins every remaining word into one string; must be last

Binder.Diagnostics

MissingArgument · TooManyArguments · UnknownFlag · MissingFlagValue · UnexpectedFlagValue

Diagnostics

Nothing in the parsing layer throws on bad user input. Every problem is reported as a diagnostic carrying a kind, a message, and a source range, so the client can render inline validation while the server rejects any input that produced one.

ModuleDiagnostics
Tokenizer.DiagnosticsUnterminatedQuote, TrailingEscape, InputTooLong
Parser.DiagnosticsMalformedFlag, DuplicateFlag
Binder.DiagnosticsMissingArgument, TooManyArguments, UnknownFlag, MissingFlagValue, UnexpectedFlagValue

See Command Syntax for the rules these enforce.

Released under the MIT Licence.