Slash Command Grammar DSL

View Source

The slash/2 DSL is built to make slash commands deterministic, easy to maintain, and fast. Instead of manually splitting strings or juggling regexes, you describe the format you expect and SlackBot generates a parser at compile time. Write your grammar by chaining primitives (literal/2, value/2, optional/1, etc.) and always finish with a single handle/3 clause—nothing may appear after handle/3, and the compiler raises if you try. This guide teaches the DSL in layers so you can follow along as the commands grow in complexity.


Why use the DSL?

  • Deterministic parsing – handlers receive structured maps, not ad-hoc token lists.
  • Readable expectations – the command format lives next to the handler, making code reviews and maintenance straightforward.
  • Compile-time validation – malformed definitions fail fast, before your bot ships.
  • Battle-tested parsing – handles quoting, whitespace, and tricky edge cases without extra work on your part.

1. Literal-only commands

Great for “one-shot” commands that trigger behavior without arguments.

slash "/cmd" do
  literal "project"
  literal "report"

  handle payload, ctx do
    # payload["parsed"] => %{command: "cmd"}
    Reports.generate(ctx)
  end
end

Slack input: /cmd project report


2. Capturing values

Use value/1 to bind user-provided tokens to names that show up in the parsed payload.

slash "/cmd" do
  literal "team", as: :mode, value: :team_show
  value :team_name
  literal "show"

  handle payload, ctx do
    %{team_name: name} = payload["parsed"]
    Teams.show(name, ctx)
  end
end

Slack input: /cmd team marketing show
Parsed payload: %{command: "cmd", mode: :team_show, team_name: "marketing"}


3. Optional segments

Wrap anything that isn’t required in optional. Omitted segments simply don’t appear in the parsed map.

slash "/cmd" do
  literal "list", as: :mode, value: :list
  optional literal("short", as: :short?)
  value :app

  handle payload, _ctx do
    payload["parsed"]
  end
end

Slack input: /cmd list short foo
Parsed payload: %{command: "cmd", mode: :list, short?: true, app: "foo"}


4. Repeating segments

repeat lets you express “zero or more” patterns. Each value inside becomes a list.

slash "/cmd" do
  literal "report", as: :mode, value: :report_teams

  repeat do
    literal "team"
    value :teams
  end

  handle payload, _ctx do
    payload["parsed"]
  end
end

Slack input: /cmd report team alpha team beta team gamma
Parsed payload: %{teams: ["alpha", "beta", "gamma"], mode: :report_teams}


5. Branching with choice

Many commands act like subcommands. choice lets you express each branch declaratively.

slash "/cmd" do
  choice do
    sequence do
      literal "list", as: :mode, value: :list
      optional literal("short", as: :short?)
      value :app
    end

    sequence do
      literal "project", as: :mode, value: :project_report
      literal "report"
    end
  end

  handle payload, ctx do
    parsed = payload["parsed"]
    handle_mode(parsed.mode, parsed, ctx)
  end
end

Slack inputs covered: /cmd list app, /cmd list short app, /cmd project report


6. End-to-end example

The tests (test/slack_bot/router_test.exs) contain a full “GrammarRouter” that combines all the primitives. Here’s how a few Slack inputs map to payloads:

Slack inputParsed payload
/cmd list short app param one param two%{mode: :list, short?: true, app: "app", params: ["one","two"]}
/cmd project report%{mode: :project_report}
/cmd team marketing show%{mode: :team_show, team_name: "marketing"}
/cmd report team one team two team three%{mode: :report_teams, teams: ["one","two","three"]}

Each branch is explicit, and the handler simply reacts to structured data.


Handler payload structure

Every DSL handler receives an enriched payload under payload["parsed"]:

%{
  command: "cmd",
  mode: :list,
  short?: true,
  app: "foo",
  params: ["one", "two"],
  teams: ["alpha", "beta"],
  extra_args: ["leftover"] # present only if tokens remain unmatched
}
  • Repeated values become lists.
  • Optional literals store the value: option (default true) when matched.
  • Any leftover tokens land in :extra_args, allowing custom fallbacks.

Quick reference

MacroPurposeExample
literal value, opts \\ []Match a literal token, optionally tagging metadataliteral "list", as: :mode, value: :list
value name, opts \\ []Capture a token and assign it to namevalue :service
optional do ... endOptional group; skipped segments leave previous values untouchedoptional literal("short", as: :short?)
repeat do ... endRepeat group until it no longer matchesrepeat do literal "team"; value :teams end
choice do ... endFirst matching branch winschoice do sequence ... end
sequence do ... endExplicit grouping (helpful inside choice)sequence do literal "project"; literal "report" end
handle payload, ctx do ... endHandler that receives the enriched payloadhandle payload, ctx do ... end

Tips

  • Use SlackBot.Diagnostics.list/2 + replay/2 to capture real commands and verify they parse as expected.
  • Prefer small, focused choice branches over one giant handler with nested case.
  • Need raw tokens? Call SlackBot.Command.lex/1 yourself.
  • See test/slack_bot/router_test.exs for more real-world examples.

Next Steps