Command-line tools are essential for automation and development workflows. Let's learn how to build professional-grade CLI tools with Python that your users will love! š
Why Build Command-Line Tools?
Command-line tools offer several advantages:
- Automation of repetitive tasks
- Integration with existing workflows
- Script-friendly interfaces
- Lower resource overhead
- Cross-platform compatibility
Setting Up Your Development Environment
First, let's create a new project and install necessary packages:
# Create a new directory
mkdir file-organizer
cd file-organizer
# Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install required packages
pip install click rich
Building a File Organizer Tool
Let's create a practical tool that organizes files by their extensions:
# file_organizer.py
import click
from pathlib import Path
from rich.progress import Progress
from rich.console import Console
import shutil
from typing import Dict, List
console = Console()
def organize_files(directory: Path) -> Dict[str, List[Path]]:
"""Organize files by their extensions."""
files_by_ext = {}
# Get all files
files = [f for f in directory.iterdir() if f.is_file()]
with Progress() as progress:
task = progress.add_task("[green]Organizing files...", total=len(files))
for file in files:
progress.advance(task)
ext = file.suffix.lower() or 'no_extension'
if ext not in files_by_ext:
files_by_ext[ext] = []
files_by_ext[ext].append(file)
return files_by_ext
@click.command()
@click.argument('directory', type=click.Path(exists=True))
@click.option('--dry-run', is_flag=True, help='Show what would be done without making changes')
@click.option('--ignore', multiple=True, help='Extensions to ignore (e.g., .git)')
def main(directory: str, dry_run: bool, ignore: tuple):
"""Organize files in DIRECTORY by their extensions."""
path = Path(directory)
with console.status("[bold green]Analyzing directory..."):
files_by_ext = organize_files(path)
# Display organization plan
console.print("\nš File Organization Plan:", style="bold blue")
for ext, files in files_by_ext.items():
if ext.lower() in ignore:
continue
console.print(f"\n[yellow]{ext}[/yellow]: {len(files)} files")
for file in files[:3]: # Show first 3 files as example
console.print(f" - {file.name}")
if len(files) > 3:
console.print(f" - ... and {len(files) - 3} more")
if dry_run:
return
# Perform organization
with Progress() as progress:
task = progress.add_task("[green]Moving files...", total=len(files_by_ext))
for ext, files in files_by_ext.items():
if ext.lower() in ignore:
continue
# Create directory for extension
ext_dir = path / ext.lstrip('.')
ext_dir.mkdir(exist_ok=True)
# Move files
for file in files:
try:
shutil.move(str(file), str(ext_dir / file.name))
except Exception as e:
console.print(f"[red]Error moving {file.name}: {e}[/red]")
progress.advance(task)
console.print("\nāØ Organization complete!", style="bold green")
if __name__ == '__main__':
main()
Adding Professional Features
1. Error Handling
Let's add robust error handling:
def safe_move(source: Path, dest: Path) -> bool:
"""Safely move a file with error handling."""
try:
if dest.exists():
# Handle duplicate files
base = dest.stem
suffix = dest.suffix
counter = 1
while dest.exists():
dest = dest.with_name(f"{base}_{counter}{suffix}")
counter += 1
shutil.move(str(source), str(dest))
return True
except Exception as e:
console.print(f"[red]Error moving {source.name}: {e}[/red]")
return False
2. Configuration Management
Add support for configuration files:
# config.py
import toml
from pathlib import Path
def load_config(config_path: Path = None) -> dict:
"""Load configuration from a TOML file."""
default_config = {
'ignore_extensions': ['.git', '.DS_Store'],
'organize_recursive': False,
'create_date_folders': False
}
if not config_path:
return default_config
try:
with open(config_path) as f:
user_config = toml.load(f)
return {**default_config, **user_config}
except Exception as e:
console.print(f"[yellow]Warning: Could not load config: {e}[/yellow]")
return default_config
3. Progress Reporting
Enhance progress reporting with file counts:
def count_files(directory: Path, ignore: tuple) -> int:
"""Count files recursively, excluding ignored extensions."""
count = 0
for item in directory.rglob('*'):
if item.is_file() and item.suffix.lower() not in ignore:
count += 1
return count
4. Interactive Mode
Add an interactive mode for better user experience:
@click.option('--interactive', '-i', is_flag=True, help='Run in interactive mode')
def main(directory: str, interactive: bool, ...):
if interactive:
extensions = list(files_by_ext.keys())
selected = questionary.checkbox(
'Select extensions to organize:',
choices=extensions
).ask()
files_by_ext = {k: v for k, v in files_by_ext.items()
if k in selected}
Using the Tool
Our file organizer can be used in various ways:
- Basic Usage:
python file_organizer.py /path/to/directory
- Dry Run:
python file_organizer.py /path/to/directory --dry-run
- Ignore Specific Extensions:
python file_organizer.py /path/to/directory --ignore .git --ignore .env
- Interactive Mode:
python file_organizer.py /path/to/directory --interactive
Best Practices for CLI Tools
- Clear Documentation
- Use descriptive help messages
- Provide examples in --help
- Document all options
- User Feedback
- Show progress for long operations
- Provide clear error messages
- Use colors for emphasis
- Safety Features
- Implement --dry-run options
- Add confirmation for destructive operations
- Handle edge cases gracefully
- Performance
- Use generators for large file lists
- Implement parallel processing for large operations
- Cache results when appropriate
Testing Your CLI Tool
Create comprehensive tests:
# test_file_organizer.py
import pytest
from pathlib import Path
from file_organizer import organize_files
@pytest.fixture
def test_directory(tmp_path):
# Create test files
(tmp_path / "test.txt").touch()
(tmp_path / "image.jpg").touch()
return tmp_path
def test_organize_files(test_directory):
result = organize_files(test_directory)
assert '.txt' in result
assert '.jpg' in result
assert len(result['.txt']) == 1
assert len(result['.jpg']) == 1
Packaging Your Tool
Make your tool installable:
# setup.py
from setuptools import setup, find_packages
setup(
name="file-organizer",
version="0.1.0",
packages=find_packages(),
install_requires=[
'click>=8.0.0',
'rich>=10.0.0',
'questionary>=1.10.0'
],
entry_points={
'console_scripts': [
'organize=file_organizer:main',
],
}
)
Conclusion
Building professional command-line tools with Python is rewarding and practical. Key takeaways:
- Use established frameworks like Click
- Provide rich user feedback
- Implement proper error handling
- Add configuration options
- Package for easy distribution
Remember to:
- Write clear documentation
- Add comprehensive tests
- Handle edge cases
- Consider user experience
Start building your own CLI tools and automate your workflow today! š