rust-shell - 解説

実装の詳細

プロセス生成の流れ

1. ユーザー入力を読み取り
2. コマンドをパース
3. 組み込みコマンドか判定
   - Yes: シェル内で直接実行
   - No: 外部コマンドを実行
4. 外部コマンドの実行
   - fork() で子プロセス生成
   - リダイレクト設定
   - exec() でプログラム実行
5. 親プロセスで wait()
6. 終了ステータスを記録

std::process::Command

// Rust の Command は fork/exec を抽象化
let output = Command::new("ls")
    .arg("-la")
    .current_dir("/tmp")
    .env("MY_VAR", "value")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::inherit())
    .output()?;

println!("stdout: {}", String::from_utf8_lossy(&output.stdout));

パイプラインの実装

コマンド: ls | grep foo | wc -l

┌────┐ stdout  ┌──────┐ stdout  ┌────┐
│ ls │ ──────→ │ grep │ ──────→ │ wc │ → stdout
└────┘  pipe   └──────┘  pipe   └────┘

// パイプの作成と接続
let mut prev_stdout = None;

for cmd in commands {
    let mut process = Command::new(&cmd.program);

    // 前のコマンドの出力を入力に接続
    if let Some(stdout) = prev_stdout.take() {
        process.stdin(Stdio::from(stdout));
    }

    // 次のコマンド用に出力をパイプに
    process.stdout(Stdio::piped());

    let mut child = process.spawn()?;
    prev_stdout = child.stdout.take();
}

よくある間違い

1. 組み込みコマンドの子プロセス実行

// 間違い: cd を子プロセスで実行
Command::new("cd").arg("/tmp").status();
// → 子プロセスのディレクトリが変わるだけで親には影響なし

// 正しい: シェルプロセス内で実行
env::set_current_dir("/tmp")?;

2. ゾンビプロセス

// 間違い: wait しない
let child = Command::new("sleep").arg("1").spawn()?;
// child は drop されても wait されない → ゾンビ

// 正しい: 明示的に wait
let mut child = Command::new("sleep").arg("1").spawn()?;
child.wait()?;

3. ファイルディスクリプタのリーク

// 間違い: リダイレクト用ファイルを閉じない
let file = File::create("out.txt")?;
process.stdout(Stdio::from(file));
// file は move されるので問題ないが、他の FD は注意

// パイプでは使わない側を閉じる必要がある

パフォーマンス考慮事項

fork() のコスト

// fork() は仮想メモリをコピー(CoW)
// 大きなプロセスでは一時的にメモリ使用増加

// 最適化: vfork() や posix_spawn()
// Rust の Command は内部で最適化を行う

パイプのバッファ

// パイプにはカーネルバッファがある(通常64KB)
// バッファが満杯になると write() がブロック
// バッファが空だと read() がブロック

// デッドロック例:
// 大量出力するコマンドの stdout を読まないと
// パイプバッファが満杯 → コマンドがブロック

発展トピック

シグナル処理

use signal_hook::{consts::SIGINT, iterator::Signals};

// Ctrl+C のハンドリング
let mut signals = Signals::new(&[SIGINT])?;

thread::spawn(move || {
    for sig in signals.forever() {
        match sig {
            SIGINT => {
                // 現在の子プロセスに SIGINT を送信
                // または無視
            }
            _ => {}
        }
    }
});

ジョブ制御

// バックグラウンド実行
struct Job {
    pid: u32,
    command: String,
    status: JobStatus,
}

enum JobStatus {
    Running,
    Stopped,
    Done,
}

// & でバックグラウンド実行
if line.ends_with('&') {
    let child = command.spawn()?;
    jobs.push(Job {
        pid: child.id(),
        command: line.to_string(),
        status: JobStatus::Running,
    });
    // wait せずに続行
}

ヒストリー

use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};

struct History {
    entries: Vec<String>,
    file_path: PathBuf,
}

impl History {
    fn add(&mut self, command: &str) {
        self.entries.push(command.to_string());
        // ファイルに追記
        if let Ok(mut file) = OpenOptions::new()
            .append(true)
            .create(true)
            .open(&self.file_path)
        {
            writeln!(file, "{}", command).ok();
        }
    }

    fn get(&self, index: usize) -> Option<&str> {
        self.entries.get(index).map(|s| s.as_str())
    }
}