Customizing Python argparse for Slack slash commands

Sumanth Reddy
3 min readMay 4, 2021

We all love slack for the slick UI and its many features. Slack also has apps and something called slash commands to boost productivity. So what are slash commands?

Slash commands act as shortcuts for specific actions in Slack. There are three types of slash commands that you may be able to use in your workspace:

Built-in slash commands created by Slack

App slash commands created by developers

Customised slash commands created by members of your organisation

More at https://api.slack.com/interactivity/slash-commands

The third option helps us do custom actions using our own slack commands. See https://api.slack.com/tutorials/your-first-slash-command for creation of custom slash commands. This article assumes you have custom slack command for some app already installed which can be invoked like:

/my-command

In layman terms, slash commands are like text which is POST’ed to some webhook/endpoint. So in above case /my-command is send as text body to some endpoint. You can even do something like below too:

/my-command foo --bar=baz

Like a command line binary syntax but simply from slack 🤗 Awesome right 😍

Assuming the webhook or endpoint runs Python app, we can use super powerful python argparse cli library to handle this slack slash command. While in principle, since we get simple text command from slack, we can break the string and parse what command has been sent and options passed etc and do custom validations and send nice error messages. But why go all the lengths when we have super cool library already available to do all these 🤭

For the above given command, assuming foo as argument —bar as an optional parameter, the simple python code would be as below:

import argparse

parser = argparse.ArgumentParser(description='Process my command')
parser.add_argument('foo', type=str, help='Magic of foo')
parser.add_argument(
'--bar',
action='store',
required=False,
type=str,
default='baz',
help='Bar optional param',
)

args = parser.parse_args(command.split())

Here the parser.parse_args() takes the input from stdin by default or can be supplied with our own args like parser.parse_args('foo --bar=baz'.split())

So basically what the above code does is declare the command params and manually feed the slack slash command we received as text in our python app.

Now this all seems good until we hit an error or help text. argparse. Because by default argparse prints to stdout and exit 😕 Doesn’t exactly help with our REST app right!!!

So we now have two issues to resolve before we go ahead to use argparse in our app:

  1. Argparse exits on error or help flag. ← No, don’t exit.
  2. Argparse prints out error/help flag to stdout — we need to send back this as response to slack. ← Capture the error/help text.

The easiest solution for #1 above is to override only those methods from the main ArgumentParser class which exits 🙃 Source for exiting methods at https://github.com/python/cpython/blob/3.8/Lib/argparse.py#L2503

Now our code just uses our CustomArgumentParser class instead of main class, So the code changes to this:

import CustomArgumentParser

parser = CustomArgumentParser(description='Process my command')
parser.add_argument('foo', type=str, help='Magic of foo')
parser.add_argument(
'--bar',
action='store',
required=False,
type=str,
default='baz',
help='Bar optional param',
)

args = parser.parse_args(command.split())

Now for #2 problem of capturing stdout, StringIO class in python comes to our rescue. — https://docs.python.org/3.8/library/io.html#io.StringIO

from io import StringIO
import sys
# hack to capture stdout for argparse help/error and send it to
# client.
sys.stdout = result = StringIO()
error_text = ''
try:
cmd_params = p.parser.parse_args(command.split())
except:
# Restore the stdout stream for future code.
sys.stdout = sys.__stdout__
error_text = result.getvalue()

In brief, we are just overriding stdout with our StringIO stream and thereby capturing everything written on stdout into our string. Neato 🤭

The overall code translates roughly into this:

— Stay Positive and Test Negative —

--

--