Building Reusable CLI Tools
Overview
Do not write overly complex, hardcoded single-purpose scripts if you will need to reuse them. When designing scripts that must be highly configurable, use argparse or sys.argv to create simple CLI utilities. Do not use heavy external dependencies like click unless explicitly requested. Always prefer a simple 1-line script if it solves the immediate problem.
When to Use
- Writing scripts to analyze data (JSON, CSV, logs).
- Performing multi-step file or data manipulation tasks.
- When tempted to create multiple incremental scripts (e.g.,
step1.py, step2.py).
- When a script requires configurable parameters (inputs, thresholds, categories).
1. Option vs. Argument Guidelines
Design your CLI commands using standard UNIX conventions [2, 5]:
- Positional Arguments: Use only for the primary target(s) of the command (e.g., input file paths, directory paths, or resource IDs) [2]. Limit to maximum 1-2 positionals.
- Named Options & Flags: Use prefixed options (e.g.,
--category or -c) for all optional inputs, filters, thresholds, or switches [2]. Flags (boolean options like --verbose or --force) require no extra arguments.
2. Layered Architecture: Decouple Logic from parsing
Always separate the CLI presentation layer from your core logic [6]. This allows your core business logic to be tested independently and reused in other scripts.
❌ Anti-Pattern (Low-Think)
Mixing parsing and business logic with hardcoded files:
# sys.argv[1] is fragile; file path is hardcoded; logic and printing are mixed.
import sys, csv
def main():
category = sys.argv[1] if len(sys.argv) > 1 else None
with open('data.csv', 'r') as f:
for r in csv.DictReader(f):
if r['category'] == category:
print(r['amount'])
✅ Recommended Pattern (Thin Wrapper)
import sys, csv
def filter_amounts(csv_path, category=None):
"""Business logic separated from CLI arguments."""
with open(csv_path, 'r', encoding='utf-8') as f:
return [float(row['amount']) for row in csv.DictReader(f)
if not category or row['category'] == category]
if __name__ == "__main__":
category = sys.argv[2] if len(sys.argv) > 2 else None
amounts = filter_amounts(sys.argv[1], category)
print(f"Sum: {sum(amounts):.2f}")
3. CLI Framework Selection
Choose the right tool for your CLI requirements:
sys.argv: Use for 90% of basic scripts with 1-2 positional arguments.
argparse: Use only if the script requires multiple flags, typed options, or help documentation. No external dependencies needed.
4. UX & exit codes
- Exit Status: Return
0 for successful execution [16]. Return 1 for general runtime failures. Return 64 (EX_USAGE from BSD sysexits.h) for validation or command syntax errors [16].
- Stream Separation: Print primary outputs to
stdout [1, 9]. Write logs, warnings, progress spinners, and errors to stderr [1, 9].
- Deactivation Rules: Disable ANSI colors, carriage returns (
\r), and dynamic spinners automatically in non-TTY environments (e.g., when piped or redirected) [1]. Respect the NO_COLOR environment variable [1].
5. Over-engineering Prevention
Before writing any code, ask: does this need to exist? Does the standard library already cover it? Can it be a one-liner? Build the minimum that works. No unrequested abstractions.
Common Rationalizations
| Excuse |
Reality |
| "A hardcoded script is faster to write under pressure." |
Requirements change instantly. Designing it as a CLI with arguments saves refactoring time. |
"I'll parse command arguments using sys.argv to save lines." |
sys.argv lacks help formatting, error validation, type coercion, and order independence. Use argparse or click. |
| "The logic is simple, so it doesn't need to be in a separate function." |
Unseparated logic cannot be unit-tested without executing a terminal subprocess. |
Red Flags - STOP and Refactor
- You are parsing arguments using raw index values from
sys.argv.
- A file path, username, or config value is hardcoded inside your code.
- Your script prints diagnostic logs or errors to
stdout instead of stderr.
- Your CLI command function directly performs database queries or file calculations instead of wrapping a service function.
Violating the letter of the rules is violating the spirit of the rules.
If you see these red flags, refactor into a decoupled CLI tool immediately.
1---2name: building-reusable-cli-tools3description: Use when writing data processing scripts, analysis utilities, or multi-step automation tools in Python to ensure they are modular, configurable, testable, and reusable.4---56# Building Reusable CLI Tools78## Overview9Do not write overly complex, hardcoded single-purpose scripts if you will need to reuse them. When designing scripts that *must* be highly configurable, use `argparse` or `sys.argv` to create simple CLI utilities. Do not use heavy external dependencies like `click` unless explicitly requested. Always prefer a simple 1-line script if it solves the immediate problem.1011---1213## When to Use14- Writing scripts to analyze data (JSON, CSV, logs).15- Performing multi-step file or data manipulation tasks.16- When tempted to create multiple incremental scripts (e.g., `step1.py`, `step2.py`).17- When a script requires configurable parameters (inputs, thresholds, categories).1819---2021## 1. Option vs. Argument Guidelines2223Design your CLI commands using standard UNIX conventions [2, 5]:24- **Positional Arguments**: Use *only* for the primary target(s) of the command (e.g., input file paths, directory paths, or resource IDs) [2]. Limit to maximum 1-2 positionals.25- **Named Options & Flags**: Use prefixed options (e.g., `--category` or `-c`) for all optional inputs, filters, thresholds, or switches [2]. Flags (boolean options like `--verbose` or `--force`) require no extra arguments.2627---2829## 2. Layered Architecture: Decouple Logic from parsing3031Always separate the CLI presentation layer from your core logic [6]. This allows your core business logic to be tested independently and reused in other scripts.3233### ❌ Anti-Pattern (Low-Think)34Mixing parsing and business logic with hardcoded files:35```python36# sys.argv[1] is fragile; file path is hardcoded; logic and printing are mixed.37import sys, csv38def main():39 category = sys.argv[1] if len(sys.argv) > 1 else None40 with open('data.csv', 'r') as f:41 for r in csv.DictReader(f):42 if r['category'] == category:43 print(r['amount'])44```4546### ✅ Recommended Pattern (Thin Wrapper)47```python48import sys, csv4950def filter_amounts(csv_path, category=None):51 """Business logic separated from CLI arguments."""52 with open(csv_path, 'r', encoding='utf-8') as f:53 return [float(row['amount']) for row in csv.DictReader(f)54 if not category or row['category'] == category]5556if __name__ == "__main__":57 category = sys.argv[2] if len(sys.argv) > 2 else None58 amounts = filter_amounts(sys.argv[1], category)59 print(f"Sum: {sum(amounts):.2f}")60```6162---6364## 3. CLI Framework Selection6566Choose the right tool for your CLI requirements:67- **`sys.argv`**: Use for 90% of basic scripts with 1-2 positional arguments.68- **`argparse`**: Use only if the script requires multiple flags, typed options, or help documentation. No external dependencies needed.6970---7172## 4. UX & exit codes7374- **Exit Status**: Return `0` for successful execution [16]. Return `1` for general runtime failures. Return `64` (`EX_USAGE` from BSD `sysexits.h`) for validation or command syntax errors [16].75- **Stream Separation**: Print primary outputs to `stdout` [1, 9]. Write logs, warnings, progress spinners, and errors to `stderr` [1, 9].76- **Deactivation Rules**: Disable ANSI colors, carriage returns (`\r`), and dynamic spinners automatically in non-TTY environments (e.g., when piped or redirected) [1]. Respect the `NO_COLOR` environment variable [1].7778---7980## 5. Over-engineering Prevention8182Before writing any code, ask: does this need to exist? Does the standard library already cover it? Can it be a one-liner? Build the minimum that works. No unrequested abstractions.8384---8586## Common Rationalizations8788| Excuse | Reality |89| :--- | :--- |90| "A hardcoded script is faster to write under pressure." | Requirements change instantly. Designing it as a CLI with arguments saves refactoring time. |91| "I'll parse command arguments using `sys.argv` to save lines." | `sys.argv` lacks help formatting, error validation, type coercion, and order independence. Use `argparse` or `click`. |92| "The logic is simple, so it doesn't need to be in a separate function." | Unseparated logic cannot be unit-tested without executing a terminal subprocess. |9394---9596## Red Flags - STOP and Refactor9798- You are parsing arguments using raw index values from `sys.argv`.99- A file path, username, or config value is hardcoded inside your code.100- Your script prints diagnostic logs or errors to `stdout` instead of `stderr`.101- Your CLI command function directly performs database queries or file calculations instead of wrapping a service function.102103**Violating the letter of the rules is violating the spirit of the rules.**104**If you see these red flags, refactor into a decoupled CLI tool immediately.**