kdnakt blog

hello there.

Rust dojo第74回を開催した

第74回です。前回はこちら。

kdnakt.hatenablog.com

[第74回の様子]

2023/04/12に第74回を開催した。

内容としてはRust By Example 日本語版20. 標準ライブラリのその他の「20.4.2. create」、「20.4.3. read lines」、「20.5. 子プロセス」、「20.5.1. パイプ」、「20.5.2. dropの延期」、「20.6. ファイルシステムとのやり取り」に取り組んだ。
参加者は自分を入れて4人。今回もOSがらみの処理が多かったためか、ブラウザ上でコードを実行できなかったのでサクサクと読み進めるだけになってしまった...。

[学んだこと]

  • 20.4.2. create
  • std::fs::File::create()関数はファイルを書き込み専用モードで開く
    • すでにファイルが存在している場合、破棄して新しい物を作成する
    • この動きはちょっと恐ろしい...
    • create_new()というnightlyビルドの関数があり、こちらはすでにファイルがある場合はエラーを返す:こちらの方が良さそう...早く正式版になってほしい
    • 似たような関数として、OpenOptions.create_new()があり、こちらは普通のビルドでも利用できるっぽい
  • create()関数を使ってファイルに書き込む例は以下のようになる
static LOREM_IPSUM: &str =
    "Lorem ipsum dolor sit amet, ...(略)...";

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    let path = Path::new("lorem_ipsum.txt");
    let display = path.display();

    // 返り値は`io::Result<File>`
    let mut file = match File::create(&path) {
        Err(why) => panic!("couldn't create {}: {}", display, why),
        Ok(file) => file,
    };

    // 返り値は`io::Result<()>`
    match file.write_all(LOREM_IPSUM.as_bytes()) {
        Err(why) => panic!("couldn't write to {}: {}", display, why),
        Ok(_) => println!("successfully wrote to {}", display),
    }
}
  • 20.4.3. read lines
  • 先週見たFile.open()してread_to_string()でString型にファイルの中身を全て読み込むと、メモリを多く消費することになり非効率である
  • std::io::BufReaderを使うとファイルを1行ずつ読み込むイテレータを取得でき効率的である
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;

fn main() {
    if let Ok(lines) = read_lines("./hosts") {
        // Optional<String>が返ってくる
        for line in lines {
            if let Ok(ip) = line {
                println!("{}", ip);
            }
        }
    }
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}
  • 効率はいいのかもしれないが、read_lines()の戻り値の型をさらっと書ける気がしない...
  • 20.5. 子プロセス
  • Rustで子プロセスを起動する方法は以下の通り
use std::process::Command;

fn main() {
    // 子プロセスを起動する
    let output = Command::new("rustc")
        .arg("--version")
        .output().unwrap_or_else(|e| {
            panic!("failed to execute process: {}", e)
    });

    // std::process::Output構造体は終了したプロセスのアウトプットを保持する
    if output.status.success() {
        // 出力がUTF8でない場合に、無効な文字を自動的にU+FFFD(�)に置き換えてくれる
        let s = String::from_utf8_lossy(&output.stdout);

        print!("rustc succeeded and stdout was:\n{}", s);
    } else {
        let s = String::from_utf8_lossy(&output.stderr);

        print!("rustc failed and stderr was:\n{}", s);
    }
}
  • プロセスの出力一つとってもシンプルにString::from()では扱えない...OSに近い領域は大変だ...
  • 20.5.1. パイプ
  • Command::new().output()で出力を得たが、Command::new().spawn()を利用すると、プロセスにアクセスできるようになる
use std::error::Error;
use std::io::prelude::*;
use std::process::{Command, Stdio};

static PANGRAM: &'static str =
"the quick brown fox jumped over the lazy dog\n";

fn main() {
    // wcコマンドを起動
    let process = match Command::new("wc")
                                .stdin(Stdio::piped())
                                .stdout(Stdio::piped())
                                .spawn() {
        Err(why) => panic!("couldn't spawn wc: {}", why),
        Ok(process) => process,
    };

    // wcの標準入力に文字列を書き込む
    match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
        Err(why) => panic!("couldn't write to wc stdin: {}", why),
        Ok(_) => println!("sent pangram to wc"),
    }

    // stdinここでdropされ、パイプがcloseされwcが送られた値の処理を開始する

    // 出力を読み込む
    let mut s = String::new();
    match process.stdout.unwrap().read_to_string(&mut s) {
        Err(why) => panic!("couldn't read wc stdout: {}", why),
        Ok(_) => print!("wc responded with:\n{}", s),
    }
    // 出力結果は以下のようになる(Macで実行)
    // wc responded with:
    //        1       9      45
}
  • 20.5.2. dropの延期
  • spawn()したプロセスの終了を待つ場合は以下のようにwait()を呼び出す必要がある
use std::process::Command;

fn main() {
    let mut child = Command::new("sleep").arg("5").spawn().unwrap();
    // wait()の戻り値はprocess::ExitStatus
    let _result = child.wait().unwrap(); // sleepの終了を5秒待機する

    println!("reached end of main");
}
use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
use std::os::unix;
use std::path::Path;

// catの簡易実装
fn cat(path: &Path) -> io::Result<String> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

// echoの簡易実装
fn echo(s: &str, path: &Path) -> io::Result<()> {
    let mut f = File::create(path)?;

    f.write_all(s.as_bytes())
}

// touchコマンドの簡易実装(すでにファイルが存在しても無視する)
fn touch(path: &Path) -> io::Result<()> {
    match OpenOptions::new().create(true).write(true).open(path) {
        Ok(_) => Ok(()),
        Err(e) => Err(e),
    }
}

fn main() {
    println!("`mkdir a`");
    
    // ディレクトリを作成
    match fs::create_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(_) => {},
    }

    println!("`echo hello > a/b.txt`");
    // unwrap_or_else()メソッドを用いてmatchを簡略化できる。
    echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`mkdir -p a/c/d`");
    // 再帰的にディレクトリ作成
    fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`touch a/c/e.txt`");
    touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`ln -s ../b.txt a/c/b.txt`");
    // Unixでのみシンボリックリンクを作成
    if cfg!(target_family = "unix") {
        unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
            println!("! {:?}", why.kind());
        });
    }

    println!("`cat a/c/b.txt`");
    match cat(&Path::new("a/c/b.txt")) {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(s) => println!("> {}", s),
    }

    println!("`ls a`");
    // ディレクトリの読込:返り値はio::Result<Vec<Path>>
    match fs::read_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(paths) => for path in paths {
            println!("> {:?}", path.unwrap().path());
        },
    }

    println!("`rm a/c/e.txt`");
    // ファイルを削除
    fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`rmdir a/c/d`");
    // 空のディレクトリを削除
    fs::remove_dir("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });
}

[まとめ]

モブプログラミングスタイルでRust dojoを開催した。
もうちょっとシンプルなAPIを期待したいんだけど、OSに近い部分だとどうしても複雑にならざるを得ないんだろうな...。

今週はプルリクエストなし。