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