Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CLI Tools

Building Command-Line Applications

Argument Parsing

Python (argparse)

import argparse

parser = argparse.ArgumentParser(description='Process log files')
parser.add_argument('input', help='Input log file')
parser.add_argument('-o', '--output', default='report.txt',
                    help='Output file (default: report.txt)')
parser.add_argument('-v', '--verbose', action='store_true',
                    help='Verbose output')
parser.add_argument('--format', choices=['text', 'json', 'csv'],
                    default='text', help='Output format')
parser.add_argument('-n', '--lines', type=int, default=100,
                    help='Number of lines to process')

args = parser.parse_args()

if args.verbose:
    print(f"Processing {args.input}...")

Go (cobra)

package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "logproc",
    Short: "Process log files",
    Long:  "A CLI tool for analyzing and processing log files",
    Run: func(cmd *cobra.Command, args []string) {
        verbose, _ := cmd.Flags().GetBool("verbose")
        format, _ := cmd.Flags().GetString("format")
        fmt.Printf("Processing with verbose=%v, format=%s\n", verbose, format)
    },
}

func init() {
    rootCmd.Flags().BoolP("verbose", "v", false, "Verbose output")
    rootCmd.Flags().StringP("format", "f", "text", "Output format (text|json|csv)")
    rootCmd.Flags().IntP("lines", "n", 100, "Number of lines to process")
}

Interactive Prompts

# Using click for interactive CLI
import click

@click.command()
@click.option('--name', prompt='Your name', help='Name to greet')
@click.option('--count', default=1, type=int, help='Number of greetings')
@click.confirmation_option(prompt='Are you sure?')
def greet(name, count):
    for _ in range(count):
        click.echo(f'Hello, {name}!')

# Select from options
@click.command()
@click.option('--format', type=click.Choice(['json', 'csv', 'text']))
def convert(format):
    click.echo(f'Converting to {format}')

Output Formatting

# Table output
from tabulate import tabulate

data = [
    ["alice", 100, "active"],
    ["bob", 200, "inactive"],
    ["charlie", 150, "active"],
]

headers = ["Name", "Requests", "Status"]
print(tabulate(data, headers=headers, tablefmt="grid"))

# +---------+-----------+----------+
# | Name    | Requests  | Status   |
# +=========+===========+==========+
# | alice   | 100       | active   |
# +---------+-----------+----------+

# JSON output
import json
print(json.dumps(data, indent=2))

# Progress bars
from tqdm import tqdm
for item in tqdm(range(100), desc="Processing"):
    process(item)

Color Output

# Using click
click.secho("Success!", fg="green")
click.secho("Warning!", fg="yellow")
click.secho("Error!", fg="red", bold=True)

# Using rich
from rich.console import Console
console = Console()
console.print("[bold green]Success![/bold green]")
console.print("[red]Error:[/red] File not found")

Subcommands

# Using click groups
@click.group()
def cli():
    """Log processing tool."""
    pass

@cli.command()
@click.argument('file')
def analyze(file):
    """Analyze a log file."""
    click.echo(f'Analyzing {file}')

@cli.command()
@click.argument('file')
@click.option('--pattern', required=True)
def search(file, pattern):
    """Search log file for pattern."""
    click.echo(f'Searching {file} for {pattern}')

@cli.command()
@click.argument('files', nargs=-1)
def merge(files):
    """Merge multiple log files."""
    click.echo(f'Merging {len(files)} files')

cli.add_command(analyze)
cli.add_command(search)
cli.add_command(merge)

Error Handling

import sys
import click

def main():
    try:
        result = process_file(args.input)
    except FileNotFoundError:
        click.secho(f"Error: File '{args.input}' not found", fg="red")
        sys.exit(1)
    except PermissionError:
        click.secho(f"Error: Permission denied", fg="red")
        sys.exit(1)
    except Exception as e:
        click.secho(f"Error: {e}", fg="red")
        sys.exit(1)

Configuration Files

import toml
import os

def load_config():
    config_paths = [
        './config.toml',
        '~/.myapp/config.toml',
        '/etc/myapp/config.toml',
    ]
    
    for path in config_paths:
        expanded = os.path.expanduser(path)
        if os.path.exists(expanded):
            return toml.load(expanded)
    
    return {}  # defaults

Interview Questions

Q: How would you design a CLI tool that handles both interactive and piped input? A: Check sys.stdin.isatty() — if False, read from stdin (piped). If True, use interactive prompts. Use argparse for flags, support --no-interactive for scripts. Handle SIGPIPE gracefully.

Q: How do you test CLI tools? A: (1) Unit test the logic functions separately, (2) use subprocess to test the CLI interface, (3) use click.testing.CliRunner for click-based CLIs, (4) test exit codes, (5) test error messages, (6) test piped input.

References