Rust로 CLI 도구 만들기 — clap + indicatif로 쓸만한 개발자 도구 제작
Python으로 만든 스크립트가 너무 느려서 Rust로 다시 만들었다. 30초→0.3초.
Cargo.toml 의존성
[package]
name = "audio-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.4", features = ["derive"] }
indicatif = "0.17"
colored = "2.1"
anyhow = "1.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
[[bin]]
name = "audio-cli"
path = "src/main.rs"
Argument Parsing with clap
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "audio-cli")]
#[command(about = "오디오 처리 도구", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(global = true, short, long)]
verbose: bool,
}
#[derive(Subcommand)]
enum Commands {
/// 오디오 파일 분석
Analyze {
/// 입력 파일 경로
#[arg(short, long)]
input: String,
/// 상세 분석 수행
#[arg(short, long)]
detailed: bool,
/// 출력 형식 (json, csv)
#[arg(short, long, default_value = "json")]
format: String,
},
/// 배치 처리
Process {
/// 입력 디렉토리
#[arg(short, long)]
input_dir: String,
/// 출력 디렉토리
#[arg(short, long)]
output_dir: String,
/// 동시 처리 수
#[arg(short = 'j', long, default_value_t = 4)]
jobs: usize,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Analyze { input, detailed, format } => {
analyze(&input, detailed, &format).await?;
}
Commands::Process { input_dir, output_dir, jobs } => {
process_batch(&input_dir, &output_dir, jobs).await?;
}
}
Ok(())
}
Progress Bar with indicatif
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
async fn process_batch(
input_dir: &str,
output_dir: &str,
jobs: usize,
) -> anyhow::Result<()> {
let files = std::fs::read_dir(input_dir)?
.collect::<Result<Vec<_>, _>>()?;
let pb = ProgressBar::new(files.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
.unwrap()
.progress_chars("#>-")
);
let mut handles = vec![];
for file in files {
let output_dir = output_dir.to_string();
let pb_clone = pb.clone();
let handle = tokio::spawn(async move {
match process_file(file.path(), &output_dir).await {
Ok(_) => {
pb_clone.inc(1);
Ok(())
}
Err(e) => {
pb_clone.println(format!("❌ Error: {}", e));
Err(e)
}
}
});
handles.push(handle);
if handles.len() >= jobs {
let _ = futures::future::join_all(handles).await;
handles = vec![];
}
}
let _ = futures::future::join_all(handles).await;
pb.finish_with_message("✓ 처리 완료!");
Ok(())
}
Colored Output
use colored::*;
fn print_summary(stats: &Stats) {
println!("{}", "=== 분석 결과 ===".bold().blue());
println!("{}: {}", "파일 수".cyan(), stats.file_count);
println!("{}: {}", "총 크기".green(), format_size(stats.total_size));
println!("{}: {}", "평균 품질".yellow(), stats.avg_quality);
if stats.errors > 0 {
println!("{}: {}", "에러".red().bold(), stats.errors);
}
}
fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut size = bytes as f64;
let mut unit_idx = 0;
while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
size /= 1024.0;
unit_idx += 1;
}
format!("{:.2} {}", size, UNITS[unit_idx])
}
Config File Support
~/.audio-cli.toml:
[defaults]
jobs = 8
output_format = "json"
[logging]
level = "info"
코드:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Config {
defaults: Defaults,
logging: Logging,
}
#[derive(Serialize, Deserialize)]
struct Defaults {
jobs: usize,
output_format: String,
}
#[derive(Serialize, Deserialize)]
struct Logging {
level: String,
}
fn load_config() -> anyhow::Result<Config> {
let config_path = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Cannot find home directory"))?
.join(".audio-cli.toml");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
Ok(toml::from_str(&content)?)
} else {
Ok(Config::default())
}
}
Cross-Platform Binary Distribution
GitHub Actions:
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: audio-cli
- os: macos-latest
target: x86_64-apple-darwin
artifact: audio-cli
- os: macos-latest
target: aarch64-apple-darwin
artifact: audio-cli
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: audio-cli.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- run: cargo build --target ${{ matrix.target }} --release
- uses: softprops/action-gh-release@v1
with:
files: target/${{ matrix.target }}/release/${{ matrix.artifact }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
결론
Rust CLI 도구는:
- 매우 빠르다 (Python의 100배)
- 배포가 간단하다 (단일 바이너리)
- 크로스 플랫폼 지원 (Mac/Windows/Linux)
- 에러 처리가 안전하다
30초 걸리던 Python 스크립트를 0.3초로 만들었다. 이제 일상적으로 쓸 수 있다.