프로젝트 생성합니다. 그리고 프로젝트 폴더로 이동합니다.
cargo new minigrep
cd minigrep
이 프로젝트는 다음과 같이 입력할 예정입니다. 이렇게 입력하면 example-filename.txt
에서 우리가 찾고자 하는 searchstring
을 찾아 줄 것입니다.
cargo run searchstring example-filename.txt
윗 코드를 실행하면 다음과 같이 결과가 나옵니다. 아무 것도 작성하지 않았기 때문에 **Hello, world!**가 나오는 것은 당연합니다.
❯ cargo run searchstring example-filename.txt
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 4.49s
Running `target/debug/minigrep searchstring example-filename.txt`
Hello, world!
인수들의 값을 읽기 위해서 std::env::args
을 이용합니다. 다음과 같이 하면 입력된 값이 args
에 저장됩니다.
use std::env;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
println!("{:?}", args)
}
윗 코드를 실행하면 다음과 같습니다. 앞에서 실행한 것과는 다르게 값 3개가 화면에 나온다.
❯ cargo run searchstring example-filename.txt
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.01s
Running `target/debug/minigrep searchstring example-filename.txt`
["target/debug/minigrep", "searchstring", "example-filename.txt"]
출력 값 설명
- "target/debug/minigrep": 지금 실행한 프로그램 이름
- "searchstring": 첫 번째로 입력한 값. 우리가 찾고자 하는 문자열
- "example-filename.txt": 두 번째로 입력한 값. 우리가 찾고자 문자열을 찾을 텍스트 파일
앞에서 살펴본 것처럼 &args[0]
은 프로그램 이름입니다.
use std::env;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("검색어: {}", query);
println!("대상 파일 이름: {}", filename);
}
윗 파일을 실행해 보면 다음과 같은 결과가 나옵니다.
❯ cargo run searchstring example-filename.txt
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 3.41s
Running `target/debug/minigrep searchstring example-filename.txt`
검색어: searchstring
대상 파일 이름: example-filename.txt
참고로 현재 사용자가 인수를 전달하지 않는 상황과 같은 에러 상황은 처리하지 않았습니다.
std::fs
을 이용하여 파일을 읽는 코드를 구현하겠습니다.
물론 프로젝트 디렉토리 안에 읽을려고 하는 poem.txt
이 있고, 그 내용은 다음과 같이 입력해 놓았습니다.
❯ ls
Cargo.lock Cargo.toml poem.txt src target
❯ cat poem.txt
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
코드는 다음과 같습니다.
use std::env;
use std::fs;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("검색어: {}", query);
println!("대상 파일 이름: {}", filename);
let contents = fs::read_to_string(filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
그러면 파일을 읽어보겠습니다. 결과는 다음과 같습니다.
❯ cargo run the poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep the poem.txt`
검색어: the
대상 파일 이름: poem.txt
읽은 파일 내용:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
만약 현재 디렉토리에 없는 파일 이름을 입력하면 다음과 같은 결과가 나옵니다.
❯ cargo run the poem
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep the poem`
검색어: the
대상 파일 이름: poem
thread 'main' panicked at '파읽을 읽을 수 없섭니다.: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:49
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
To improve our program, we’ll fix four problems that have to do with the program’s structure and how it’s handling potential errors.
우리의 프로그램을 향상하기 위해서, 우리는 이 프로그램의 구조와 이 프로그램의 잠재적인 에러를 다루기 위한 방법과 관련이 있는 네 가지 문제를 수선(fix)할 것이다.
- 모든 것을
main
함수에서 하고 있다. 여러 함수로 나눠야 한다. - 1번과 묶여 있는 문제인데,
main
함수가 길어질수록 더 많은 변수를 선언해야 하는데, 범위 안에서 우리가 가진 변수가 많으면 많을 수록, 그 변수들의 각각의 목적의 노선을 유지하는 것이 어렵다(the more variables we have in scope, the harder it will be to keep track of the purpose of each). 그래서 설정(configuration) 변수들을 하나의 구조체로 모아 그 변수들의 목적을 명확하게 만드는 것이 최상이다. - 현재 파일을 열지 못했을 때, 에러 메세지가 단순하다.
- 모든 에러 처리 로직을 한 곳으로 모으면 사용자에게 더 의미있는 에러 메세지를 출력할 수 있다.
- 여러분의 프로그램을
main.rs
과lib.rs
으로 나누고 프로그램의 로직은lib.rs
으로 옮긴다. - 여러분의 명령어 줄 구문분석(parsing) 로직이 충분히 작다면, 그 로직을
main.rs
안에 남겨둘 수 있다. - When the command line parsing logic starts getting complicated, extract it from main.rs and move it to lib.rs.
parse_config()
을 만들어서 인수에서 query
, filename
을 파싱하는 파서를 옮긴다.
use std::env;
use std::fs;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
println!("검색어: {}", query);
println!("대상 파일 이름: {}", filename);
let contents = fs::read_to_string(filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
윗 코드를 실행하면 다음과 같다. 앞에서 했던 것과 정확하게 똑같이 작동한다. 아직까지는 main()
에서 변수 query
, filename
을 선언하고 있기는 하지만, 거기에 적당한 값을 파싱하여 넣어주는 역할은 parse_config()
이 맡게 되었다.
❯ cargo run the poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep the poem.txt`
검색어: the
대상 파일 이름: poem.txt
읽은 파일 내용:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
그러나 앞에서는 여전히 개별 변수에 대입하고 있다. 이는 아직까지 적절한 추상화(abstraction)를 못 했다는 것을 모여주는 것이다. 이것들을 구조체로 묶자. 코드를 다 짜고 cargo fmt
이라고 하면 코드를 정리할 수 있다.
use std::env;
use std::fs;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("검색어: {}", config.query);
println!("대상 파일 이름: {}", config.filename);
let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
struct Config {
query: String,
filename: String,
}
fn parse_config(args: &[String]) -> Config {
let query = &args[1].clone();
let filename = &args[2].clone();
Config {
query: query.to_string(),
filename: filename.to_string(),
}
}
실행 결과는 다음과 같습니다. 앞에 같은 결과가 나왔습니다.
❯ cargo run the poem.txt
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.77s
Running `target/debug/minigrep the poem.txt`
검색어: the
대상 파일 이름: poem.txt
읽은 파일 내용:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
So now that the purpose of the parse_config function is to create a Config instance, we can change parse_config from a plain function to a function named new that is associated with the Config struct.
Rust does not provide constructors, but a common idiom is to create a new() static method, also called an associated function:
use std::env;
use std::fs;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("검색어: {}", config.query);
println!("대상 파일 이름: {}", config.filename);
let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = &args[1].clone();
let filename = &args[2].clone();
Config {
query: query.to_string(),
filename: filename.to_string(),
}
}
}
실행 결과는 다음과 같습니다. 앞에 같은 결과가 나왔습니다.
❯ cargo run the poem.txt
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.03s
Running `target/debug/minigrep the poem.txt`
검색어: the
대상 파일 이름: poem.txt
읽은 파일 내용:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
지금까지 우리는 cargo run the poem.txt
이라고 실행습니다. 그런데 단순하게 cargo run
이라고 실행하면 어떨가요? 한 번 해보겠습니다.
❯ cargo run
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.49s
Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:22:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
에러가 발생했습니다. 그러나 앞의 에러는 명확하지 않으니 Config
에 args.len()
을 사용하여 인수를 받은 백터의 길이를 확인하여 이에 따라 구체적인 에러를 메세지를 출력하도록 코드를 다음과 같이 수정해 봤습니다.
use std::env;
use std::fs;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("검색어: {}", config.query);
println!("대상 파일 이름: {}", config.filename);
let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("인수 숫자가 충분하지 않습니다.");
}
let query = &args[1].clone();
let filename = &args[2].clone();
Config {
query: query.to_string(),
filename: filename.to_string(),
}
}
}
실행 결과는 다음과 같습니다. 여전히 에러가 발생했지만, 우리가 의도한 바대로 에러 메세지가 출력되었습니다.
❯ cargo run
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep`
thread 'main' panicked at '인수 숫자가 충분하지 않습니다.', src/main.rs:23:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
그러나 이러한 방법은 코드를 짜는 사람에게 유용한 것이지, 이 프로그램을 사용하는 사용자 입장에서는 유용하지 않습니다. 이를 사용자에 맞게 바꿔 보겠습니다. 9자에서 배운 Result
타입을 이용하겠습니다.
use std::env;
use std::fs;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("검색어: {}", config.query);
println!("대상 파일 이름: {}", config.filename);
let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Result<Config, &str> {
if args.len() < 3 {
return Err("필요한 인수를 입력하지 않았습니다.");
}
let query = &args[1].clone();
let filename = &args[2].clone();
Ok(Config {
query: query.to_string(),
filename: filename.to_string(),
})
}
}
실행 결과는 다음과 같습니다. 그러나 Config
의 리턴값이 Result<Config, &str>
인데, 여기서 config.filename
과 같이 값을 뽑아낼 수 없기 때문에 에러가 발생하고 있습니다.
❯ cargo run
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
error[E0609]: no field `query` on type `Result<Config, &str>`
--> src/main.rs:8:32
|
8 | println!("검색어: {}", config.query);
| ^^^^^
error[E0609]: no field `filename` on type `Result<Config, &str>`
--> src/main.rs:9:37
|
9 | println!("대상 파일 이름: {}", config.filename);
| ^^^^^^^^
error[E0609]: no field `filename` on type `Result<Config, &str>`
--> src/main.rs:11:46
|
11 | let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
| ^^^^^^^^
error: aborting due to 3 previous errors
For more information about this error, try `rustc --explain E0609`.
error: could not compile `minigrep`
이럴 해결하기 위해서 let config
부분을 에러를 처리할 수 있게 바꾸고, process::exit()
을 이용하고자 이 합니다. process::exit()
는 에러가 난 경우 프로그램을 즉시 멈추고(stop), 퇴당(exit) 상태 코드로 전달된 숫자를 반환합니다.
use std::env;
use std::fs;
use std::process;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!(
"입력한 명령어에서 인수들을 파싱하는데 다음과 같은 문제가 발생했습니다: {}",
err
);
process::exit(1);
});
println!("검색어: {}", config.query);
println!("대상 파일 이름: {}", config.filename);
let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Result<Config, &str> {
if args.len() < 3 {
return Err("필요한 인수를 입력하지 않았습니다.");
}
let query = &args[1].clone();
let filename = &args[2].clone();
Ok(Config {
query: query.to_string(),
filename: filename.to_string(),
})
}
}
실행 결과는 다음과 같습니다. 에러를 발생하지 않았고 우리가 의도한 바대로 사용자에게 에러 메세지가 출력되었습니다.
❯ cargo run
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/minigrep`
입력한 명령어에서 인수들을 파싱하는데 다음과 같은 문제가 발생했습니다: 필요한 인수를 입력하지 않았습니다.
우선 파일을 읽는 부분을 main()
에서 빼보겠습니다. run()
가 이 부분을 담당하게 되며, 인수로 Config
를 갖게 됩니다.
use std::env;
use std::fs;
use std::process;
fn main() {
//println!("Hello, world!");
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!(
"입력한 명령어에서 인수들을 파싱하는데 다음과 같은 문제가 발생했습니다: {}",
err
);
process::exit(1);
});
println!("검색어: {}", config.query);
println!("대상 파일 이름: {}", config.filename);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.filename).expect("파읽을 읽을 수 없습니다.");
println!("읽은 파일 내용: \n {}", contents)
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Result<Config, &str> {
if args.len() < 3 {
return Err("필요한 인수를 입력하지 않았습니다.");
}
let query = &args[1].clone();
let filename = &args[2].clone();
Ok(Config {
query: query.to_string(),
filename: filename.to_string(),
})
}
}
윗 코드를 실행하면 결과는 다음과 같습니다. 문제없이 돌아갑니다.
❯ cargo run the poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minigrep the poem.txt`
검색어: the
대상 파일 이름: poem.txt
읽은 파일 내용:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
인수를 잘못 넣은 에러도 앞와 같게 처리하고 있습니다.
❯ cargo run
Compiling minigrep v0.1.0 (/Users/jaehwan/git/rust/projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/minigrep`
입력한 명령어에서 인수들을 파싱하는데 다음과 같은 문제가 발생했습니다: 필요한 인수를 입력하지 않았습니다.