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 argument
  • argv[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 arg3

Output:

['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 arg3

Output:

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 --force

Here, --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 help

Example 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 version

Example 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.png

Where 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 0success
  • Code 1 or 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 json

Normal 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

  1. 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.

  2. 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.

  3. 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 colorama in Python or chalk in Node.js).

  4. 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.

  5. Write good documentation. In addition to the built-in help, consider maintaining a README or a documentation page with detailed examples.

  6. 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.