Skip to main content
โšก Calmops

Building CLI Tools with Python

Introduction

Command-line tools are essential for developer productivity. Python provides excellent libraries for building CLI applications. This guide covers creating professional CLI tools with Python.

Using Click

Basic Structure

import click

@click.command()
@click.option('--name', default='World', help='Name to greet')
@click.option('--count', default=1, help='Number of times to greet')
def hello(name, count):
    """Simple program that greets NAME for a total of COUNT times."""
    for _ in range(count):
        click.echo(f'Hello, {name}!')

if __name__ == '__main__':
    hello()

Groups and Commands

@click.group()
def cli():
    """Main CLI group."""
    pass

@cli.command()
@click.argument('file', type=click.Path(exists=True))
def process(file):
    """Process a file."""
    click.echo(f'Processing {file}...')

@cli.command()
@click.argument('name')
def delete(name):
    """Delete a resource."""
    if click.confirm(f'Do you want to delete {name}?'):
        click.echo(f'Deleted {name}')

if __name__ == '__main__':
    cli()

Options and Arguments

# Short and long options
@click.option('--name', '-n', help='Your name')

# Multiple values
@click.option('--files', multiple=True, help='Files to process')

# Choice
@click.option('--format', type=click.Choice(['json', 'yaml', 'csv']))

# Boolean flags
@click.option('--verbose/--quiet', default=False)

# Required option
@click.option('--config', required=True, type=click.Path())

Using argparse

Basic Parser

import argparse

parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
                    help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
                    const=sum, default=max,
                    help='sum the integers (default: find the max)')
parser.add_argument('--verbose', action='store_true',
                    help='increase output verbosity')

args = parser.parse_args()
print(args.accumulate(args.integers))

Subcommands

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='available commands')

# Add command
add_parser = subparsers.add_parser('add', help='Add numbers')
add_parser.add_argument('x', type=int, help='First number')
add_parser.add_argument('y', type=int, help='Second number')

# Subtract command
sub_parser = subparsers.add_parser('sub', help='Subtract numbers')

Rich Terminal Output

Using Rich Library

from rich.console import Console
from rich.progress import track
from rich.table import Table

console = Console()

# Styled output
console.print("[bold red]Warning![/bold red] Something went wrong")
console.print("[green]Success![/green] Operation complete")

# Tables
table = Table(title="Star Wars Movies")
table.add_column("Episode", style="cyan")
table.add_column("Title", style="magenta")
table.add_column("Year", justify="right")

table.add_row("IV", "A New Hope", "1977")
table.add_row("V", "The Empire Strikes Back", "1980")

console.print(table)

# Progress bars
for i in track(range(10), description="Processing..."):
    # Do work
    pass

Input Handling

Interactive Prompts

# Using Click
name = click.prompt('Enter your name', default='Anonymous')
password = click.prompt('Password', hide_input=True)

# Using Rich
from rich.prompt import Prompt, Confirm, Password

name = Prompt.ask("Your name?", default="World")
if Confirm.ask("Continue?"):
    pass

Password Input

import getpass

password = getpass.getpass()

Configuration Files

YAML Configuration

import yaml

def load_config(config_file):
    with open(config_file, 'r') as f:
        return yaml.safe_load(f)

Environment Variables

import os

api_key = os.environ.get('API_KEY', 'default_key')

Python Config Files

# config.py
import configparser

config = configparser.ConfigParser()
config.read('config.ini')

api_key = config['api']['key']

Building and Packaging

Project Structure

my_cli/
โ”œโ”€โ”€ my_cli/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ””โ”€โ”€ cli.py
โ”œโ”€โ”€ setup.py
โ”œโ”€โ”€ pyproject.toml
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ LICENSE

setup.py

from setuptools import setup, find_packages

setup(
    name='my-cli',
    version='1.0.0',
    packages=find_packages(),
    include_package_data=True,
    install_requires=[
        'click>=8.0',
        'rich>=10.0',
    ],
    entry_points={
        'console_scripts': [
            'mycli=my_cli.cli:cli',
        ],
    },
    python_requires='>=3.8',
)

pyproject.toml

[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my-cli"
version = "1.0.0"
dependencies = ["click", "rich"]

[project.scripts]
mycli = "my_cli.cli:cli"

Error Handling

Graceful Errors

import click
from click import ClickException

@click.command()
def risky_command():
    try:
        # Perform operation
        pass
    except ValueError as e:
        raise click.ClickException(f"Invalid value: {e}")
    except Exception as e:
        raise click.ClickException(f"Unexpected error: {e}")

Debug Mode

import click

@click.command()
@click.option('--debug', is_flag=True, help='Enable debug mode')
def command(debug):
    if debug:
        # Set debug logging
        pass
    # Run command

Testing CLI Applications

Using Click’s Test Runner

from click.testing import CliRunner

def test_hello():
    runner = CliRunner()
    result = runner.invoke(cli, ['--name', 'Test'])
    assert result.exit_code == 0
    assert 'Hello, Test!' in result.output

def test_missing_option():
    runner = CliRunner()
    result = runner.invoke(cli, [])
    assert result.exit_code == 2

Advanced Patterns

Progress Feedback

import click

@click.command()
def download():
    with click.progressbar(length=100, label='Downloading') as bar:
        for i in range(10):
            # Simulate download
            bar.update(10)

Spinner

with click_spinner.spinner():
    # Long running operation
    pass

Conclusion

Python makes building CLI tools enjoyable. Start with Click for simple commands, add Rich for beautiful output, and package properly for distribution. Good CLI tools improve developer productivity.


Resources

Comments