🦀

[Rust] 引数の有無で read 対象(ファイル・標準入力)を切り替える

に公開

概要

Rustで作成したプログラムの動作を、以下のようにしたかった。

  • 引数にファイルが指定されている場合、ファイルの内容を読み込む。
  • 引数にファイルが指定されていない場合、標準入力の内容を読み込む。

上記の動作を実現した方法を記載します。

使用するプログラム

説明では、ファイルの内容を表示し、行頭に行数を表示するプログラムを使用します。
説明で使用するプログラムのソースコードは、以下にあります。
https://github.com/shu-kitamura/BufRead_sample

プログラムの引数にファイル名が指定されている場合、ファイルの内容を表示します。

$ cargo run test.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/bufread_sample test.txt`
1: aaa
2: bbb
3: ccc
4: ddd
5: eee
6: fff

プログラムの引数に何も指定されていない場合は、標準入力の内容を表示します。

$ echo -e "aaa\nbbb\nccc\nddd\neee\nfff" | cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/bufread_sample`
1: aaa
2: bbb
3: ccc
4: ddd
5: eee
6: fff

実装内容

引数を受け取り、引数の数が

  • 1 より大きい場合、ファイルから読み込む。
  • 1 以下の場合、標準入力から読み込む。

とすることで、概要にて記載した動作を実現した。

use std::{
    env::args,
    fs::File,
    io::{BufRead, BufReader, Stdin, stdin},
};

fn main() {
    // 引数を受け取る
    let args: Vec<String> = args().collect();

    if args.len() > 1 {
        // ファイルを read する処理
        let file: File = match File::open(&args[1]) {
            Ok(f) => f,
            Err(e) => panic!("{e}")
        };
        let br: BufReader<File> = BufReader::new(file);
        read_line(br);
    } else {
        // 標準入力を read する処理
        let stdin: Stdin = stdin();
        let br: BufReader<Stdin> = BufReader::new(stdin);
        read_line(br);
    }
}

/// ファイル・標準入力の内容を、行数ともに表示する関数。
fn read_line<T: BufRead>(br: T) {
    for (i, result) in br.lines().enumerate() {
        match result {
            Ok(line) => println!("{}: {}", i+1, line),
            Err(e) => panic!("{e}")
        }
    }
}

苦戦したところ

ファイルから read する処理標準入力から read する処理の共通部分を関数にまとめるのに苦戦しました。
上記の処理は、それぞれ以下のように実装していました。

  • ファイルから read する処理

            let file: File = match File::open(&args[1]){
                Ok(f) => f,
                Err(e) => panic!("{e}")
            };
    
            let br: BufReader<File> = BufReader::new(file);
    
            for (i, result) in br.lines().enumerate() {
                match result {
                    Ok(line) => println!("{}: {}", i+1, line),
                    Err(e) => panic!("{e}")
                }
            }
    
  • 標準入力から read する処理

            let stdin: Stdin = stdin();
            let br: BufReader<Stdin> = BufReader::new(stdin);
    
            for (i, result) in br.lines().enumerate() {
                match result {
                    Ok(line) => println!("{}: {}", i+1, line),
                    Err(e) => panic!("{e}")
                }
            }
    

ファイル・標準入力のどちらの場合でも、以下の処理は共通しています。
これでは処理内容が重複しているため、DRY原則に反しています。

    for (i, result) in br.lines().enumerate() {
        match result {
            Ok(line) => println!("{}: {}", i+1, line),
            Err(e) => panic!("{e}")
        }
    }

重複している処理を関数にしたかったです。
ファイル・標準入力のどちらの処理でも、BufReaderを使用しているため、
以下のように、引数にBufReaderをとる関数を考えましたが、コンパイルエラーになりました。

fn read_line(br: BufReader) {
    for (i, result) in br.lines().enumerate() {
        match result {
            Ok(line) => println!("{}: {}", i+1, line),
            Err(e) => panic!("{e}")
        }
    }
}

上記のように関数を定義する場合、引数にBufReader<T>と型を指定する必要があります。
以下のようにBufReader<File>とすることでコンパイルエラーは解消できます。

fn read_line(br: BufReader<File>) {
    for (i, result) in br.lines().enumerate() {
        match result {
            Ok(line) => println!("{}: {}", i+1, line),
            Err(e) => panic!("{e}")
        }
    }
}

しかし、引数をBufReader<File>とするとBufReader<Stdin>を引数に受け取ることができません。(逆も然りです)
そのため、BufReader<File>, BufReader<Stdin>のどちらにも対応できる関数にはなりません。

BufReaderは、BufReadトレイトを実装しているため、ジェネリクスを使って、以下のように定義することで実現できました。

fn read_line<T: BufRead>(br: T) {
    for (i, result) in br.lines().enumerate() {
        match result {
            Ok(line) => println!("{}: {}", i+1, line),
            Err(e) => panic!("{e}")
        }
    }
}

トレイトとジェネリクスを理解していれば、苦戦もなかったと思います。
今回の苦戦を通して、これらの理解が深まったと思います。

Discussion