docopt
An innovative library that automatically generates command-line argument parsers from docstrings. Takes a declarative approach.
GitHub Overview
docopt/docopt
Create *beautiful* command-line interfaces with Python
Topics
Star History
Framework
docopt
Overview
docopt is an innovative library that automatically generates command-line interfaces (CLI) from Python script docstrings. It's designed based on the philosophy that "your program's help message is its specification," enabling a declarative approach to defining CLIs. The original version is no longer maintained, and migration to docopt-ng is now recommended.
Details
docopt takes a completely different approach from traditional CLI libraries. It parses the docstring that describes how to use your program and automatically performs argument parsing based on that description. This method prevents divergence between documentation and code, ensuring the usage information is always up-to-date.
Maintenance Status (2024)
- Original docopt: No updates since version 0.6.2 in 2013, with 28 pull requests remaining unprocessed. The project is effectively unmaintained.
- docopt-ng: A fork maintained by the Jazzband project, providing type hints, complete test coverage, and ongoing maintenance. The latest release was in May 2023.
- Future Outlook: Even docopt-ng faces uncertainty regarding long-term maintenance, with discussions about transitioning to more modern CLI frameworks. For new projects, modern frameworks with type hints and input validation are increasingly recommended over docopt-based solutions.
Key Features
- Docstring-driven Development: Define CLI by simply writing a
Usage:
section in your program's docstring - Self-documenting: Help message and parser exist in the same place
- Pattern-based Syntax: Uses conventional command-line help message notation
- Minimal Code: Implement CLI with just a few lines
- Dictionary Output: Parse results returned as an easy-to-use dictionary
- POSIX Compliant: Follows standard command-line conventions
Usage Pattern Syntax
- Positional Arguments: Enclosed in angle brackets like
<file>
,<host>
- Options: Format as
-h
,--help
, or-h, --help
- Commands: Simple words like
add
,ship
- Required Elements: Enclosed in
()
(e.g.,(add|rm)
) - Optional Elements: Enclosed in
[]
(e.g.,[--timeout=<seconds>]
) - Mutually Exclusive: Separated by
|
(e.g.,(-a|-b)
) - Repeating Elements: Append
...
(e.g.,NAME...
)
Pros and Cons
Pros
- Improved maintainability through unified documentation and code
- Intuitive and readable syntax
- Low learning curve
- Concise and elegant code
- Complies with standard command-line conventions
Cons
- May not be suitable for complex CLIs
- No built-in data validation (requires separate implementation)
- Difficult to customize error messages
- Type hints are inherently limited (due to docstring-based operation)
- Original version is completely unmaintained (no updates since 2013)
- docopt-ng also faces long-term maintenance uncertainty
- Can be challenging to debug
- Modern CLI libraries are recommended for new projects
Key Links
- Official Site
- GitHub (Original - Not Recommended)
- GitHub (docopt-ng - Recommended)
- PyPI (docopt-ng)
- Discussion on docopt-ng Future
Code Examples
Basic Usage Example
"""Simple greeting program
Usage:
hello.py <name> [--times=<n>]
hello.py -h | --help
hello.py --version
Options:
-h --help Show this help message
--version Show version
--times=<n> Number of times to greet [default: 1]
"""
from docopt import docopt
def main():
arguments = docopt(__doc__, version='Hello 1.0')
name = arguments['<name>']
times = int(arguments['--times'])
for _ in range(times):
print(f'Hello, {name}!')
if __name__ == '__main__':
main()
Multiple Commands Example
"""File management tool
Usage:
file_manager.py list [<directory>]
file_manager.py copy <source> <destination> [--force]
file_manager.py delete <file>... [--confirm]
file_manager.py (-h | --help)
file_manager.py --version
Commands:
list List directory contents
copy Copy a file
delete Delete files
Options:
-h --help Show this help message
--version Show version
--force Overwrite existing files
--confirm Confirm before deletion
"""
from docopt import docopt
import os
import shutil
def main():
args = docopt(__doc__, version='File Manager 1.0')
if args['list']:
directory = args['<directory>'] or '.'
files = os.listdir(directory)
for f in files:
print(f)
elif args['copy']:
source = args['<source>']
destination = args['<destination>']
if os.path.exists(destination) and not args['--force']:
print(f'Error: {destination} already exists. Use --force to overwrite.')
else:
shutil.copy2(source, destination)
print(f'Copied: {source} -> {destination}')
elif args['delete']:
files = args['<file>']
if args['--confirm']:
response = input(f'Delete {len(files)} files? (y/n): ')
if response.lower() != 'y':
print('Deletion cancelled.')
return
for file in files:
os.remove(file)
print(f'Deleted: {file}')
if __name__ == '__main__':
main()
Example with Pydantic Validation
"""Coordinate calculator
Usage:
calculator.py distance <x1> <y1> <x2> <y2>
calculator.py midpoint <x1> <y1> <x2> <y2>
calculator.py (-h | --help)
calculator.py --version
Options:
-h --help Show this help message
--version Show version
"""
from docopt import docopt
from pydantic import BaseModel, Field, ValidationError
import math
class CoordinateArgs(BaseModel):
x1: float = Field(..., alias='<x1>')
y1: float = Field(..., alias='<y1>')
x2: float = Field(..., alias='<x2>')
y2: float = Field(..., alias='<y2>')
distance: bool
midpoint: bool
def calculate_distance(x1, y1, x2, y2):
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
def calculate_midpoint(x1, y1, x2, y2):
return ((x1 + x2) / 2, (y1 + y2) / 2)
def main():
raw_args = docopt(__doc__, version='Calculator 1.0')
try:
args = CoordinateArgs(**raw_args)
if args.distance:
result = calculate_distance(args.x1, args.y1, args.x2, args.y2)
print(f'Distance: {result:.2f}')
elif args.midpoint:
mx, my = calculate_midpoint(args.x1, args.y1, args.x2, args.y2)
print(f'Midpoint: ({mx:.2f}, {my:.2f})')
except ValidationError as e:
print('Error: Invalid arguments.')
print(e)
exit(1)
if __name__ == '__main__':
main()
Advanced Pattern Example
"""Git-style tool with subcommands
Usage:
mytool init [<name>] [--bare]
mytool clone <url> [<directory>]
mytool commit -m <message> [--amend]
mytool push [<remote>] [<branch>]
mytool pull [<remote>] [<branch>] [--rebase]
mytool status [--short]
mytool log [--oneline] [--graph] [-n <number>]
mytool (-h | --help)
mytool --version
Options:
-h --help Show this help message
--version Show version
--bare Create a bare repository
-m <message> Commit message
--amend Amend the previous commit
--rebase Use rebase when pulling
--short Show short format
--oneline Show each commit on one line
--graph Show graph display
-n <number> Number of commits to show [default: 10]
"""
from docopt import docopt
def main():
args = docopt(__doc__, version='MyTool 1.0')
# Command detection and processing
if args['init']:
name = args['<name>'] or 'myproject'
bare = args['--bare']
print(f'Initialized project "{name}"')
if bare:
print('(bare repository)')
elif args['clone']:
url = args['<url>']
directory = args['<directory>'] or url.split('/')[-1]
print(f'Cloning: {url} -> {directory}')
elif args['commit']:
message = args['<message>']
amend = args['--amend']
if amend:
print(f'Amending previous commit: "{message}"')
else:
print(f'Committing: "{message}"')
elif args['status']:
if args['--short']:
print('M file1.txt\nA file2.txt')
else:
print('Modified files:\n file1.txt\nNew files:\n file2.txt')
if __name__ == '__main__':
main()