Complete Guide to Building a CLI
In this article, I’ll cover a complete guide on how to build a professional CLI (Command Line Interface) that is easy to use and, most importantly, easy to integrate with other applications. If you’ve never built a CLI before, don’t worry — we’ll start from scratch.
What is a CLI?
CLI stands for Command Line Interface. It’s the type
of program you use by typing commands in the terminal, such as
git, npm, docker,
pip, among others. Unlike programs with a graphical
interface (GUI), CLIs are controlled by text, making them extremely
powerful, automatable, and easy to integrate with other systems.
Understanding ARGV
First and foremost, you need to understand what argv is.
The answer is simple: argv is an array of strings containing the
arguments passed to the program. Everything typed in the
terminal is split by spaces and transformed into this array.
argv[0]→ program name (or runtime + script)argv[1]→ first argumentargv[2]→ second argument- And so on…
How argv works in different languages
In interpreted languages (like Python),
argv[0] is the name of your script, and the arguments start
from argv[1].
In compiled languages (like C, C++, Go, Rust),
argv[0] is the name of the compiled executable.
Python Example
import sys
print(sys.argv)Run:
python3 script.py arg1 arg2 arg3Output:
['script.py', 'arg1', 'arg2', 'arg3']
C Example
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}Compile and run:
gcc program.c -o program
./program arg1 arg2 arg3Output:
argv[0] = ./program
argv[1] = arg1
argv[2] = arg2
argv[3] = arg3
Building Your CLI
To build a solid and professional CLI, follow the steps below:
1. Define the Actions
The first argument (after the program name) should be the action, that is, the action the user wants to execute.
my_program <action> [flags and arguments]Let’s use git as an example. It has several actions like
commit, pull, push,
rebase, etc. Take a look at the command below:
git commit -m "commit message"In this example: - commit → is the
action - -m → is a flag -
"commit message" → is the flag’s value
Tip for beginners: Think of the action as a function, and the flags as the parameters of that function.
How to implement actions
The simplest way to implement actions is to use a conditional structure to check the first argument:
Python Example:
import sys
def action_hello():
print("Hello! Welcome to my CLI!")
def action_version():
print("my_program v1.0.0")
if len(sys.argv) < 2:
print("Usage: my_program <action>")
print("Use 'my_program help' to see available actions.")
sys.exit(1)
action = sys.argv[1]
if action == "hello":
action_hello()
elif action == "version":
action_version()
else:
print(f"Unknown action: {action}")
print("Use 'my_program help' to see available actions.")2. Define the Flags
Now that you’ve separated each possible action of your CLI, you need to define the flags that each action can receive. Flags modify the behavior of an action.
There are two types of flags:
Boolean Flags (flags without a value)
These are flags that don’t receive a value — they simply indicate
that something should be activated. They usually start with
--.
git push --forceHere, --force is a boolean flag indicating that the push
should be forced.
Value Flags (flags with a value)
These are flags that receive a value right after them. They can use
- (short form) or -- (long form).
git commit -m "commit message"
git commit --message "commit message"Here, -m (or --message) is a flag that
receives the message as a value.
How to implement flags
Python Example:
import sys
def parse_flags(args):
"""Parses the arguments and returns a dictionary of flags."""
flags = {}
i = 0
while i < len(args):
if args[i].startswith("--"):
flag_name = args[i][2:] # Remove the -- prefix
# Check if it's a boolean flag or a value flag
if i + 1 < len(args) and not args[i + 1].startswith("-"):
flags[flag_name] = args[i + 1]
i += 2
else:
flags[flag_name] = True
i += 1
elif args[i].startswith("-"):
flag_name = args[i][1:] # Remove the - prefix
if i + 1 < len(args) and not args[i + 1].startswith("-"):
flags[flag_name] = args[i + 1]
i += 2
else:
flags[flag_name] = True
i += 1
else:
i += 1
return flags
# Usage example:
# my_program commit -m "message" --force
action = sys.argv[1]
flags = parse_flags(sys.argv[2:])
print(f"Action: {action}")
print(f"Flags: {flags}")3. Define a Help
Every command-line program needs a help
action. It should list all available actions and flags, with a clear
description of each one.
my_program helpExample output:
my_program - An amazing tool for managing tasks
Usage: my_program <action> [flags]
Available actions:
help Shows this help message
version Shows the program version
add Adds a new task
--title <text> Task title (required)
--priority <n> Priority from 1 to 5 (default: 3)
list Lists all tasks
--filter <status> Filter by status (pending, completed)
--limit <n> Limits the number of results
done Marks a task as completed
--id <id> Task ID (required)
Examples:
my_program add --title "Buy coffee" --priority 1
my_program list --filter pending
my_program done --id 42
Tip: Always include practical examples in the help. This is very helpful for beginners.
4. Define a Version
Create an action called version that shows the current
version of your CLI. This is essential for debugging and so that users
know which version they are using.
my_program versionExample output:
my_program v1.0.0
Tip: Follow the Semantic Versioning (SemVer) standard for numbering your versions:
MAJOR.MINOR.PATCH(e.g.,1.2.3).
5. Define Stored Configs
It is often convenient for your CLI to store user configurations so
they don’t have to type them every time. This is very common — think of
git config or npm config.
Example:
my_program config-user --username "Mateus" --email "mateus@gmail.com" --password "password"Once configured, in subsequent actions you can require only the identifying flag:
my_program set-profile-picture --username "Mateus" --picture photo.pngWhere to store configurations?
It is strongly recommended that you save all data in
a single location, preferably in a hidden folder in the user’s
home directory (directories starting with . are
hidden on Linux/macOS).
Example: configurations could be saved in the file
~/.my_program/config.json:
{
"username": "Mateus",
"email": "mateus@gmail.com",
"password": "test"
}Common storage formats: - JSON → simple and widely supported - SQLite → ideal for more complex or relational data - TOML/YAML → good for human-readable configuration files - Text files → for very simple data
6. Error Handling
A professional CLI should handle errors clearly and in a user-friendly way. Never let the program “crash” with an incomprehensible error.
Best practices:
import sys
if len(sys.argv) < 2:
print("Error: no action provided.")
print("Usage: my_program <action> [flags]")
print("Use 'my_program help' for more information.")
sys.exit(1)Tips for good errors: - Say what went wrong - Say why it went wrong (if possible) - Suggest how to fix it
Example:
Error: the --title flag is required for the 'add' action.
Usage: my_program add --title "My task"
7. Exit Codes
Command-line programs communicate success or failure through exit codes. This is essential so that other programs and scripts can determine whether your CLI executed correctly.
- Code
0→ success - Code
1or higher → error
import sys
# Success
sys.exit(0)
# Generic error
sys.exit(1)This allows other programs to use your CLI like this:
my_program add --title "Task" && echo "Success!" || echo "Failed!"8. Structured Output (Output for Other Applications)
If your CLI can be used by other programs (and it should!), consider offering output in structured formats like JSON.
my_program list --output jsonNormal output (for humans):
ID | Title | Status
1 | Buy coffee | pending
2 | Study CLI | completed
JSON output (for programs):
[
{"id": 1, "title": "Buy coffee", "status": "pending"},
{"id": 2, "title": "Study CLI", "status": "completed"}
]This makes your CLI easily integrable with other systems, scripts, and pipelines.
Extra Tips
Never mix naming conventions. If an action is called
set-profile-picture(separated by-), use the same pattern in all actions. This improves readability and makes usage more intuitive.If the information is sensitive, encrypt it. Never store passwords in plain text. Use some form of encryption and require the decryption password as an argument or stored configuration.
Add colors to the output. Colors make the output much more readable. For example, errors in red, success in green, warnings in yellow. Most languages have libraries for this (such as
coloramain Python orchalkin Node.js).Support auto-complete. If possible, allow the user to set up auto-complete in their shell (Bash, Zsh, Fish). This drastically improves the user experience.
Write good documentation. In addition to the built-in
help, consider maintaining a README or a documentation page with detailed examples.Test your CLI. Write automated tests for each action and for combinations of flags. This prevents regressions and ensures everything works as expected.
Conclusion
Building a professional CLI goes beyond just reading arguments from the terminal. It involves thinking about the user experience, the consistency of commands, error handling, and integrability with other systems. By following the steps in this guide, you’ll have a solid foundation for creating CLIs that are pleasant to use and easy to maintain.
Remember: a good CLI is one that the user can use without having to constantly refer to the documentation. Invest in good help messages, intuitive action names, and clear error messages.
