Skip to content

Instantly share code, notes, and snippets.

@tobert
Last active May 3, 2016 20:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tobert/014f3806704c24979fb9e368fbdf1544 to your computer and use it in GitHub Desktop.
Save tobert/014f3806704c24979fb9e368fbdf1544 to your computer and use it in GitHub Desktop.
package hal
import (
"regexp"
"time"
"github.com/netflix/hal-9001/hal"
)
/* hal.Cmd, hal.Param, hal.CmdInst, and hal.ParamInst are handles for hal's
* command parsing. While it's possible to use the standard library flags or an
* off-the-github command-line parser, they have proven to be clunky and often
* hacky to use. This API is purpose-built for building bot plugins, where folks
* expect a little more flexibility and the ability to use events as context.
* Rules (as they form)!
* 1. Cmd and Param are parsed in the order they were defined.
* 2a. "*" as user input means "whatever, from the current context" e.g. --room *
* 2b. "*" as a Cmd.Token means "anything and everything remaining in argv"
*/
// common REs for Param.ValidRE
const IntRE = `^\d+$`
const FloatRE = `^(\d+|\d+.\d+)$`
const BoolRE = `(?i:^(true|false)$)`
const CmdRE = `^!\w+$`
const SubCmdRE = `^\w+$`
// Cmd models a tree of commands and subcommands along with their parameters.
// The tree will almost always be 1 or 2 levels deep. Deeper is possible but
// unlikely to be much higher, KISS.
//
// Key is the command name, e.g.
// "!uptime" => Cmd{"uptime"}
// "!mark ohai" => Cmd{Token: "mark", Cmds: []Cmd{Cmd{"*"}}}
// "!prefs set" => Cmd{Token: "prefs", MustSubCmd: true, Cmds: []Cmd{Cmd{"set"}}}
type Cmd struct {
Token string `json:"token"` // * => slurp everything remaining
Usage string `json:"usage"`
Params []*Param `json:"parameters"`
SubCmds []*Cmd `json:"subcommands"`
Prev *Cmd // parent command, nil for root
MustSubCmd bool // a subcommand is always required
}
// does anyone else feel weird writing ParamInsts?
type CmdInst struct {
Cmd *Cmd `json:"command"`
SubCmd *Cmd `json:"subcommand"`
ParamInsts []*ParamInst `json:"parameters"`
}
// TODO: consider whether it makes sense to generate typed params e.g.
// type IntParam struct { Default int }
// func (p *IntParamInst) Value() int { return p.value }
// There aren't many of them, they are unlikely change much, and the resulting
// code would get full type checking.
// Same for CmdInst.
// Param defines a parameter of a command.
type Param struct {
Key string `json:"key"`
Default string `json:"default"`
Usage string `json:"usage"`
Required bool `json:"required"`
ValidRE string `json:"validation_re2"`
validre *regexp.Regexp
}
type ParamInst struct {
Param *Param `json:"param"`
Found bool // was the parameter set?
Value string // provided value or the default
Cmd *Cmd // the command the parameter is attached to
}
func NewCmd(token string) *Cmd {
cmd := Cmd{
Token: token,
Params: make([]*Param, 0),
SubCmds: make([]*Cmd, 0),
}
return &cmd
}
func (c *Cmd) subcmds() []*Cmd {
if c.SubCmds == nil {
c.SubCmds = make([]*Cmd, 0)
}
return c.SubCmds
}
func (c *Cmd) params() []*Param {
if c.Params == nil {
c.Params = make([]*Param, 0)
}
return c.Params
}
// chaining methods - see example() below
// TODO: these are stubs
func (c *Cmd) AddParam(key, def string, required bool) *Cmd { return c }
func (c *Cmd) AddAlias(name, alias string) *Cmd { return c }
func (c *Cmd) AddUsage(name, usage string) *Cmd { return c }
func (c *Cmd) AddPParam(position int, def string, required bool) *Cmd { return c }
func (c *Cmd) AddCmd(name string) *Cmd { return c }
// GetParam gets a parameter by its key. Returns nil for no match.
func (c *Cmd) GetParam(key string) *Param {
for _, p := range c.params() {
if p.Key == key {
return p
}
}
return nil
}
// GetSubCmd gets a subcommand by its token. Returns nil for no match.
func (c *Cmd) GetSubCmd(token string) *Cmd {
for _, s := range c.subcmds() {
if s.Token == token {
return s
}
}
return nil
}
// KeyParam adds "key" parameter with validation and can be chained.
// TODO: if there are going to be a few of these, maybe they should be generated.
func (c *Cmd) KeyParam(required bool) *Cmd {
p := Param{
Key: "key",
ValidRE: "^key$",
Usage: "--key/-k the key string",
Required: required,
}
p.validre = regexp.MustCompile("^key$")
c.Params = append(c.Params, &p)
return c
}
// TODO: Add SetDefault(string), etc. if method chaining proves to be useful.
// TODO: these are stubs
func (c *Cmd) AddUserParam(def string, required bool) *Cmd { return c }
func (c *Cmd) AddRoomParam(def string, required bool) *Cmd { return c }
func (c *Cmd) AddBrokerParam(def string, required bool) *Cmd { return c }
func (c *Cmd) AddPluginParam(def string, required bool) *Cmd { return c }
func (c *Cmd) AddIdParam(def string, required bool) *Cmd { return c }
// parse a list of argv-style strings (0 is always the command name e.g. []string{"prefs"})
func (c *Cmd) Process(argv []string) *CmdInst { return &CmdInst{} }
//prefs := pc.ProcessEvent(evt)
func (c *Cmd) ProcessEvent(evt *hal.Evt) *CmdInst {
argv := evt.BodyAsArgv()
ci := c.Process(argv)
// TODO: process "*" options and fill them in from evt
return ci
}
// SubCmdKey returns the subcommand's key string. Returns empty string
// if there is no subcommand.
func (c *CmdInst) SubCmdToken() string {
if c.SubCmd != nil {
return c.SubCmd.Token
}
return ""
}
// helpers to convert strings to commonly-used types
// TODO: these are stubs
func (p *ParamInst) String() string { return "" }
func (p *ParamInst) Int() int { return 0 }
func (p *ParamInst) Float() float64 { return 0.0 }
func (p *ParamInst) Bool() bool { return false }
// include almost-but-not-quite-precise intervals like day, week, and year
// TODO: this is a stub
func (p *ParamInst) Duration() time.Duration { return time.Second }
// handle a variety of formats automatically
// TODO: this is a stub
func (p *ParamInst) Time() time.Time { return time.Now() }
func example(evt hal.Evt) {
// example 1
oc := Cmd{
Token: "oncall",
MustSubCmd: true,
Usage: "search Pagerduty escalation policies for a string",
SubCmds: []*Cmd{
NewCmd("cache-status"),
NewCmd("cache-interval").AddPParam(0, "1h", true),
NewCmd("*"), // everything else is a search string
},
}
oc.GetSubCmd("cache-status").Usage = "check the status of the background caching job"
oc.GetSubCmd("cache-interval").Usage = "set the background caching job interval"
oc.GetSubCmd("*").Usage = "create a mark in time with an (optional) text note"
// hmm maybe we can abuse varargs a bit without ruining safety....
// basically achieves a type-safe kwargs...
// NewCmd("*", Usage{"create a mark in time with an (optional) text note"})
oci := oc.Process(evt.BodyAsArgv())
switch oci.SubCmdToken() {
case "cache-status":
cacheStatus(&evt)
case "cache-interval":
cacheInterval(&evt, oci)
case "*":
search(&evt, oci)
}
// example 2
// Alias: requiring explicit aliases instead of guessing seems right
// the following appear in a couple plugins so they'll probably have built
// in helpers for consistency, e.g. pc.Cmd("set").KeyParam(true).RoomParam(false)
pc := NewCmd("prefs")
pc.AddCmd("set").
AddParam("key", "", true).
AddAlias("key", "k"). // vertically aligned for your viewing pleasure
AddParam("value", "", true).
AddAlias("value", "v").
AddParam("room", "", false).
AddAlias("room", "r").
AddUsage("room", "Set the room ID").
AddParam("user", "", false).
AddAlias("user", "u").
AddParam("broker", "", false).
AddAlias("broker", "b")
// consider doing evt.ProcessCmd(cmd) or something like that
// so this can go in its own package that isn't coupled to the rest of hal
prefs := pc.ProcessEvent(&evt)
switch prefs.SubCmdToken() {
case "set":
setPref(&evt, prefs)
}
}
// stubs for example
func cacheStatus(evt *hal.Evt) {}
func cacheInterval(evt *hal.Evt, oci *CmdInst) {}
func search(evt *hal.Evt, oci *CmdInst) {}
func setPref(evt *hal.Evt, oci *CmdInst) {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment