Command-Line Interfaces (CLI)

Build powerful command-line tools and scripts using argparse and click to handle arguments, options, and user interaction professionally.

Why Learn CLI Development?

Command-line interfaces are essential for automation, system administration, data processing, and building developer tools. Professional CLI tools provide clear help text, handle errors gracefully, and follow Unix conventions.

What Are Command-Line Interfaces?

A Command-Line Interface (CLI) allows users to interact with programs through text commands in a terminal or shell. CLIs are powerful, scriptable, and efficient for automation tasks.

CLI Advantages
  • Fast and efficient for repetitive tasks
  • Easy to automate with scripts
  • Works over SSH and remote connections
  • Lower resource usage than GUIs
  • Perfect for server environments
Common CLI Examples
  • git commit -m "message"
  • pip install package
  • grep -r "pattern" .
  • docker run -p 8080:80 nginx
  • python script.py --input file.txt

Basic Command-Line Arguments (sys.argv)

The simplest way to access command-line arguments is through sys.argv, a list containing all arguments passed to the script.

# simple_cli.py
import sys

# sys.argv[0] is the script name
# sys.argv[1:] are the arguments

print(f"Script name: {sys.argv[0]}")
print(f"Arguments: {sys.argv[1:]}")

if len(sys.argv) > 1:
    name = sys.argv[1]
    print(f"Hello, {name}!")
else:
    print("Hello, World!")

Running the script:

$ python simple_cli.py Alice
Script name: simple_cli.py
Arguments: ['Alice']
Hello, Alice!

Limitations of sys.argv: No type checking, no help text, manual parsing, no validation. For professional CLIs, use argparse or click instead.

Introduction to argparse

The argparse module is Python's standard library for building professional command-line interfaces. It handles argument parsing, generates help text, and validates input.

Basic argparse Usage

# greet.py
import argparse

# Create parser
parser = argparse.ArgumentParser(
    description='A simple greeting program'
)

# Add argument
parser.add_argument('name', help='Name of person to greet')

# Parse arguments
args = parser.parse_args()

# Use arguments
print(f"Hello, {args.name}!")

Running the script:

$ python greet.py Alice
Hello, Alice!

Auto-generated help:

$ python greet.py --help
usage: greet.py [-h] name

A simple greeting program

positional arguments:
  name        Name of person to greet

optional arguments:
  -h, --help  show this help message and exit

Positional vs Optional Arguments

Positional arguments are required and must be provided in order.
Optional arguments start with - or -- and can appear in any order.

# calculator.py
import argparse

parser = argparse.ArgumentParser(description='Simple calculator')

# Positional arguments (required)
parser.add_argument('num1', type=int, help='First number')
parser.add_argument('num2', type=int, help='Second number')

# Optional arguments (flags)
parser.add_argument('-o', '--operation',
                   default='add',
                   choices=['add', 'sub', 'mul', 'div'],
                   help='Operation to perform (default: add)')

parser.add_argument('-v', '--verbose',
                   action='store_true',
                   help='Print verbose output')

args = parser.parse_args()
# Perform calculation
result = 0
if args.operation == 'add':
    result = args.num1 + args.num2
elif args.operation == 'sub':
    result = args.num1 - args.num2
elif args.operation == 'mul':
    result = args.num1 * args.num2
elif args.operation == 'div':
    result = args.num1 / args.num2

if args.verbose:
    print(f"Calculating: {args.num1} {args.operation} {args.num2}")
    print(f"Result: {result}")
else:
    print(result)

Examples:

$ python calculator.py 10 5
15

$ python calculator.py 10 5 --operation mul
50

$ python calculator.py 10 5 -o div -v
Calculating: 10 div 5
Result: 2.0

Argument Types and Validation

Use the type parameter to automatically convert and validate arguments.

import argparse

parser = argparse.ArgumentParser()

# Integer argument
parser.add_argument('--age', type=int, help='Your age')

# Float argument
parser.add_argument('--price', type=float, help='Price in dollars')

# File argument (must exist)
parser.add_argument('--input',
                   type=argparse.FileType('r'),
                   help='Input file')

# Custom validation function
def positive_int(value):
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError(f"{value} is not a positive integer")
    return ivalue

parser.add_argument('--count',
                   type=positive_int,
                   help='A positive integer')

args = parser.parse_args()

Error handling:

$ python script.py --age twenty
error: argument --age: invalid int value: 'twenty'

$ python script.py --count -5
error: argument --count: -5 is not a positive integer

Default Values and Choices

parser = argparse.ArgumentParser()

# Default value
parser.add_argument('--output',
                   default='output.txt',
                   help='Output filename (default: output.txt)')

# Limited choices
parser.add_argument('--format',
                   choices=['json', 'xml', 'csv'],
                   default='json',
                   help='Output format')

# Boolean flag (store_true means default is False)
parser.add_argument('--debug',
                   action='store_true',
                   help='Enable debug mode')

# Count occurrences (for -vvv style verbosity)
parser.add_argument('-v', '--verbose',
                   action='count',
                   default=0,
                   help='Increase verbosity (can be repeated)')

args = parser.parse_args()

print(f"Output: {args.output}")
print(f"Format: {args.format}")
print(f"Debug: {args.debug}")
print(f"Verbosity level: {args.verbose}")

Examples:

$ python script.py
Output: output.txt
Format: json
Debug: False
Verbosity level: 0

$ python script.py --format csv --debug -vvv
Output: output.txt
Format: csv
Debug: True
Verbosity level: 3

Advanced argparse Features

Multiple Values (nargs)

Use nargs to accept multiple values for a single argument.

parser = argparse.ArgumentParser()

# Exactly 2 values
parser.add_argument('--point', nargs=2, type=float,
                   help='X and Y coordinates')

# One or more values
parser.add_argument('--files', nargs='+',
                   help='One or more files to process')

# Zero or more values
parser.add_argument('--tags', nargs='*',
                   help='Optional tags')

# Optional single value
parser.add_argument('--config', nargs='?',
                   const='default.conf',
                   help='Config file')

args = parser.parse_args()

if args.point:
    x, y = args.point
    print(f"Point: ({x}, {y})")

if args.files:
    print(f"Processing {len(args.files)} files")
    for file in args.files:
        print(f"  - {file}")

Examples:

$ python script.py --point 10.5 20.3 --files a.txt b.txt c.txt
Point: (10.5, 20.3)
Processing 3 files
  - a.txt
  - b.txt
  - c.txt

Mutually Exclusive Groups

Use mutually exclusive groups when only one of several options should be allowed.

parser = argparse.ArgumentParser()

# Create mutually exclusive group
group = parser.add_mutually_exclusive_group()
group.add_argument('--json', action='store_true',
                  help='Output as JSON')
group.add_argument('--xml', action='store_true',
                  help='Output as XML')
group.add_argument('--csv', action='store_true',
                  help='Output as CSV')

args = parser.parse_args()

if args.json:
    print("Using JSON format")
elif args.xml:
    print("Using XML format")
elif args.csv:
    print("Using CSV format")
else:
    print("Using default format")

Error when using multiple exclusive options:

$ python script.py --json --xml
error: argument --xml: not allowed with argument --json

Subcommands

Subcommands allow you to create CLI tools with multiple commands, like git commit,git push, etc.

# tool.py - A CLI with subcommands
import argparse

parser = argparse.ArgumentParser(prog='tool')
subparsers = parser.add_subparsers(dest='command', help='Available commands')

# 'create' subcommand
create_parser = subparsers.add_parser('create', help='Create a new item')
create_parser.add_argument('name', help='Name of item')
create_parser.add_argument('--type', default='standard', help='Type of item')

# 'delete' subcommand
delete_parser = subparsers.add_parser('delete', help='Delete an item')
delete_parser.add_argument('name', help='Name of item to delete')
delete_parser.add_argument('--force', action='store_true',
                          help='Force deletion')

# 'list' subcommand
list_parser = subparsers.add_parser('list', help='List all items')
list_parser.add_argument('--verbose', action='store_true',
                        help='Show detailed information')

args = parser.parse_args()
# Handle subcommands
if args.command == 'create':
    print(f"Creating {args.type} item: {args.name}")
elif args.command == 'delete':
    if args.force:
        print(f"Force deleting: {args.name}")
    else:
        print(f"Deleting: {args.name}")
elif args.command == 'list':
    if args.verbose:
        print("Listing items (verbose mode)")
    else:
        print("Listing items")
else:
    parser.print_help()

Using subcommands:

$ python tool.py create myitem --type premium
Creating premium item: myitem

$ python tool.py delete myitem --force
Force deleting: myitem

$ python tool.py list --verbose
Listing items (verbose mode)

Introduction to click

click is a third-party library that makes creating CLIs even easier with decorators and a more intuitive API. It's especially popular for complex CLI applications.

Install click: pip install click

Basic click Usage

# hello.py
import click

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

if __name__ == '__main__':
    hello()

Running the script:

$ python hello.py Alice
Hello, Alice!

$ python hello.py Alice --greeting "Hi" --count 3
Hi, Alice!
Hi, Alice!
Hi, Alice!

click Options and Types

import click

@click.command()
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
@click.option('--name', prompt='Your name', help='Your name')
@click.option('--age', type=int, help='Your age')
@click.option('--height', type=float, help='Height in meters')
@click.option('--color',
             type=click.Choice(['red', 'green', 'blue']),
             help='Favorite color')
@click.option('--input-file',
             type=click.File('r'),
             help='Input file')
def process(verbose, name, age, height, color, input_file):
    """Process user information."""
    if verbose:
        click.echo("Running in verbose mode")

    click.echo(f"Name: {name}")
    if age:
        click.echo(f"Age: {age}")
    if height:
        click.echo(f"Height: {height}m")
    if color:
        click.echo(f"Favorite color: {color}")

    if input_file:
        content = input_file.read()
        click.echo(f"File content: {content}")

if __name__ == '__main__':
    process()

click Command Groups

Create multi-command CLIs (like git, docker) with command groups.

# manager.py
import click

@click.group()
def cli():
    """Project management tool."""
    pass

@cli.command()
@click.argument('name')
@click.option('--type', default='basic', help='Project type')
def create(name, type):
    """Create a new project."""
    click.echo(f"Creating {type} project: {name}")

@cli.command()
@click.argument('name')
@click.option('--force', is_flag=True, help='Force deletion')
def delete(name, force):
    """Delete a project."""
    if force:
        click.echo(f"Force deleting project: {name}")
    else:
        if click.confirm(f'Delete project {name}?'):
            click.echo(f"Deleting project: {name}")

@cli.command()
@click.option('--detailed', is_flag=True, help='Show details')
def list(detailed):
    """List all projects."""
    if detailed:
        click.echo("Detailed project list:")
    else:
        click.echo("Project list:")

if __name__ == '__main__':
    cli()

Using command groups:

$ python manager.py create myproject --type advanced
Creating advanced project: myproject

$ python manager.py delete myproject
Delete project myproject? [y/N]: y
Deleting project: myproject

$ python manager.py list --detailed
Detailed project list:

Building Real CLI Tools

Example 1: File Processing Tool

# fileproc.py - Process text files
import argparse
import sys

def count_words(filename):
    """Count words in a file."""
    try:
        with open(filename, 'r') as f:
            content = f.read()
            words = content.split()
            return len(words)
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

def count_lines(filename):
    """Count lines in a file."""
    with open(filename, 'r') as f:
        return sum(1 for _ in f)

def main():
    parser = argparse.ArgumentParser(
        description='File processing utility',
        epilog='Example: fileproc.py --words myfile.txt'
    )

    parser.add_argument('files', nargs='+', help='Files to process')
    parser.add_argument('-w', '--words', action='store_true',
                       help='Count words')
    parser.add_argument('-l', '--lines', action='store_true',
                       help='Count lines')
    parser.add_argument('-v', '--verbose', action='store_true',
                       help='Show detailed output')

    args = parser.parse_args()

    # Default to word count if no option specified
    if not args.words and not args.lines:
        args.words = True

    for filename in args.files:
        if args.verbose:
            print(f"\nProcessing: {filename}")

        if args.words:
            word_count = count_words(filename)
            print(f"{word_count} words")

        if args.lines:
            line_count = count_lines(filename)
            print(f"{line_count} lines")

if __name__ == '__main__':
    main()

Usage examples:

$ python fileproc.py document.txt
1523 words

$ python fileproc.py --lines document.txt
87 lines

$ python fileproc.py -wl -v document.txt
Processing: document.txt
1523 words
87 lines

Example 2: Data Conversion Tool with click

# convert.py - Convert between data formats
import click
import json
import csv
import sys

@click.command()
@click.argument('input_file', type=click.File('r'))
@click.option('--output', '-o', type=click.File('w'), default='-',
             help='Output file (default: stdout)')
@click.option('--from-format', type=click.Choice(['json', 'csv']),
             required=True, help='Input format')
@click.option('--to-format', type=click.Choice(['json', 'csv']),
             required=True, help='Output format')
