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 packagegrep -r "pattern" .docker run -p 8080:80 nginxpython 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 exitPositional 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.0Argument 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 integerDefault 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: 3Advanced 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.txtMutually 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 --jsonSubcommands
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 linesExample 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 recordsBest 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
--verbosefor debugging output - Support
--versionflag - 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