Beginner’s Guide to Creating Your Own Python Shell with the cmd Module

0
10



Image by Author | Ideogram

 

Introduction

 
I am sure most of you have used a command shell at some point. If you haven’t, while it may look boring compared to a GUI, a command-line interface (CLI) is very lightweight and gives you much more control. Some common examples are SQLite, Git, and the Python REPL. In this tutorial, we will learn how to use Python’s cmd module to build our own interactive shell without any external dependencies. It’s very simple and intuitive to get started. Working on projects like this helps you understand how things work under the hood—things you normally use every day but don’t think much about. It’s also useful if you ever plan to build admin consoles for your applications.

We’ll build our shell in small chunks, starting with a minimal example and then gradually adding commands, help, basic validation, aliases, and friendly output. The only requirement from your side is to have a basic familiarity with Python functions and classes. So, let’s get started.

 

Step-by-Step Process to Build Your Own Python Shell

 
Let’s have a quick overview of the cmd module, since we’ve already used it but haven’t explained what it is. It provides a base class (cmd.Cmd) to build line-oriented command interpreters, which means it processes one command at a time. It also takes care of reading input via a prompt, command dispatch (mapping text to methods), and the help system. You implement commands by defining methods named do_<command>. The module handles the rest—easy and simple.
 

// Step 1: Creating the Minimal Shell

Create a file called myshell.py:

import cmd

class MyShell(cmd.Cmd):
    intro = "Welcome to MyShell! Type help or ? to list commands.\n"
    prompt = "(myshell) "

    def do_greet(self, arg):
        """Greet the named person: greet <name>"""
        name = arg.strip() or "stranger"
        print(f"Hello, {name}!")

    def do_exit(self, arg):
        """Exit the shell."""
        print("Goodbye!")
        return True  # Returning True tells cmdloop() to exit

if __name__ == "__main__":
    MyShell().cmdloop()

 

Run the code using:

 

Now, let’s try a few commands. cmdloop() will start the interactive loop, and we will be able to interact as shown below:

Welcome to MyShell! Type help or ? to list commands.
(myshell) help

Documented commands (type help <topic>):
========================================
exit  greet  help

(myshell) ?
Documented commands (type help <topic>):
========================================
exit  greet  help

(myshell) greet kanwal
Hello, kanwal!

(myshell) exit
Goodbye!

 

 

// Step 2: Parsing Arguments Cleanly

For more complex commands that require multiple input values—such as adding or multiplying—you will need something to parse spaces, quotes, and so on. For this, we will use shlex.split. Let’s take a look at the example code:

import cmd
import shlex

class MyShell(cmd.Cmd):
    intro = "Welcome to MyShell! Type help or ? to list commands.\n"
    prompt = "(myshell) "

    def do_add(self, arg):
        """Add numbers: add 1 2 3"""
        try:
            parts = shlex.split(arg)
            nums = [float(p) for p in parts]
        except ValueError:
            print("Error: all arguments must be numbers.")
            return
        except Exception as e:
            print(f"Parse error: {e}")
            return

        total = sum(nums)
        print(f"Sum = {total}")

    def do_exit(self, arg):
        """Exit the shell."""
        print("Goodbye!")
        return True

if __name__ == "__main__":
    MyShell().cmdloop()

 
Now, let’s give it a try:

Welcome to MyShell! Type help or ? to list commands.

(myshell) ?
Documented commands (type help <topic>):
========================================
add  exit  help

(myshell) add 2450 3456 8904 3467 
Sum = 18277.0

(myshell) exit
Goodbye!

 

// Step 3: Adding a Help System

We have already seen one help command above. That’s the default one that the cmd module creates automatically. It shows the method docstrings. You can also add custom help for a command or topic like this:

class MyShell(cmd.Cmd):
    intro = "Welcome to MyShell! Type help or ? to list commands.\n"
    prompt = "(myshell) "

    def do_greet(self, arg):
        """Greet someone. Usage: greet <name>"""
        name = arg.strip() or "stranger"
        print(f"Hello, {name}!")

    def help_greet(self):
        print("greet <name>")
        print("  Prints a friendly greeting.")
        print("  Example: greet Alice")

    def do_exit(self, arg):
        """Exit the shell."""
        print("Goodbye!")
        return True

    def do_help(self, arg):
        # Keep default behavior, but show a tiny guide when no arg
        if arg:
            return super().do_help(arg)
        super().do_help(arg)
        print()
        print("Basics:")
        print("  - Use 'help' or '?' to list commands.")
        print("  - Use 'help <command>' for details.")
        print("  - Arguments support quotes (via shlex).")

 

Welcome to MyShell! Type help or ? to list commands.

(myshell) help
Documented commands (type help <topic>):
========================================
exit  greet
Undocumented commands:
======================
help
Basics:
  - Use 'help' or '?' to list commands.
  - Use 'help <command>' for details.
  - Arguments support quotes (via shlex).
  
(myshell) help greet
greet <name>
  Prints a friendly greeting.
  Example: greet Alice
  
(myshell) help exit
Exit the shell.

(myshell) exit
Goodbye!

 

// Step 4: Handling Errors and Unknown Commands

We can override the default() method to intercept unknown commands. You can also use emptyline() to control what happens when the user presses Enter with no input. Let’s make the shell do nothing if no input has been entered:

