コード全文はこちら
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
がうまくやってくれているみたいですね。すごい。
今後
次はリダイレクト関連ですかね。パイプや&よりも簡単そうだし。
毎度のことですが、ご指摘大歓迎です!色々な意見を伺いたくてこれを書いているので、何かありましたらぜひよろしくお願いします!