Parse dense arguments¶
You want a command to take input a slash-command form cannot express: an
open number of targets, a flag repeated or given several values, a modifier
that applies a set number of times. The typed options in
Declare a command surface cap at 25, are fixed arity
and are what to use for anything the platform can resolve for you. For the
rest, take the text as one string and parse it with dense.
1. Decide where the text comes from¶
dense.Parse takes a string and does not care which. Two sources, two costs:
- A slash-command
Stringoption. No privileged intent, and the platform validated everything around it. Declare the option as usual and read it within.Args.String("input"). - Message content. Needs
NeedMessages, which on Discord is the privileged Message Content intent; see what declaring one costs. Readmsg.Content, strip your own prefix, and parse the rest. This module adds no prefix-command dispatcher; that loop is yours.
Either way, an identifier inside the text is text. <@1531213889763541213>
comes back as that string, not as something the platform resolved. A command
that needs a resolved user or channel puts it in a typed option beside the
dense string, and reads it through Args.User or Args.Channel.
2. Parse¶
p, err := dense.Parse(`attack goblin -t goblin2 goblin3 -d3 1d6 -adv`)
if err != nil {
// ErrUnterminatedQuote, ErrCountWithoutValue, ErrInvalidCount,
// or ErrWordAfterBoolean: say which, and show the line.
return err
}
The grammar has one rule that decides everything: positionals come first, and after the first flag every word belongs to the most recent flag.
| Input | Reading |
|---|---|
attack goblin -adv |
positionals attack goblin; adv present, no values |
attack -adv goblin |
positional attack; adv = goblin |
attack goblin -t a b -t c |
t = a, b, c |
attack -d3 1d6 2d6 |
d = 1d6 and 2d6, each with three uses |
attack -t "-adv" |
t = -adv, because a quoted word is never a flag |
attack -b -2 |
b = -2, because a flag starts with a letter |
Chat clients rewrite punctuation; curly quotes and en or em dashes are put back before parsing, so a flag typed on a phone still parses.
3. Read positionals and flags¶
verb := p.Positionals[0] // "attack"; check len first
targets := p.Strings("t") // []string{"goblin2", "goblin3"}
advantage := p.Has("adv") // true, and p.Values("adv") is empty
for _, d := range p.Values("d") { // Value{Text: "1d6", Uses: 3}
applyDamage(d.Text, d.Uses)
}
Has and Values are two questions on purpose. A flag with no values is
present and boolean; the string true is never invented, so -t and
-t true are different results. Values is nil for an absent flag and empty
for a present one, and Flags() lists names in order of first appearance.
4. Declare booleans when a bare word after one would be wrong¶
Undeclared, attack -adv goblin binds goblin to adv, which is the one
reading the rule allows. If adv can never take a value, say so:
Now attack -adv goblin is refused with ErrWordAfterBoolean, whose message
tells the writer to quote the word or move it before the flags. The
declaration is optional; without it the ordering rule alone decides.
5. Handle the refusals¶
Every refusal is a sentinel you match with errors.Is, and every one is the
writer's to fix, so the useful response is to reply with the message:
| Sentinel | The line |
|---|---|
ErrUnterminatedQuote |
attack "big goblin |
ErrCountWithoutValue |
attack -d3 -t x, a count with nothing to apply it to |
ErrInvalidCount |
attack -d0 1d6, or a count over nine digits |
ErrWordAfterBoolean |
attack -adv goblin with adv declared |
What it does not do¶
No help generation, no subcommand routing, no type conversion: 1d6 is a
string, and rolling it is yours. No -- end-of-flags marker, because
quoting already makes any word a literal; no -abc bundling; no =. A flag
name cannot end in a digit, because trailing digits are the use count. The
reasoning, the alternatives weighed and the measurements behind the grammar
are in spec 0024.