Skip to main content

Reusable CLI framework and utilities for consistent command-line interfaces

Project description

cli-standard-kit

Reusable CLI framework and utilities for consistent command-line interfaces in Python.

This package provides a complete framework and common building blocks for standardized, professional Python CLI tools:

🎯 Core Framework

  • StandardCLI: Manage argparse and command execution with subcommand support
  • BaseCommand: Abstract base class for creating reusable CLI commands
  • Command Registration: Dynamically register commands with cli.register()
  • Automatic Help: Built-in help generation for commands and subcommands

🛠️ Utilities

  • Standardized logging (file + console) with verbose/quiet modes
  • ANSI color utilities and message formatting templates
  • Conventional directory layout helpers
  • File operations for batch processing with progress tracking
  • Argument parser with a consistent set of flags
  • Pure Python (stdlib only, argparse-based)

Installation

From PyPI

pip install cli-standard-kit

Development (editable)

git clone https://github.com/c3nk/cli-standard-kit.git
cd cli-standard-kit
pip install -e .

Quick Start

Using the Framework

Create a CLI with commands using the framework:

from cli_commons import StandardCLI, BaseCommand
from argparse import ArgumentParser

class ListCommand(BaseCommand):
    name = "list"
    description = "List items"
    
    def add_arguments(self, parser: ArgumentParser) -> None:
        parser.add_argument("--all", action="store_true", help="Show all items")
    
    def run(self, args) -> int:
        print("Listing items...")
        if args.all:
            print("All items")
        return 0

class CreateCommand(BaseCommand):
    name = "create"
    description = "Create a new item"
    
    def add_arguments(self, parser: ArgumentParser) -> None:
        parser.add_argument("name", help="Item name")
    
    def run(self, args) -> int:
        print(f"Creating {args.name}...")
        return 0

def main():
    cli = StandardCLI("mytool", "My awesome CLI tool")
    cli.register(ListCommand())
    cli.register(CreateCommand())
    return cli.run()

if __name__ == "__main__":
    exit(main())

Usage:

mytool list --all
mytool create myitem
mytool --help

Using Utilities Only

Below is a minimal CLI using cli-standard-kit utility components.

import sys
from pathlib import Path
from cli_commons.parser import create_standard_parser, validate_arguments
from cli_commons.logger import setup_logging
from cli_commons.directories import setup_directories
from cli_commons.colors import MessageFormatter
from cli_commons.file_ops import get_files_recursive, process_batch_files


def process_file(file_path: Path) -> tuple[bool, str]:
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            _ = f.read()
        return True, "Processed successfully"
    except Exception as e:
        return False, str(e)


def main() -> int:
    parser = create_standard_parser(
        prog="my-tool",
        description="My awesome CLI tool",
        version="1.0.0",
        epilog="Examples:\n  my-tool ./inputs --output ./outputs",
    )
    args = parser.parse_args()

    errors = validate_arguments(args)
    if errors:
        for error in errors:
            print(MessageFormatter.error(error), file=sys.stderr)
        return 1

    logger = setup_logging(args.log_file, args.verbose, args.quiet)
    dirs = setup_directories(Path.cwd())

    logger.info("Starting my-tool")

    try:
        input_files: list[Path] = []
        for p in args.paths:
            input_files.extend(get_files_recursive(p))

        if not input_files:
            print(MessageFormatter.warning("No files found to process"))
            return 0

        print(MessageFormatter.process(f"Found {len(input_files)} files"))

        stats = process_batch_files(
            input_files,
            process_file,
            dirs,
            logger,
            dry_run=args.dry_run,
        )

        print("\n" + "=" * 70)
        if args.dry_run:
            print(MessageFormatter.dry_run(f"Would process: {stats['processed']} files"))
        else:
            if stats["failed"] > 0:
                print(
                    MessageFormatter.warning(
                        f"Processed: {stats['processed']}, Failed: {stats['failed']}"
                    )
                )
            else:
                print(
                    MessageFormatter.success(
                        f"All {stats['processed']} files processed successfully"
                    )
                )

        print("=" * 70 + "\n")
        if args.log_file:
            print(f"Log file: {args.log_file}")
        return 0

    except KeyboardInterrupt:
        print(MessageFormatter.warning("Interrupted by user"), file=sys.stderr)
        logger.warning("Interrupted by user")
        return 130
    except Exception as e:
        print(MessageFormatter.error(str(e)), file=sys.stderr)
        logger.exception("Fatal error")
        return 1


if __name__ == "__main__":
    sys.exit(main())

Framework API

StandardCLI

from cli_commons import StandardCLI, BaseCommand

cli = StandardCLI(
    prog="mytool",
    description="My CLI tool",
    epilog="See https://example.com for more info"
)

# Register commands
cli.register(MyCommand())

# Run CLI
exit_code = cli.run()

BaseCommand

from cli_commons import BaseCommand
from argparse import ArgumentParser

class MyCommand(BaseCommand):
    name = "mycmd"  # Required: command name
    description = "Does something"  # Optional: shown in help
    
    def add_arguments(self, parser: ArgumentParser) -> None:
        """Add command-specific arguments."""
        parser.add_argument("--flag", action="store_true")
        parser.add_argument("input", help="Input file")
    
    def run(self, args) -> int:
        """Execute command logic. Return 0 for success, non-zero for error."""
        print(f"Processing {args.input}")
        return 0

