How To Make Beautiful Command-Line Interfaces In Python With Click | by Emmett Boudreau | Towards Data Science

Command Line Interfaces, or CLIs are an important tool that programmers often use to control their software from a terminal or REPL. The problem with command line interfaces is that while they are not particularly hard to make, they can be a bit tedious and annoying to make. Furthermore, they can be very hard to make well, or perfect. Often command line interfaces can end up being a jumbled mess, which is problematic for an application that might have users who are also interacting with a CLI. Things that do not look good can simply be more confusing, we are so used to consuming content, reading, or interpreting things in one way that if they are not up to those specifications it can cause a small short in our brain.
Fortunately for Python programmers, there are some pretty awesome tools available for creating enhanced CLI’s inside of Python. One example of such tools is a Python module called Click. I listed Click in another article about awesome decorators in Python, if you would like to read that article as well you may do so here:
towardsdatascience.com
The Click module is a composable command-line toolkit for Python that was spearheaded by the Pallets team. If you would like to learn more about the Pallets team and Click respectively, you can check out the Click Github page here:
github.com
You might not know it, but it is likely that you have actually used another module from the Pallets team as well. They are also responsible for the Flask WSGI module, which is an industry-standard for Python programmers. Needless to say, the team is notable for these contributions to software and I thank them for their commitments in open-source software.
Today I wanted to go over click and take a look at what the module has to offer. The module is fully-featured and quite robust, so there is a bit to unpack here. We are also going to be involving ourself with a project from another article to demonstrate the capabilities of the package.
Applications of the Click module likely could apply to any type of application being built in Python. This is a great thing, because in many cases Click can certainly spice things up. First I created a nice little directory to store our Python project. Since there will not be much here, and I already have the click module installed in my global environment for Python, I am not going to bother making a virtual environment for this project.
Let us write the basis of our file:
import click as clkdef main():if __name__ == "main":    main()
All we did here was write a main function, import click, and then do the if __name... Python thing to name our main function, since we will likely be calling this code via the CLI. Now let us move onto what the click module actually contains so that we can build some pretty command-line interfaces!
The first thing we are going to need to do in order to utilize click is draw out a clear goal, and determine the parameters that will be provided via the CLI to control that application. I came up with a neat idea for a neat little
battle simulator
that will take randomness and forces into account and deliver a verdict on the battle. I thought this would be a fun project because there can be a good bit of types and things of that nature, maybe I could even open up a new repository for it and create something even cooler out of it in a future article. Given that we are creating a battle simulator, the first thing I am going to do here is define the
“ top of the type heirarchy.”
Listen to me, Julia programmer and all. What I mean is we need an
“ abstract type...”
We need a CLASS which will be a parent class of the other classes. Going from paradigm to paradigm can really be rough on your brain at times like this. First thing is first, let us engage with a visual element to my game, a REPL-bound print and clear loop. The reason why I want to do this first is to see if this idea works, which will help me to realize if I need to implement it the whole time.
I mean this whole thing is quite spontaneous, after all.
While I will not be going into the detail of how I made this little game, you may find exactly that in this article:
towardsdatascience.com
In this article, we are primarily assessing the click module’s application. That being said, in order to understand the project a bit better, I will also be showing the classes and doing some basic explaining to reveal how the game works. For a full, in-depth view of the game and me programming its components, I would certainly suggest reading the article above! Also, here is a link to the branch of the Github repository I am going to be working with today:
github.com
Anyway, the class structure goes a little something like this in this program:
PlayGrid([players])The play-grid will take a player and print a grid with the players on top of it. It takes a full list of players and manages the visual aspect and interactions with other figures on the environment.
Player(pos,
I thought I would write a few articles along with the creation of this neat little project, since I created a whole thing just to put CLI’s into it. In the future, we will train a model to control our player characters, but for now we are simply going to add some command-line arguments and slightly alter the code of this project in order to take this new data in and do something with it. Given that this is basically a character-based 2-D game engine we have written, there are some globally defined variables, and definitely a lot of those we might want to be able to change from the command-line. The main arguments I would like to be able to change are
width of grid
height of grid
number of random characters
In the future, I want to consider allowing the end-user to set the characters, as well.
Without further ado, let us try and accomplish some of these goals with the click module!
I am going to start off by calling the click @click.option() decorator. We will put all of the following decorators atop our main function.
# CLIS@clk.command()@clk.option('--width', default = 30, help = 'Width of the draw grid.')@click.option('--height', default = 100, help =' Height of draw grid.')@clk.option('--players', prompt='Number to simulate',              help='The number of randomly generated players to include.')
This can take various key-word arguments, but the positional arguments are of course the most important, which is simply an argument name. Next, provide help information and things of this nature. We can also prompt the user to enter some values. I also need to remove the following code that will globally define some of these aliases:
CHAR_W = 30CHAR_H = 100rplayers = 20
We will need to populate these as variables likely throughout the entire package, which is a bit of work, but really will not be that terrible. Our main function now looks like this:
click.command()@click.option('--width', default = 30, help = 'Width of the draw grid.')@click.option('--height', default = 100, help =' Height of draw grid.')@click.option('--players', prompt='Number to simulate',              help='The number of randomly generated players to include.')def main():    players = []    players.append(Player([11, 20], 0))    players.append(Player([5, 6], 1))    players.append(Player([20, 10], 2))    players.append(Player([11, 20], 0))    players.append(Player([11, 20], 0))    players.append(Player([11, 20], 0))    players.append(Player([11, 20], 0))    players.append(Player([11, 20], 0))    players.append(Player([11, 20], 0))    game = PlayGrid(players)    for i in range(1, 50):        sleep(.5)        game.update("".join(["Iteration: ", str(i)]))
What needs to be here is a comprehensive while loop that begins the game and continues looping until all the players have died. I am going to go ahead and do this, even though we are focused on the CLI’s because it will make this the last time we need to adjust these arguments, besides some future upgrades I would like to do. We need to add all of our command-line arguments to our main function as arguments:
def main(players, width, height):
The first thing I am going to do is get rid of this big players.append() thing we have going on here, this will all be extracted to an iterative loop instead inside of a new function I have yet to define,
players = random_players(players)
Now the main() function looks a bit like this:
def main(players, width, height):    players = random_players()    game = PlayGrid(players)    for i in range(1, 50):        sleep(.5)        game.update("".join(["Iteration: ", str(i)]))
We are going to go ahead and get rid of this for-loop, and just focus on initialization. We are also going to need to change the way we work with width/height inside of the update() function. Here is a look at that function:
def update(self, message):        clear()        self.empty_grid()        self.draw_players(self.grid)        self.make_moves()        print(self.draw_grid())        print("\n" + message)
We could add the grid width and height into here, since this is called everytime that the grid is drawn. However, I think instead I am going to have the grid sizes be defined as global variables. This will mean that even the players will have the access to know what size the grid is, which might be essential to keep this working. Another great approach might be to put it in the class, but if that is the case, then the initialized version of the class will need to be passed as self whenever any method from the player is called. This certainly could make sense, but given that this is more a talk of the Grid being the dimensions of the window, it might be a good idea to keep such a thing global so we always know its size. Given that these are CLIs, they will be global before us, so we can ignore those arguments.
players = random_players()    game = PlayGrid(players)    for i in range(1, 50):        sleep(.5)        game.update("".join(["Iteration: ", str(i)]))
Now two of the three things are taken care of, and the only thing we have to do is create a basic while loop that will do everything while the players > 1. This way the game will not end until a player has eliminated all of the others, which by the way is not implemented yet — so we are going to be watching these senseless battles forever.
forever.
round_counter = 0    while len(players) > 1:        round_counter += 1        sleep(.5)        game.update("".join(["Iteration: ", round_counter]))
Now we just need to write our random players function:
def random_players(n_p):    players = []    for p in range(0, n_p):        pos = [random.randrange(1, width), random.randrange(1, height)]        player = Player(pos, p)        players.append(player)    return(players)
The Player() constructor needs a position and an ID. The ID is its position of index in the players list.
Now lets try and run this:
players    for p in range(0, n_p):TypeError: 'str' object cannot be interpreted as an integer
We can fix this by simply adding an integer cast when we pass that type through.
def main(players, width, height):    players = random_players(int(players))
Strong typing is great.
I also renamed width and height to w and h so that we can adjust the global definitions independently of the CLI. Here is a look at the whole opening package:
import click as clkimport randomfrom time import sleepfrom os import system, namewidth = 30height = 100# CLIS@clk.command()@clk.option('--w', default = 30, help = 'Width of the draw grid.')@clk.option('--h', default = 100, help =' Height of draw grid.')@clk.option('--players', prompt='Number to simulate',              help='The number of randomly generated players to include.')def main(players, w, h):    players = random_players(int(players))    game = PlayGrid(players)    round_counter = 0    width = w    height = h    while len(players) > 1:        round_counter += 1        sleep(.5)        game.update("".join(["Iteration: ", round_counter]))
Okay, let us try this out!:
[emmac@fedora CharacterClash]$ python3 character_clash.py.......yers    current = list(grid[newpos[1]])KeyError: 40[emmac@fedora CharacterClash]$
My best guess as to why this is happening for right now is that our range is mixing our x and y values. Funny story, I had a big issue because they were flipped inside of the function that draws the grid:
def empty_grid(self):        self.grid = dict()        str = ""        for row in range(1, height):            str = ""            for i in range(1, width):                str += "`"            self.grid[row] = str
Now I get another error, which is similar:
File "/home/emmac/dev/CharacterClash/character_clash.py", line 55, in draw_players    current[newpos[1]] = player.symbol[0]IndexError: list assignment index out of range
I think the issue here is likely with the indexing I am doing here. It should be newpos[0] not newpos[1]. This time we are calling the x by mistake when we should call the y. Allow me to explain a bit better with a bit of a reference in the code:
current = list(grid[newpos[1]])current[newpos[0]] = player.symbol[0]current[newpos[0] + modifier] = player.symbol[1]
In this function, current is a lit of characters in the string that is that column. We selected that column by calling the grid dict with our y value. Now we call the x value in the index of current in order to get the position and replace it with our character symbol, hopefully with apt results.
One last thing, we are from time to time getting this key-error:
File "/home/emmac/dev/CharacterClash/character_clash.py", line 54, in draw_players    current = list(grid[newpos[1]])KeyError: 0
This key error may lead one to believe that the draw_players function is not working properly, but really the data is not in a proper format to work with that function — there is no 0 index of the grid, which we need. We just need to change the for loop that populates this dictionary:
def empty_grid(self):        self.grid = dict()        str = ""        for row in range(1, height):            str = ""            for i in range(1, width):                str += "`"            self.grid[row] = str
Replace 1 with 0,
for row in range(0, height):            str = ""            for i in range(0, width):
We also need to add + 1 to the height, that way all of the grid gets populated. Anytime a position comes through the player movement function, we will need to check and make sure it is not in the bounds. However, I think a proper way to do this would be to program some collision, and since we can now test out our CLI’s, I think we shall do so. Whenever we run our new file, we get the following result:
[emmac@fedora core]$ python3 character_clash.pyNumber to simulate:
I will now try my best to get a screenshot of each loop. In order to help me with this, I am going to set the sleep() timer for 2 seconds. This will be followed by an animated GIF of what played out on screen:
[emmac@fedora core]$ python3 character_clash.pyNumber to simulate: 5
The result of all of this work so far is pretty cool, as you can see in the animation above. Additionally, when calling we have options to adjust the height and width:
[emmac@fedora core]$ python3 character_clash.py --helpUsage: character_clash.py [OPTIONS]Options:  --w INTEGER     Width of the draw grid.  --h INTEGER     Height of draw grid.  --players TEXT  The number of randomly generated players to include.  --help          Show this message and exit.[emmac@fedora core]$
If we are not provided with the players argument then we will prompt the user. That was incredibly easy, and the output is very concise. Obviously, there is a lot more to do. The flipping does not work exactly correct, and some of the coordination is wrong in some areas. Furthermore, there is not any AI violence just yet, but I am soon to come with another article on this project where we finish this portion and more coming afterwards with this hilarious little project. For now, let us talk a bit more about click.
The great thing about Click is that it provides you with a complete data-template printout that echos based on the data of your arguments. Furthermore, Click includes all different types of arguments, and even has support for all sorts of different calls. Sure we could have started our main() function with some input, but parsing it as an incoming CLI was a lot cooler.
Not only that, but the actual Click portion of this project was incredibly easy code to work with. This is another one of those modules that sprinkles decorator magic all over Python, which is something I really do love. So yes, I believe that Click is a bit easier, and includes a lot of really sweet components, it allows you to write passwords, prompt for arguments, provide choices, all kinds of neat things that most other options do not offer. That being said, it is also from Pallets, and their software is likely worth checking out either way.
This is a really cool project, and I am very happy to see it continue coming along. It might not have been the least complex choice in the world, but it is rather fun to work with, and I think that my approach to doing it was pretty awesome in all honesty. In doing this project, I have wondered a lot about how cool it would be to have a little REPL-based engine that took care of all the display for you so one could write really interactive REPL’s with visual feedback and such things. This seems like a really neat idea, I am sure perhaps it has been attempted before.
However, truthfully if I were going to write such a thing, I would probably use a lot more types and also write it in Julia. Sometimes projects grab me and I feel as though I should try to make them awesome. Thank you very much for reading, it means the world to me! Hopefully this article was helpful in making your CLI’s come to life, and with this real-project based example perhaps made more sense of how to implement this module into some software. Happy programming,
and happy (almost) new year!