def convert(input_file, output, from_format, to_format):
    """Convert data between JSON and CSV formats."""

    try:
        # Read input
        if from_format == 'json':
            data = json.load(input_file)
            if not isinstance(data, list):
                raise ValueError("JSON must be a list of objects")
        else:  # CSV
            reader = csv.DictReader(input_file)
            data = list(reader)

        # Write output
        if to_format == 'json':
            json.dump(data, output, indent=2)
            output.write('\n')
        else:  # CSV
            if not data:
                click.echo("Warning: No data to convert", err=True)
                return

            writer = csv.DictWriter(output, fieldnames=data[0].keys())
            writer.writeheader()
            writer.writerows(data)

        click.echo(f"✓ Converted {len(data)} records", err=True)

    except json.JSONDecodeError as e:
        click.echo(f"Error: Invalid JSON - {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)

if __name__ == '__main__':
    convert()

Usage examples:

$ python convert.py data.json --from-format json --to-format csv -o output.csv
✓ Converted 100 records

$ python convert.py data.csv --from-format csv --to-format json
[
  {name": "Alice", "age": "30"},
  {name": "Bob", "age": "25"}
]
✓ Converted 2 records

Best Practices

DO
  • Provide clear, helpful help text
  • Validate inputs and provide meaningful error messages
  • Follow Unix conventions (e.g., exit code 0 for success)
  • Use --verbose for debugging output
  • Support --version flag
  • Write to stderr for errors, stdout for output
DON'T
  • Show stack traces to end users
  • Require interactive input in scripts
  • Use unclear or cryptic argument names
  • Exit without proper error messages
  • Ignore the user's locale/encoding
  • Print sensitive information (passwords, keys)

Comprehensive Help Text

parser = argparse.ArgumentParser(
    prog='mytool',
    description='A comprehensive tool for data processing',
    epilog='For more information, visit: https://example.com/docs',
    formatter_class=argparse.RawDescriptionHelpFormatter
)

parser.add_argument('--version', action='version', version='%(prog)s 1.0.0')

parser.add_argument(
    'input',
    help='Input file path (supports .txt, .csv, .json)'
)

parser.add_argument(
    '--output', '-o',
    metavar='FILE',
    help='Output file path (default: stdout)'
)

# Good help text is descriptive and includes examples
parser.add_argument(
    '--format',
    choices=['json', 'csv', 'xml'],
    help='Output format: json (default), csv, or xml'
)

Proper Error Handling

import sys
import click

@click.command()
@click.argument('filename')
def process_file(filename):
    """Process a file with proper error handling."""

    try:
        with open(filename, 'r') as f:
            content = f.read()
            # Process content...
            click.echo("✓ File processed successfully")

    except FileNotFoundError:
        click.echo(f"Error: File '{filename}' not found", err=True)
        sys.exit(1)

    except PermissionError:
        click.echo(f"Error: Permission denied reading '{filename}'", err=True)
        sys.exit(1)

    except Exception as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)

User Feedback

import click

@click.command()
@click.argument('files', nargs=-1)
def process(files):
    """Process multiple files with progress feedback."""

    if not files:
        click.echo("No files specified", err=True)
        return

    # Progress bar for long operations
    with click.progressbar(files, label='Processing files') as bar:
        for filename in bar:
            # Process file...
            pass

    # Styled output
    click.secho("✓ All files processed!", fg='green', bold=True)

    # Confirmation prompts
    if click.confirm('Delete temporary files?'):
        click.echo("Deleting temporary files...")

    # Color-coded messages
    click.secho("Warning: Using default settings", fg='yellow')
    click.secho("Error: Connection failed", fg='red', err=True)

Key Takeaways

  • sys.argv is simple but limited - use argparse or click for professional CLIs
  • argparse is Python's standard library for building CLIs with type checking and validation
  • Positional arguments are required, optional arguments start with - or --
  • Use type parameter for automatic conversion and validation
  • Subcommands enable multi-command tools like git or docker
  • click offers a decorator-based API that's more intuitive for complex CLIs
  • Always provide clear help text and meaningful error messages
  • Follow Unix conventions: exit code 0 for success, non-zero for errors
  • Write errors to stderr, output to stdout
  • Add progress indicators and color for better user experience
What's Next?

You've mastered CLI development! Now let's learn how to manage application configuration across different environments and deployment scenarios.

  • Configuration Management - Handle config files, environment variables, and secrets
  • Environment-Specific Settings - Manage dev, staging, and production configurations
  • Best Practices - Learn the 12-factor app methodology and secure config handling