get_cli() Helper

from cli_commons import get_cli

cli = get_cli("mytool", "My tool description")
cli.register(MyCommand())
cli.run()

Utility Modules

colors.py

from cli_commons.colors import Colors, MessageFormatter

print(f"{Colors.GREEN}Success{Colors.END}")
print(MessageFormatter.success("Operation completed"))
print(MessageFormatter.error("Something failed"))
print(MessageFormatter.warning("Be careful"))

logger.py

from pathlib import Path
from cli_commons.logger import setup_logging

logger = setup_logging()  # creates ./logs/process_<timestamp>.log
logger = setup_logging(verbose=True)
logger = setup_logging(quiet=True)
logger = setup_logging(log_file=Path("./my.log"))

logger.info("Information message")
logger.debug("Debug message")
logger.warning("Warning message")
logger.error("Error message")

directories.py

from pathlib import Path
from cli_commons.directories import setup_directories, get_timestamped_dir

dirs = setup_directories()  # inputs/, outputs/, inputs/processed/, inputs/failed/, logs/
timestamped = get_timestamped_dir(Path("./outputs"), prefix="run")

file_ops.py

from pathlib import Path
from cli_commons.file_ops import (
    process_batch_files,
    get_files_recursive,
    get_output_filename,
    safe_rename,
)

def process_file(file_path: Path) -> tuple[bool, str]:
    return True, "Success"

files = get_files_recursive(Path("./inputs"), pattern="*.txt")
stats = process_batch_files(files, process_file, dirs, logger, dry_run=False)
output = get_output_filename(Path("file.txt"), suffix="processed")  # file_processed.txt
safe_rename(Path("old.txt"), Path("new.txt"), logger)

parser.py

from cli_commons.parser import (
    create_standard_parser,
    validate_arguments,
    validate_paths,
    validate_output_dir,
)

parser = create_standard_parser(
    prog="my-tool",
    description="What this tool does",
    version="1.0.0",
    epilog="Examples:\n  my-tool ./input",
)
args = parser.parse_args()

errors = validate_arguments(args)
if errors:
    for error in errors:
        print(error)
    sys.exit(1)

Standard Flags

Flag Short Description
--help -h Show help message
--version Show version
--verbose -v Enable verbose output (DEBUG)
--quiet -q Suppress output (ERROR only)
--dry-run -n Show what would be done
--log-file Save logs to file
--output -o Output directory (default: ./outputs)
--json JSON format output

Directory Structure

Created by setup_directories():

project/
├── inputs/
├── outputs/
├── inputs/processed/
├── inputs/failed/
└── logs/

Requirements

  • Python 3.9 or higher
  • No external dependencies (stdlib only)

License

MIT

Roadmap

Core Features (Planned)

  • Auto Command Discovery: Automatically discover and register commands from a directory
  • Global Flags: Add --verbose, --quiet, --dry-run flags available to all commands
  • Command Aliases: Support short names for commands (e.g., lslist)
  • Command Groups: Organize commands into groups (e.g., db:migrate, db:seed)

Advanced Features (Planned)

  • Middleware/Hooks System: Pre/post command execution hooks for logging, timing, authentication
  • Configuration File Support: Load settings from YAML/JSON/TOML config files
  • Enhanced Error Handling: Centralized exception handling with user-friendly error messages
  • Progress Indicators: Progress bars and spinners for long-running operations

Developer Experience (Planned)

  • Command Metadata: Rich command metadata (version, author, examples) for enhanced help
  • Testing Utilities: Mock helpers and testing framework for CLI commands
  • Tab Completion: Bash/Zsh tab completion scripts for commands and arguments
  • Plugin System: Support for external plugins and extensible architecture

Utility Enhancements (Planned)

  • Table/Formatter Utilities: Table formatting and JSON/CSV export utilities
  • Interactive Prompts: User input prompts and confirmation dialogs
  • File Watchers: File change monitoring and auto-reload functionality

See CHANGELOG.md for version history and recent changes.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cli_standard_kit-1.0.4.tar.gz (14.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cli_standard_kit-1.0.4-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

File details

Details for the file cli_standard_kit-1.0.4.tar.gz.

File metadata

  • Download URL: cli_standard_kit-1.0.4.tar.gz
  • Upload date:
  • Size: 14.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for cli_standard_kit-1.0.4.tar.gz
Algorithm Hash digest
SHA256 7c456545013cd70d70cb410ccf222357a69aa58dbceb61f969c5b791750d97cb
MD5 a0e74487a29199651edddf5212d47c14
BLAKE2b-256 9b9281f3aef0529ddf3676b10431370cb6fa3ad3e5a062e50ece98d0b7df5577

See more details on using hashes here.

File details

Details for the file cli_standard_kit-1.0.4-py3-none-any.whl.

File metadata

File hashes

Hashes for cli_standard_kit-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 50a12e5c6bc2f5ca93bc52cfeff5f3a8efb24db2bc1be6f818a1ed87ec830c27
MD5 de9b29aa083d1dd365a997777af96d94
BLAKE2b-256 e46a7d243e51743bc5ad40842c3ccbaf79e55f8932705a71a3b6b1ec52fe293c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page