Skip to content

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.

import "gitlab.com/phpboyscout/go/comms/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 String option. No privileged intent, and the platform validated everything around it. Declare the option as usual and read it with in.Args.String("input").
  • Message content. Needs NeedMessages, which on Discord is the privileged Message Content intent; see what declaring one costs. Read msg.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:

p, err := dense.Parse(line, dense.Booleans("adv", "dis"))

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.