홈강좌Rust 입문·중급 47편 중 34편째
Rust 실습 — 파일의 단어 수를 세는 명령줄 도구 wordfreq 1판
입문 구간에서 배운 것만으로 명령줄 도구를 만든다. 파일 이름을 인자로 받아(env::args) 파일을 읽고(fs::read_to_string), HashMap 으로 단어를 세서 많이 나온 10개를 찍는다. 인자가 없거나 파일이 없으면 이유를 찍고 종료 코드 2·1 로 끝낸다.
01 무엇을 만드나
cargo new wordfreq
cargo run -- sample.txtcargo run 뒤의 -- 는 그다음 낱말을 cargo 가 아니라 우리 프로그램에 넘기라는 표시다. 프로젝트 폴더(Cargo.toml 옆)에 읽을 파일 sample.txt 를 둔다.
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 이다.
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 돌려 본다
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 fixthe 가 8번으로 가장 많다. 3번 나온 it·compiler·code·a 는 reverse() 때문에 알파벳 거꾸로 늘어섰다. 같은 횟수끼리 순서는 정하지 않았으니 이 정도로 둔다.
없는 파일을 넘기면 이유를 찍고 종료 코드 1 로 끝난다.
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 로 끝난다.
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판으로 다시 만든다. 다음 편부터 중급 구간이고, 첫 주제는 수명이다.