Skip to content

Instantly share code, notes, and snippets.

@dialektike
Created August 21, 2021 08:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dialektike/f323c75d42934053bb23cd490a230179 to your computer and use it in GitHub Desktop.
Save dialektike/f323c75d42934053bb23cd490a230179 to your computer and use it in GitHub Desktop.
12 장 I/O 프로젝트: 명령줄(Command Line) 프로그램 작성하기

I/O 프로젝트: 명령줄(Command Line) 프로그램 작성하기

명령줄 인수들(Arguments) 처리하기

프로젝트 생성합니다. 그리고 프로젝트 폴더로 이동합니다.

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)할 것이다.

  1. 모든 것을 main 함수에서 하고 있다. 여러 함수로 나눠야 한다.
  2. 1번과 묶여 있는 문제인데, main 함수가 길어질수록 더 많은 변수를 선언해야 하는데, 범위 안에서 우리가 가진 변수가 많으면 많을 수록, 그 변수들의 각각의 목적의 노선을 유지하는 것이 어렵다(the more variables we have in scope, the harder it will be to keep track of the purpose of each). 그래서 설정(configuration) 변수들을 하나의 구조체로 모아 그 변수들의 목적을 명확하게 만드는 것이 최상이다.
  3. 현재 파일을 열지 못했을 때, 에러 메세지가 단순하다.
  4. 모든 에러 처리 로직을 한 곳으로 모으면 사용자에게 더 의미있는 에러 메세지를 출력할 수 있다.

바이너리 프로젝트를 위한 관심 분리(separation)

  • 여러분의 프로그램을 main.rslib.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!

설정 값들을 그룹화(Grouping)

그러나 앞에서는 여전히 개별 변수에 대입하고 있다. 이는 아직까지 적절한 추상화(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!

Config을 위한 Constructor를 생성하기

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!

에러 '다루는 법(handling)'을 수선하기

지금까지 우리는 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

에러가 발생했습니다. 그러나 앞의 에러는 명확하지 않으니 Configargs.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()에서 로직을 분리하기

우선 파일을 읽는 부분을 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`
입력한 명령어에서 인수들을 파싱하는데 다음과 같은 문제가 발생했습니다: 필요한 인수를 입력하지 않았습니다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment