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.