본문으로 건너뛰기
Advertisement

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 개발자에게 친숙합니다.

Advertisement