DEV Community

Discussion on: How I want my command inputs, but nobody agrees

Collapse
 
geordiepowers profile image
Geordie Powers

I've spent a bunch of time thinking about how to handle commands as well, and I really like what I've come up with. I hated how it felt to use an if statement or most OO concepts, and a switch/match statement also didn't feel ideal.

My use case was also with a Discord bot, and this is how I've done it in Go:

Define a "command" type that's just function signature:

type command func(session *discordgo.Session, message *discordgo.Message)

Discordgo's session and message structs contain virtually all the information I've needed to implement a command's functionality, so they're the only arguments the function needs to take in the case of this bot.

Now create a place to store commands - a map of "command" type objects, keyed on strings (this is the important bit!)

var commands map[string]command = make(map[string]command)

Now we can write a function to map a string to a command handler function, and store it in the commands map.

func Register(name string, handlerFunc command) {
    commands[name] = handlerFunc
}

I've been defining these in a package called "commands" so I can use them anywhere; now all I have to do is import that package and call the register function like so:

commands.Register("!help", func(session *discordgo.Session, message *discordgo.Message) {
    // send help message reply
    session.ChannelMessageSend(message.ChannelID, "[help message contents here]")
})

Now when a message comes into the bot, we can check to see if it's a command and simply call the function in the map.

func ProcessCmd(session *discordgo.Session, message *discordgo.Message) {
    // leaving out things here for brevity, replacing with comments...
    // determine if message starts with our command prefix (! or something), return if it doesn't
    // split the message on spaces or whatever delimiter you choose
    // take the first argument, the command name (this will be !help in the case of a help command), store it in cmdName string variable

    // now check if this command name is registered as a command, and if it is, store it in the "cmdFunc" variable. Then we can simply call it and pass our arguments!
    if cmdFunc, ok := commands[cmdName]; ok {
        cmdFunc(session, message)
    }
}

Now whenever the bot sees a message beginning with "!help", it'll call the function we've registered in the map at the "!help" key.

Out of all the approaches I've tried so far, I like this one the best. I can store the commands anywhere (rather than a static map in a single package as in this example), register commands from any file, split up the code (so not all commands have to be defined in the same spot, long functions can be moved elsewhere, even to other packages). It's even sometimes useful to pass a wrapper to the commands.Register function down to other packages to limit or augment their ability to register their own commands (eg: don't allow a certain package to register commands that kill our Go process, but do allow others - maybe one that defines administrator commands?)

With this system, we can also un-register (and even register!) commands at runtime very easily:

func Unregister(name string) {
    delete(commands, name)
}

With some nicely defined command syntax, it's possible to (for example) have admin users use a command to register their own simple commands. Imagine this being an incoming Discord message:

!createReplyCommand trigger="hello" reply="world!

It's easy to see how we could then parse the contents of this message and generate a new handler function that looks for a "hello" message and replies "world!"

I originally did this all in JavaScript. I haven't gotten to this bit in my Go implementation yet, but in JS this system allowed for "plugins" to my Discord bot to be loaded (and unloaded) at runtime and register their functionality without restarting the program. These plugins are just independent JS files that adhered to the bot's core API.

I'm also planning to play around with command handling ideas in rust, and my first idea was to use a match.. but I'm not sold on it yet. I'd like to figure out what else can be done!

Collapse
 
legolord208 profile image
jD91mZM2

I like your approach! It's sad however that it requires a map :(

Collapse
 
wefhy profile image
wefhy • Edited

In fact it does not. You can use it even in C with structures like:
gist.github.com/wefhy/4fa3039bac8a...
And additionally you can automatically generate help message from this.
But I also love Rust for things like this ;)

Collapse
 
secretlyjaron profile image
Not Jaron

In C/C++ you could do something like this with XMacros so the mapping gets taken care of at compile time. Might be able to come up with a solution using this: internals.rust-lang.org/t/x-macro-... - Kind of a pain to set up, though!

Collapse
 
geordiepowers profile image
Geordie Powers

It's not ideal, but I'm not too concerned. In my benchmarks versus the switch style, it's about half as fast to process a command. Startup time is of course greatly increased, but since the system allows for new commands to be later added during runtime, program startup doesn't need to happen often.

I can't speak for every situation, but my Discord bots aren't super performance-critical, so I find the maintainability and expressive freedom offered by this solution to be a much bigger win.