[2026] Rust CLI 도구 만들기 | clap, 파일 처리, 에러 처리

[2026] Rust CLI 도구 만들기 | clap, 파일 처리, 에러 처리

이 글의 핵심

Rust CLI 도구 만들기: clap, 파일 처리, 에러 처리. 프로젝트 설정·clap으로 인자 파싱.

들어가며

단일 바이너리로 배포하기 쉽고, clap 등으로 인자 파싱·도움말을 정리하기 좋아 CLI 도구에 자주 쓰입니다. 파일·표준 입출력과 함께 Result로 오류를 전파하는 패턴이 일반적입니다.

1. 프로젝트 설정

Cargo.toml

아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

[package]
name = "my-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.0", features = [derive] }

2. clap으로 인자 파싱

기본 사용

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use clap::Parser;
#[derive(Parser)]
#[command(name = "my-cli")]
#[command(about = "간단한 CLI 도구", long_about = None)]
struct Args {
    #[arg(short, long)]
    name: String,
    
    #[arg(short, long, default_value_t = 1)]
    count: u32,
}
fn main() {
    let args = Args::parse();
    
    for _ in 0..args.count {
        println!("Hello, {}!", args.name);
    }
}

서브커맨드

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
#[derive(Subcommand)]
enum Commands {
    Add { name: String },
    Remove { id: u32 },
    List,
}
fn main() {
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Add { name } => {
            println!("추가: {}", name);
        }
        Commands::Remove { id } => {
            println!("삭제: {}", id);
        }
        Commands::List => {
            println!("목록 출력");
        }
    }
}

3. 파일 처리

파일 읽기/쓰기

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::fs;
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
fn read_file(path: &str) -> io::Result<String> {
    fs::read_to_string(path)
}
fn read_lines(path: &str) -> io::Result<Vec<String>> {
    let file = fs::File::open(path)?;
    let reader = BufReader::new(file);
    
    let mut lines = Vec::new();
    for line in reader.lines() {
        lines.push(line?);
    }
    
    Ok(lines)
}
fn write_file(path: &str, content: &str) -> io::Result<()> {
    fs::write(path, content)
}
fn append_file(path: &str, content: &str) -> io::Result<()> {
    use std::fs::OpenOptions;
    
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open(path)?;
    
    writeln!(file, "{}", content)?;
    Ok(())
}

4. 실전 예제: 단어 카운터

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use clap::Parser;
use std::fs;
use std::collections::HashMap;
#[derive(Parser)]
struct Args {
    #[arg(help = "파일 경로")]
    file: String,
    
    #[arg(short, long, help = "대소문자 구분 안 함")]
    ignore_case: bool,
}
fn count_words(text: &str, ignore_case: bool) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    
    for word in text.split_whitespace() {
        let word = word.trim_matches(|c: char| !c.is_alphanumeric());
        let word = if ignore_case {
            word.to_lowercase()
        } else {
            word.to_string()
        };
        
        *counts.entry(word).or_insert(0) += 1;
    }
    
    counts
}
fn main() -> std::io::Result<()> {
    let args = Args::parse();
    
    let content = fs::read_to_string(&args.file)?;
    let counts = count_words(&content, args.ignore_case);
    
    let mut words: Vec<_> = counts.iter().collect();
    words.sort_by(|a, b| b.1.cmp(a.1));
    
    println!("단어 빈도:");
    for (word, count) in words.iter().take(10) {
        println!("{}: {}", word, count);
    }
    
    Ok(())
}

실전 심화 보강

실전 예제: JSON 한 줄 포맷터 (stdin·파일·출력 경로)

단계: (1) 입력 소스 선택 (2) serde_json으로 파싱 (3) 예쁜 출력 또는 압축 출력 (4) 실패 시 비제로 종료 코드. Cargo.toml에 다음을 추가합니다. 아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

[dependencies]
clap = { version = "4", features = [derive] }
serde = { version = "1", features = [derive] }
serde_json = "1"
anyhow = "1"

다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process;
#[derive(Parser)]
#[command(name = "jsonfmt")]
#[command(about = "stdin 또는 파일의 JSON을 포맷합니다.")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
#[derive(Subcommand)]
enum Commands {
    /// 파일을 읽어 stdout에 출력
    File {
        path: PathBuf,
        #[arg(long, default_value_t = false)]
        compact: bool,
    },
    /// stdin에서 읽기
    Stdin {
        #[arg(long, default_value_t = false)]
        compact: bool,
    },
}
fn format_json(text: &str, compact: bool) -> Result<String> {
    let v: serde_json::Value =
        serde_json::from_str(text).context("JSON 파싱 실패")?;
    Ok(if compact {
        serde_json::to_string(&v)?
    } else {
        serde_json::to_string_pretty(&v)?
    })
}
fn main() {
    let cli = Cli::parse();
    let run = || -> Result<()> {
        match cli.command {
            Commands::File { path, compact } => {
                let s = fs::read_to_string(&path)
                    .with_context(|| format!("파일 읽기 실패: {}", path.display()))?;
                println!("{}", format_json(&s, compact)?);
            }
            Commands::Stdin { compact } => {
                let mut buf = String::new();
                io::stdin().read_to_string(&mut buf)?;
                println!("{}", format_json(&buf.trim(), compact)?);
            }
        }
        Ok(())
    };
    if let Err(e) = run() {
        eprintln!("error: {:#}", e);
        process::exit(1);
    }
}

자주 하는 실수

  • main에서 Result를 쓰지 않고 unwrap만 남발해 사용자에게 스택 트레이스가 그대로 노출되는 경우.
  • 상대 경로를 현재 작업 디렉터리에만 의존해 스크립트·CI에서 엉뚱한 파일을 읽는 경우.
  • 바이너리 이름과 패키지 이름을 혼동cargo install 후 실행 파일 이름을 문서에 잘못 안내하는 경우.

주의사항

  • Windows·macOS·Linux에서 경로 구분자와 줄바꿈이 다릅니다. 가능하면 std::path::Pathread_to_string을 사용하세요.
  • CLI는 종료 코드 규약(성공 0, 실패 비0)을 지키는 것이 스크립트 연동에 필수입니다.
  • 민감한 정보는 환경 변수나 설정 파일로 분리하고, --help 예시에 시크릿을 넣지 마세요.

실무에서는 이렇게

  • tracing + RUST_LOG로 디버그 로그를 켜고, 릴리스에서는 기본을 info 이상으로 둡니다.
  • clapenv 속성으로 API_KEY 같은 값을 플래그와 환경 변수 양쪽에서 받을 수 있게 하면 운영이 편합니다.
  • 배포는 cargo build —release 후 단일 바이너리를 GitHub Releases나 패키지 매니저에 올리고, 버전은 clapversionCargo.toml을 맞춥니다.

비교 및 대안

접근장점언제 쓸까
clap derive빠른 개발, 서브커맨드·검증 풍부대부분의 팀 CLI
clap builder API동적 인자 구성플러그인형 도구
std::env::args의존성 제로초소형 스크립트 대체
Python argparse 호출 래핑기존 스크립트 재사용점진적 Rust 이전

추가 리소스


정리

핵심 요약

  1. clap: CLI 인자 파싱, 서브커맨드
  2. std::fs: 파일 읽기/쓰기
  3. BufReader: 효율적인 파일 읽기
  4. Result: 에러 처리
  5. ?: 에러 전파

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3