コード全文はこちら
agatan/rsh
Rustで自作シェルもどきを作る(字句解析編) - Qiita
前回はユーザからの入力を受け付けてパースするところまで実装したので、今回は実際に入力されたコマンドを実行させてみます。
ただし、いきなりいろいろやるのはきついので、まずは単純なコマンド(パイプやリダイレクトは一旦無視します)の実行をやってみました。
Command
Rustにはstd::io::process::Commandという構造体があります。これを使えば簡単にコマンド実行は出来そうです。
http://doc.rust-lang.org/std/io/process/struct.Command.htmlより引用
use std::io::Command;
let mut process = match Command::new("sh").arg("-c").arg("echo hello").spawn() {
Ok(p) => p,
Err(e) => panic!("failed to execute process: {}", e),
};
let output = process.stdout.as_mut().unwrap().read_to_end();
いろいろと便利そうな関数が定義されているのですが、今回は最も単純にコマンドを実行させるために、`output`という関数を使用してみました。
```rust:exec.rs
use std;
use std::io::process::Command;
use parse::Token;
pub fn exec(tokens: Vec<Token>) {
if tokens.iter()
.find(|&t| match t {
&Token::Str(_) => false, _ => true } ).is_some() {
panic!("Not implemented yet.");
}
let first_cmd = tokens.iter()
.take_while(|&t| match t {
&Token::Str(_) => true,
_ => false
})
.map(|ref t| match *t {
&Token::Str(ref x) => x.clone(),
_ => panic!("Shouldn't be reached."),
})
.collect::<Vec<String>>();
let mut output = match Command::new(first_cmd[0].as_slice())
.args(first_cmd.as_slice().tail())
.output() {
Ok(p) => p,
Err(e) => panic!("Failed to execute: {}", e),
};
print!("{}", String::from_utf8_lossy(output.output.as_slice()));
}
execはVec<Token>を受け取る関数です。
今回はパイプなどは未実装ですから、Str(_)以外のTokenが含まれていたらpanic!(例外送出)をしています。
さて、Commandはnewで実行したいコマンド名を指定し、それに続けてargやargsで引数を追加していきます。
ユーザからの入力を利用しているので、ひとつずつしか追加できないargよりは、スライスを渡して複数追加できるargsを使いたいです。
これらにあわせてVec<Token>をVec<String>もしくはVec<&str>に変換してあげれば良さそうです。
ということでlet first_cmd = ...という長い文は、その変換を行っています。
正直まだ&とrefの関係とか所有権の問題はきちんと理解できていないというか書き方がよくわかっていないので、コンパイラに怒ってもらいながらなんとか直してみました。
コンパイルを通るように、を第一に書いていったので汚かったり効率悪かったりするかもしれません...
あとはCommandに登録していって、outputを読んであげれば完了です。
ただしoutputはバイト列を返すみたいなので、String::from_utf8_lossyを使って、文字に直してあげます。
追記(2014/12/21)
このままだと標準入力や標準出力、標準エラー出力が子プロセスと結びついておらず、仮想のパイプとつながったような状態になるようです。
そのため、標準入力から読み取るような関数を実行しようとするとそのプロセスは失敗します。
修正したのが以下のコードです。(Commandを作成する部分のみ)
let mut output = match Command::new(first_cmd[0].as_slice())
.args(first_cmd.as_slice().tail())
.stdin(StdioContainer::InheritFd(STDIN_FILENO))
.stdout(StdioContainer::InheritFd(STDOUT_FILENO))
.stderr(StdioContainer::InheritFd(STDERR_FILENO))
.spawn() {
Ok(p) => p,
Err(e) => panic!("ERROR: {}", e)
};
match output.wait() {
Ok(e) => println!("Exit: {}", e),
Err(e) => println!("Error: {}", e)
}
このように、stdoutを標準出力と関連付けることで、前のコードでのoutputから読み取って文字列に変換して出力、という手順は不要になりました。
C言語ではforkしたらwaitしろと言われて育ってきたので、とりあえずwaitを入れてみていますが、どうもwaitを入れなくてもゾンビプロセスが残っているようには見えないので謎です。
調査中です。
追記ここまで
main
先ほどのexec関数をmainで使える形にしてみます。
use std::io;
mod parse;
mod exec;
fn main() {
loop {
print!("% > ");
let input = std::io::stdin().read_line().ok().expect("Failed to read.");
let parser = parse::Parser::new(input);
let tokens: Vec<parse::Token> = parser.collect();
exec::exec(tokens);
}
}
プロンプトを表示してからユーザーの入力を受付け、パースしてVec<Token>になおしてからexecに渡しています。
実行結果
% > echo hello
hello
% > echo "hello world"
"hello world"
% >
あれ?"の扱いなんて考えてなかったのになぜかうまくいってますね。不思議。
検証用にmain.rsを書き換えて実行してみました。
% > echo hello
Str(echo)
Str(hello)
hello
% > echo "hello world"
Str(echo)
Str("hello)
Str(world")
"hello world"
% > echo "hello world"
Str(echo)
Str("hello)
Str(world")
"hello world"
% >
echoがうまくやってくれているみたいですね。すごい。
今後
次はリダイレクト関連ですかね。パイプや&よりも簡単そうだし。
毎度のことですが、ご指摘大歓迎です!色々な意見を伺いたくてこれを書いているので、何かありましたらぜひよろしくお願いします!