cli-template.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. """
  2. CLI Application Template
  3. A production-ready CLI application structure.
  4. Usage:
  5. python cli.py --help
  6. python cli.py greet "World"
  7. python cli.py config init
  8. """
  9. import sys
  10. from pathlib import Path
  11. from typing import Annotated, Optional
  12. import typer
  13. from rich.console import Console
  14. from rich.table import Table
  15. from rich.panel import Panel
  16. from rich.progress import track
  17. # =============================================================================
  18. # App Setup
  19. # =============================================================================
  20. app = typer.Typer(
  21. name="myapp",
  22. help="My awesome CLI application",
  23. no_args_is_help=True,
  24. add_completion=True,
  25. rich_markup_mode="rich",
  26. )
  27. console = Console()
  28. err_console = Console(stderr=True)
  29. # Sub-applications
  30. config_app = typer.Typer(help="Configuration commands")
  31. app.add_typer(config_app, name="config")
  32. # =============================================================================
  33. # State and Configuration
  34. # =============================================================================
  35. class AppState:
  36. """Application state shared across commands."""
  37. def __init__(self):
  38. self.verbose: bool = False
  39. self.config_dir: Path = Path.home() / ".config" / "myapp"
  40. self.config_file: Path = self.config_dir / "config.toml"
  41. state = AppState()
  42. @app.callback()
  43. def main(
  44. verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
  45. config: Optional[Path] = typer.Option(
  46. None, "--config", "-c", help="Config file path"
  47. ),
  48. ):
  49. """
  50. [bold blue]MyApp[/bold blue] - A sample CLI application.
  51. Use [green]--help[/green] on any command for more info.
  52. """
  53. state.verbose = verbose
  54. if config:
  55. state.config_file = config
  56. # =============================================================================
  57. # Utility Functions
  58. # =============================================================================
  59. def log(message: str, style: str = ""):
  60. """Log message if verbose mode is enabled."""
  61. if state.verbose:
  62. console.print(f"[dim]{message}[/dim]", style=style)
  63. def error(message: str, code: int = 1) -> None:
  64. """Print error and exit."""
  65. err_console.print(f"[red]Error:[/red] {message}")
  66. raise typer.Exit(code)
  67. def success(message: str) -> None:
  68. """Print success message."""
  69. console.print(f"[green]✓[/green] {message}")
  70. # =============================================================================
  71. # Commands
  72. # =============================================================================
  73. @app.command()
  74. def greet(
  75. name: Annotated[str, typer.Argument(help="Name to greet")],
  76. count: Annotated[int, typer.Option("--count", "-n", help="Times to greet")] = 1,
  77. loud: Annotated[bool, typer.Option("--loud", "-l", help="Uppercase")] = False,
  78. ):
  79. """
  80. Say hello to someone.
  81. Example:
  82. myapp greet World
  83. myapp greet World --count 3 --loud
  84. """
  85. message = f"Hello, {name}!"
  86. if loud:
  87. message = message.upper()
  88. for _ in range(count):
  89. console.print(message)
  90. @app.command()
  91. def process(
  92. files: Annotated[
  93. list[Path],
  94. typer.Argument(
  95. help="Files to process",
  96. exists=True,
  97. readable=True,
  98. ),
  99. ],
  100. output: Annotated[
  101. Optional[Path],
  102. typer.Option("--output", "-o", help="Output file"),
  103. ] = None,
  104. ):
  105. """
  106. Process one or more files.
  107. Example:
  108. myapp process file1.txt file2.txt -o output.txt
  109. """
  110. log(f"Processing {len(files)} files")
  111. results = []
  112. for file in track(files, description="Processing..."):
  113. log(f"Processing: {file}")
  114. # Simulate processing
  115. results.append(f"Processed: {file.name}")
  116. if output:
  117. output.write_text("\n".join(results))
  118. success(f"Results written to {output}")
  119. else:
  120. for result in results:
  121. console.print(result)
  122. @app.command()
  123. def status():
  124. """Show application status."""
  125. table = Table(title="Application Status")
  126. table.add_column("Setting", style="cyan")
  127. table.add_column("Value", style="green")
  128. table.add_row("Config Dir", str(state.config_dir))
  129. table.add_row("Config File", str(state.config_file))
  130. table.add_row("Verbose", str(state.verbose))
  131. table.add_row(
  132. "Config Exists",
  133. "✓" if state.config_file.exists() else "✗"
  134. )
  135. console.print(table)
  136. # =============================================================================
  137. # Config Subcommands
  138. # =============================================================================
  139. @config_app.command("init")
  140. def config_init(
  141. force: Annotated[
  142. bool,
  143. typer.Option("--force", "-f", help="Overwrite existing"),
  144. ] = False,
  145. ):
  146. """Initialize configuration file."""
  147. if state.config_file.exists() and not force:
  148. if not typer.confirm(f"Config exists at {state.config_file}. Overwrite?"):
  149. raise typer.Abort()
  150. state.config_dir.mkdir(parents=True, exist_ok=True)
  151. default_config = """
  152. # MyApp Configuration
  153. # See documentation for all options
  154. [general]
  155. verbose = false
  156. [server]
  157. host = "localhost"
  158. port = 8080
  159. """.strip()
  160. state.config_file.write_text(default_config)
  161. success(f"Created config: {state.config_file}")
  162. @config_app.command("show")
  163. def config_show():
  164. """Show current configuration."""
  165. if not state.config_file.exists():
  166. error(f"Config not found: {state.config_file}")
  167. content = state.config_file.read_text()
  168. console.print(Panel(content, title=str(state.config_file), border_style="blue"))
  169. @config_app.command("path")
  170. def config_path():
  171. """Print config file path."""
  172. typer.echo(state.config_file)
  173. # =============================================================================
  174. # Version
  175. # =============================================================================
  176. def version_callback(value: bool):
  177. if value:
  178. console.print("myapp version [bold]1.0.0[/bold]")
  179. raise typer.Exit()
  180. @app.callback()
  181. def version_option(
  182. version: Annotated[
  183. bool,
  184. typer.Option(
  185. "--version",
  186. callback=version_callback,
  187. is_eager=True,
  188. help="Show version",
  189. ),
  190. ] = False,
  191. ):
  192. pass
  193. # =============================================================================
  194. # Entry Point
  195. # =============================================================================
  196. if __name__ == "__main__":
  197. app()