CLI 도구 개발
argparse, Click, Typer로 커맨드라인 도구를 만들고 Rich로 출력을 아름답게 꾸밉니다.
argparse — 표준 라이브러리
import argparse
import sys
def main():
parser = argparse.ArgumentParser(
description="파일 변환 도구",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
예시:
python cli.py convert input.csv -o output.json --format json
python cli.py info input.csv --verbose
""",
)
subparsers = parser.add_subparsers(dest="command", required=True)
# convert 서브커맨드
convert_parser = subparsers.add_parser("convert", help="파일 형식 변환")
convert_parser.add_argument("input", help="입력 파일 경로")
convert_parser.add_argument("-o", "--output", required=True, help="출력 파일 경로")
convert_parser.add_argument(
"--format",
choices=["json", "csv", "yaml"],
default="json",
help="출력 형식 (기본: json)",
)
convert_parser.add_argument("--encoding", default="utf-8")
# info 서브커맨드
info_parser = subparsers.add_parser("info", help="파일 정보 출력")
info_parser.add_argument("input", help="분석할 파일")
info_parser.add_argument("-v", "--verbose", action="store_true", help="상세 출력")
args = parser.parse_args()
if args.command == "convert":
convert(args.input, args.output, args.format, args.encoding)
elif args.command == "info":
show_info(args.input, args.verbose)
def convert(input_file, output_file, fmt, encoding):
print(f"{input_file} → {output_file} ({fmt})")
def show_info(input_file, verbose):
print(f"파일: {input_file}, 상세: {verbose}")
if __name__ == "__main__":
main()
Click — 데코레이터 기반 CLI
pip install click
import click
from pathlib import Path
@click.group()
@click.version_option(version="1.0.0")
def cli():
"""파일 처리 CLI 도구"""
pass
@cli.command()
@click.argument("input_file", type=click.Path(exists=True))
@click.option("-o", "--output", required=True, type=click.Path(), help="출력 파일")
@click.option("--format", "fmt", type=click.Choice(["json", "csv", "yaml"]), default="json")
@click.option("--encoding", default="utf-8", show_default=True)
@click.option("--dry-run", is_flag=True, help="실제 변환 없이 시뮬레이션")
def convert(input_file, output, fmt, encoding, dry_run):
"""파일 형식을 변환합니다."""
if dry_run:
click.echo(f"[DRY-RUN] {input_file} → {output} ({fmt})")
return
click.echo(f"변환 중: {input_file}", err=True) # stderr 출력
# ... 실제 변환 로직
click.secho(f"✅ 완료: {output}", fg="green", bold=True)
@cli.command()
@click.argument("files", nargs=-1, required=True, type=click.Path(exists=True))
@click.option("--verbose", "-v", is_flag=True)
@click.pass_context
def info(ctx, files, verbose):
"""파일 정보를 출력합니다."""
for file in files:
path = Path(file)
size = path.stat().st_size
click.echo(f"{path.name}: {size:,} bytes")
if verbose:
click.echo(f" 절대경로: {path.resolve()}")
click.echo(f" 확장자: {path.suffix}")
# 프롬프트 + 확인
@cli.command()
@click.argument("target")
@click.confirmation_option(prompt="정말 삭제하시겠습니까?")
def delete(target):
"""파일/디렉터리를 삭제합니다."""
click.echo(f"삭제: {target}")
# 진행 바
@cli.command()
@click.argument("items", type=int, default=100)
def process(items):
"""대용량 처리 시뮬레이션"""
with click.progressbar(range(items), label="처리 중") as bar:
for _ in bar:
import time; time.sleep(0.01)
if __name__ == "__main__":
cli()
Typer — 타입 힌트 기반 CLI
pip install typer[all]
import typer
from pathlib import Path
from typing import Optional
from enum import Enum
app = typer.Typer(help="파일 처리 CLI 도구", rich_markup_mode="rich")
class OutputFormat(str, Enum):
json = "json"
csv = "csv"
yaml = "yaml"
@app.command()
def convert(
input_file: Path = typer.Argument(..., help="입력 파일", exists=True),
output: Path = typer.Option(..., "--output", "-o", help="출력 경로"),
fmt: OutputFormat = typer.Option(OutputFormat.json, "--format", help="출력 형식"),
encoding: str = typer.Option("utf-8", help="인코딩"),
dry_run: bool = typer.Option(False, "--dry-run", help="시뮬레이션 모드"),
):
"""[bold]파일 형식을 변환[/bold]합니다."""
if dry_run:
typer.echo(f"[DRY-RUN] {input_file} → {output}")
return
typer.echo(f"변환 중: {input_file}")
typer.secho("완료!", fg=typer.colors.GREEN, bold=True)
@app.command()
def info(
files: list[Path] = typer.Argument(..., help="분석할 파일 목록"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
):
"""파일 정보를 출력합니다."""
for file in files:
size = file.stat().st_size
typer.echo(f"{file.name}: {size:,} bytes")
if verbose:
typer.echo(f" 경로: {file.resolve()}")
# 서브 앱 (중첩 커맨드)
db_app = typer.Typer(help="데이터베이스 관련 명령")
app.add_typer(db_app, name="db")
@db_app.command("migrate")
def db_migrate(
revision: str = typer.Argument("head"),
dry_run: bool = typer.Option(False),
):
"""마이그레이션 실행"""
typer.echo(f"마이그레이션: {revision}")
if __name__ == "__main__":
app()
Rich — 터미널 출력 스타일링
pip install rich
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
from rich.syntax import Syntax
from rich.tree import Tree
from rich.live import Live
from rich import print as rprint
import time
console = Console()
# ── 기본 마크업 ──────────────────────────────────────────
console.print("[bold green]성공![/bold green] 작업 완료")
console.print("[red]오류:[/red] 파일을 찾을 수 없습니다", style="bold")
console.rule("[blue]섹션 구분선[/blue]")
# ── 패널 ────────────────────────────────────────────────
console.print(Panel.fit(
"[bold]배포 완료[/bold]\n서버: prod-01\n버전: v2.3.1",
title="[green]✅ SUCCESS[/green]",
border_style="green",
))
# ── 테이블 ───────────────────────────────────────────────
table = Table(title="서버 상태", show_lines=True)
table.add_column("서버", style="cyan", no_wrap=True)
table.add_column("상태", justify="center")
table.add_column("CPU", justify="right")
table.add_column("메모리", justify="right")
table.add_row("web-01", "[green]●[/green] 정상", "23%", "4.2 GB")
table.add_row("web-02", "[green]●[/green] 정상", "31%", "3.8 GB")
table.add_row("db-01", "[yellow]●[/yellow] 경고", "87%", "14.1 GB")
table.add_row("cache-01","[red]●[/red] 오류", "—", "—")
console.print(table)
# ── 진행 바 ──────────────────────────────────────────────
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=console,
) as progress:
task1 = progress.add_task("파일 다운로드", total=100)
task2 = progress.add_task("압축 해제", total=50)
for i in range(100):
progress.update(task1, advance=1)
if i >= 50:
progress.update(task2, advance=1)
time.sleep(0.03)
# ── 코드 하이라이팅 ──────────────────────────────────────
code = '''
def hello(name: str) -> str:
return f"Hello, {name}!"
'''
console.print(Syntax(code, "python", theme="monokai", line_numbers=True))
# ── 트리 구조 ────────────────────────────────────────────
tree = Tree("[bold]프로젝트[/bold]")
src = tree.add("[blue]src/[/blue]")
src.add("main.py")
src.add("config.py")
tests = tree.add("[yellow]tests/[/yellow]")
tests.add("test_main.py")
console.print(tree)
# ── 로깅 통합 ────────────────────────────────────────────
from rich.logging import RichHandler
import logging
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
handlers=[RichHandler(console=console, rich_tracebacks=True)],
)
logger = logging.getLogger(__name__)
logger.info("서버 시작")
logger.warning("디스크 용량 부족")
logger.error("연결 실패")
Click + Rich 통합 패턴
import click
from rich.console import Console
from rich.table import Table
console = Console()
err_console = Console(stderr=True)
def success(msg: str):
console.print(f"[bold green]✅[/bold green] {msg}")
def error(msg: str):
err_console.print(f"[bold red]❌[/bold red] {msg}")
raise SystemExit(1)
@click.command()
@click.argument("query")
@click.option("--limit", default=10, show_default=True)
def search(query: str, limit: int):
"""데이터베이스 검색"""
console.print(f"검색 중: [cyan]{query}[/cyan]")
# 결과 테이블
table = Table()
table.add_column("ID")
table.add_column("이름")
table.add_column("점수", justify="right")
for i in range(1, limit + 1):
table.add_row(str(i), f"항목 {i}", f"{90 + i}")
console.print(table)
success(f"{limit}건 검색 완료")
if __name__ == "__main__":
search()
정리
| 도구 | 특징 | 추천 상황 |
|---|---|---|
argparse | 표준 라이브러리, 장황 | 의존성 없는 배포 |
Click | 데코레이터, 직관적 | 일반 CLI 도구 |
Typer | 타입 힌트, 자동완성 | 타입 중심 개발 |
Rich | 출력 스타일링 | 모든 CLI 출력 개선 |
Click + Rich 조합이 가장 실용적이며, Typer는 FastAPI 개발자에게 친숙합니다.