class MyShell(cmd.Cmd):
    prompt = "(myshell) "

    def default(self, line):
        print(f"Unknown command: {line!r}. Type 'help' to list commands.")

    def emptyline(self):
        # By default, pressing Enter repeats the last command. Override to do nothing.
        pass

    def do_exit(self, arg):
        """Exit the shell."""
        print("Goodbye!")
        return True

 

(myshell) help

Documented commands (type help <topic>):
========================================
exit  help

(myshell) 
(myshell) 
(myshell) exit
Goodbye!

 

// Step 5: Adding Command Aliases

Aliases are short names. Tons of my friends have aliases as well, especially those with the same names. Similarly, you can use a shorter—and easier to remember—version of commands by implementing aliases in the precmd() method. Here’s how you can do this:

import shlex
import cmd

class MyShell(cmd.Cmd):
    prompt = "(myshell) "
    aliases = {
        "quit": "exit",
        "q": "exit",
        "hello": "greet",
        "sum": "add",
    }

    def precmd(self, line: str) -> str:
        # Normalize/massage input before dispatch
        parts = line.strip().split(maxsplit=1)
        if not parts:
            return line
        cmd_name = parts[0]
        rest = parts[1] if len(parts) > 1 else ""
        if cmd_name in self.aliases:
            cmd_name = self.aliases[cmd_name]
        return f"{cmd_name} {rest}".strip()

    # Define commands used above
    def do_greet(self, arg):
        """Greet someone. Usage: greet <name>"""
        name = arg.strip() or "stranger"
        print(f"Hello, {name}!")

    def do_add(self, arg):
        """Add numbers: add 1 2 3"""
        try:
            nums = [float(x) for x in shlex.split(arg)]
        except Exception:
            print("Usage: add <num> [<num> ...]")
            return
        print(f"Sum = {sum(nums)}")

    def do_exit(self, arg):
        """Exit the shell."""
        print("Goodbye!")
        return True

 

(myshell) ?
Documented commands (type help <topic>):
========================================
add  exit  greet  help

(myshell) sum 2 3 4
Sum = 9.0

(myshell) add 2 3 4
Sum = 9.0

(myshell) q
Goodbye!

 

// Step 6: Putting It All Together

import cmd
import shlex

class MyShell(cmd.Cmd):
    intro = "Welcome to MyShell! Type help or ? to list commands."
    prompt = "(myshell) "

    # Simple aliases
    aliases = {"q": "exit", "quit": "exit", "hello": "greet", "sum": "add"}

    # ----- Input processing -----
    def precmd(self, line):
        parts = line.strip().split(maxsplit=1)
        if not parts:
            return line
        cmd_name = parts[0]
        rest = parts[1] if len(parts) > 1 else ""
        if cmd_name in self.aliases:
            cmd_name = self.aliases[cmd_name]
        return f"{cmd_name} {rest}".strip()

    def emptyline(self):
        # Do nothing on empty line
        pass

    def default(self, line):
        print(f"Unknown command: {line!r}. Type 'help' to list commands.")

    # ----- Commands -----
    def do_greet(self, arg):
        """Greet someone. Usage: greet <name>"""
        name = arg.strip() or "stranger"
        print(f"Hello, {name}!")

    def help_greet(self):
        print("greet <name>")
        print("  Prints a friendly greeting.")
        print("  Example: greet Alice")

    def do_add(self, arg):
        """Add numbers: add 1 2 3"""
        try:
            nums = [float(x) for x in shlex.split(arg)]
        except Exception:
            print("Usage: add <num> [<num> ...]")
            return
        print(f"Sum = {sum(nums)}")

    def do_echo(self, arg):
        """Echo back what you type: echo <text>"""
        print(arg)

    def do_exit(self, arg):
        """Exit the shell."""
        print("Goodbye!")
        return True

    # ----- Help tweaks -----
    def do_help(self, arg):
        if arg:
            return super().do_help(arg)
        super().do_help(arg)
        print()
        print("Basics:")
        print("  - Use 'help' or '?' to list commands.")
        print("  - Use 'help <command>' for details.")
        print('  - Quotes are supported in arguments (e.g., add "3.5" 2).')

if __name__ == "__main__":
    MyShell().cmdloop()

 

Welcome to MyShell! Type help or ? to list commands.
(myshell) hello
Hello, stranger!
(myshell) WRONG COMMAND
Unknown command: 'WRONG COMMAND'. Type 'help' to list commands.
(myshell) echo KDnuggets is a great site
KDnuggets is a great site
(myshell) exit
Goodbye!

 

Wrapping Up

 
You just learned how to build an interactive shell using the cmd module without any external dependencies. Isn’t that amazing?

Before we finish, it’s worth pointing out a few common pitfalls to watch out for: First, forgetting to return True in your exit command will prevent the shell loop from ending. Second, relying on simple whitespace splitting for arguments can cause problems when users enter quoted text, so using shlex is recommended. Finally, not handling the emptyline() method properly can lead to unexpected behavior, like repeating the last command when the user presses Enter on an empty line.

I would love to hear your thoughts about this article in the comments section, as well as any ideas you might want to explore further.
 
 

Kanwal Mehreen is a machine learning engineer and a technical writer with a profound passion for data science and the intersection of AI with medicine. She co-authored the ebook “Maximizing Productivity with ChatGPT”. As a Google Generation Scholar 2022 for APAC, she champions diversity and academic excellence. She’s also recognized as a Teradata Diversity in Tech Scholar, Mitacs Globalink Research Scholar, and Harvard WeCode Scholar. Kanwal is an ardent advocate for change, having founded FEMCodes to empower women in STEM fields.