hyunsu blog

홈강좌Rust 입문·중급 47편 중 34편째

Rust 실습 — 파일의 단어 수를 세는 명령줄 도구 wordfreq 1판

목차 · 3
  1. 01무엇을 만드나
  2. 02전체 코드
  3. 03돌려 본다

입문 구간에서 배운 것만으로 명령줄 도구를 만든다. 파일 이름을 인자로 받아(env::args) 파일을 읽고(fs::read_to_string), HashMap 으로 단어를 세서 많이 나온 10개를 찍는다. 인자가 없거나 파일이 없으면 이유를 찍고 종료 코드 2·1 로 끝낸다.

01 무엇을 만드나

bash
cargo new wordfreq
cargo run -- sample.txt

cargo run 뒤의 -- 는 그다음 낱말을 cargo 가 아니라 우리 프로그램에 넘기라는 표시다. 프로젝트 폴더(Cargo.toml 옆)에 읽을 파일 sample.txt 를 둔다.

text
The compiler is strict, and the compiler is kind.
It stops the build when the code is wrong,
and it tells you where the code is wrong and how to fix it.
Read the error, fix the code, and run the build again.
A strict compiler is a friend, not a wall.

02 전체 코드

함수 셋으로 나눴다. 세기(count_words), 고르기(top), 그리고 입력과 출력을 맡는 main 이다.

rust
use std::collections::HashMap;
use std::env;
use std::fs;
use std::process;

fn count_words(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for raw in text.split_whitespace() {
        let word = raw.trim_matches(['.', ',', '!', '?', '"']).to_lowercase();
        if word.is_empty() {
            continue;
        }
        let count = counts.entry(word).or_insert(0);
        *count += 1;
    }
    counts
}

fn top(counts: HashMap<String, usize>, n: usize) -> Vec<(usize, String)> {
    let mut pairs = Vec::new();
    for (word, count) in counts {
        pairs.push((count, word));
    }
    pairs.sort();
    pairs.reverse();
    pairs.truncate(n);
    pairs
}

fn main() {
    let Some(path) = env::args().nth(1) else {
        eprintln!("usage: wordfreq <file>");
        process::exit(2);
    };

    let text = match fs::read_to_string(&path) {
        Ok(text) => text,
        Err(e) => {
            eprintln!("cannot read {path}: {e}");
            process::exit(1);
        }
    };

    for (count, word) in top(count_words(&text), 10) {
        println!("{count:>3} {word}");
    }
}

count_words — 단어를 센다

29편의 단어 세기와 같은 뼈대다. 두 가지를 더했다.

  • raw.trim_matches(['.', ',', '!', '?', '"']) 는 단어 앞뒤의 문장 부호를 떼어 낸다. strict, 가 strict 가 된다.
  • .to_lowercase() 는 소문자로 바꾼다. The 와 the 를 같은 단어로 센다. 결과가 새 String 이라 HashMap 의 키가 String 이다.
  • 부호만 있던 낱말은 빈 문자열이 되니 continue 로 건너뛴다(13편). *count += 1 은 *count = *count + 1 의 줄임이다.

top — 많이 나온 것부터 n 개

HashMap 은 순서가 없으니(29편) Vec 으로 옮겨 정렬한다(28편).

  • for (word, count) in counts 는 HashMap 의 짝을 하나씩 꺼낸다. counts 를 넘겨받았으니(17편) 이동해도 된다.
  • 짝을 (count, word) 순서로 튜플(8편)에 담는다. 튜플은 앞 칸부터 비교하므로 sort() 가 횟수 순으로 늘어놓는다.
  • reverse() 로 많은 것부터, truncate(n) 으로 앞의 n 개만 남긴다.

main — 입력을 받고 실패를 알린다

  • env::args().nth(1) 은 두 번째 인자(첫째는 프로그램 자신)를 Option 으로 준다. 27편의 let else 로, 없으면 사용법을 찍고 끝낸다.
  • fs::read_to_string(&path) 는 파일을 통째로 String 으로 읽어 Result 를 돌려준다(30편). match 로 나눈다.
  • eprintln! 은 println! 과 같은데, 화면의 오류 출력(stderr)으로 찍는다. 결과와 오류가 섞이지 않는다.
  • process::exit(1) 은 그 자리에서 종료 코드 1 로 끝낸다. 31편처럼 0 이 아닌 코드가 "실패" 다.
  • {count:>3} 은 숫자를 3칸 폭에 오른쪽으로 맞춰 찍는다. 줄이 가지런해진다.

03 돌려 본다

text
   Compiling wordfreq v0.1.0 (~/projects/wordfreq)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/wordfreq sample.txt`
  8 the
  5 is
  4 and
  3 it
  3 compiler
  3 code
  3 a
  2 wrong
  2 strict
  2 fix

the 가 8번으로 가장 많다. 3번 나온 it·compiler·code·a 는 reverse() 때문에 알파벳 거꾸로 늘어섰다. 같은 횟수끼리 순서는 정하지 않았으니 이 정도로 둔다.

없는 파일을 넘기면 이유를 찍고 종료 코드 1 로 끝난다.

text
   Compiling wordfreq v0.1.0 (~/projects/wordfreq)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/wordfreq nope.txt`
cannot read nope.txt: No such file or directory (os error 2)

인자 없이 돌리면 사용법을 찍고 종료 코드 2 로 끝난다.

text
   Compiling wordfreq v0.1.0 (~/projects/wordfreq)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/wordfreq`
usage: wordfreq <file>

둘 다 패닉 없이, 사람이 읽을 문장과 종료 코드로 실패를 알렸다. 1편의 설치부터 여기까지가 입문이다. 이 도구는 47편에서 모듈·반복자·스레드·테스트를 입혀 2판으로 다시 만든다. 다음 편부터 중급 구간이고, 첫 주제는 수명이다.