Rustが最近とてもおもしろいので、勉強がてら自作シェルみたいなものを作ってみたいと思います。
とはいえ、C言語でさえろくにシステムプログラムを書いたことがないので、道は険しくなりそうです。
バージョン
0.13.0-nightlyを使用しました。
コード全文はこちら
agatan/rsh
字句解析
さて、シェルといったらまずはユーザの入力を受け付けてパースし、コマンドを実行しなくてはなりません。
というわけでまず初めにパース部分についてやってみます。
さくっと終わらせたかったのですが、どうもまだStringと&strとかそのへんで詰まってしまいます...
Tokenの規定
enumを使ってTokenを列挙します。
一応最終的にはパイプやらリダイレクトやらも実装したいなーと思っているので、その辺を考慮に入れた実装にしてみました。
enum Token {
Str(String),
Pipe,
RedirectTo,
RedirectFrom,
Ampersand,
}
Strは特殊な文字以外の文字列ですから、要素としてStringを保持させておきました。
parser
パースには(おもしろそうだったので)iteratorトレイトを実装させることにしました。
実際つかうときにはいらない気もしますが、ちょっとためしたかったので。
構造体としてParserを作ります。ソースとなる文字列と、現在どこまでパース済みなのかを保持するcurrentを持たせました。
pub struct Parser {
src: String,
pub current: uint,
}
この構造体にIteratorトレイトを実装すればよいのですが、補助関数としていくつか実装しておきます。
impl Parser {
pub fn new(src: String) -> Parser {
Parser { src: src, current: 0 }
}
pub fn current_char(&self) -> char {
self.src.char_at(self.current)
}
fn skip_whitespace(&mut self) {
while self.current_char().is_whitespace() {
self.current += 1;
if self.current >= self.src.char_len() {
return;
}
}
}
fn get_pipe(&mut self) -> Option<Token> {
if self.current_char() == '|' {
self.current += 1;
self.skip_whitespace();
Some(Token::Pipe)
} else {
None
}
}
fn get_ampersand(&mut self) -> Option<Token> {
if self.current_char() == '&' {
self.current += 1;
self.skip_whitespace();
Some(Token::Ampersand)
} else {
None
}
}
fn get_redirect_to(&mut self) -> Option<Token> {
if self.current_char() == '>' {
self.current += 1;
self.skip_whitespace();
Some(Token::RedirectTo)
} else {
None
}
}
fn get_redirect_from(&mut self) -> Option<Token> {
if self.current_char() == '<' {
self.current += 1;
self.skip_whitespace();
Some(Token::RedirectFrom)
fn get_str(&mut self) -> Option<Token> {
let mut i = self.current;
while !KEYWORDS.contains_char(self.src.char_at(i)) {
i += 1;
}
if i == self.current {
None
} else {
let result = Some(Token::Str(self.src.slice_chars(self.current, i).to_string()));
self.current = i;
self.skip_whitespace();
result
}
}
}
newは新しいパーサーを生成するための関数です。Parserは文字列の所有権を要求していますが、入力された文字列はパースする以外に使い道は無いと思ったので大丈夫と判断しました。
current_charは現在注目している文字を返します。
skip_whitespaceは空白文字を飛ばすようにself.currentをいじります。パーサーでは、基本的に1トークンを読み終えたら空白を飛ばして次のトークンになりうる文字の先頭までジャンプするべきなので、必ずトークンを読んだらこの関数を呼び出します。
のこりの関数は、それぞれのTokenを取得することを試みる関数です。
Option<Token>が帰ってくるので、Noneが帰ってきたら今みているトークンは別の種類のものであるといえます。
これらを用いてIteratorを実装しました。
impl std::iter::Iterator<Token> for Parser {
fn next(&mut self) -> Option<Token> {
if self.current >= self.src.char_len() {
return None;
}
let mut result: Option<Token> = self.get_pipe();
if result.is_some() { return result; }
result = self.get_ampersand();
if result.is_some() { return result; }
result = self.get_redirect_to();
if result.is_some() { return result; }
result = self.get_redirect_from();
if result.is_some() { return result; }
result = self.get_str();
if result.is_some() { return result; }
None
}
}
きれいじゃないコードですが、うまいやり方が他に思いつかなかったので...
純粋にある種類のトークンを取得しようと試みてNoneが帰ってきたら別の種類で試す、ということを繰り返しています。
先頭で末尾まで読み込んだかを判定しています。
すべての条件に当てはまらなくなったらパース失敗でNoneを返しています。
試す
use std::io;
mod parse;
fn main() {
loop {
let input = std::io::stdin().read_line().ok().expect("Failed to read.");
let mut parser = parse::Parser::new(input);
for token in parser {
println!("{}", token);
}
}
}
実行結果
ls -a | grep foo
Str(ls)
Str(-a)
Pipe
Str(grep)
Str(foo)
ls -a| grep foo>result.txt &
Str(ls)
Str(-a)
Pipe
Str(grep)
Str(foo)
RedirectTo
Str(result.txt)
Ampersand
このような感じになりました。
Iteratorを実装しているので、for .. in ..が使えて気持ち良いです。
反省点
パースは失敗しうる計算だからOptionかなーどうせOptionかえすならIterator実装しちゃえばお得かなーとおもって漠然と実装してみましたが、パースは失敗した理由がほしいことがほとんどなのでよく考えたらResultを使うべきだった気がしてきました。
効率とかは正直Rustでの効率のよい書き方がよくわかっていないのであまり気にせず、とりあえず動くものを、と作ってみました。
あとはパーサを書いたことが殆どなかったので成功法がわからなかったので、もっときれいな書き方があるんじゃないかという気も...
今後
とりあえず動くものを、コードをたくさん書こう、の精神で進めてみます。
次は単純なコマンド実行を実装したいです。
といってもRustにはCommandとかProcessとかがあって、ちょっと読んで見た感じ割りと素直にC言語のexecvpとかを呼び出しているようなので、それを使えばそこまで難しくはないのかな?
ソースの全文を掲載しますので、Rust固有であってもそうでなくても、より良い書き方などありましたらご教授いただけると幸いです。よろしくお願いします。
use std;
static KEYWORDS: &'static str = "|&<> \n";
# [deriving(Show)]
pub enum Token {
Str(String),
Pipe,
RedirectTo,
RedirectFrom,
Ampersand,
}
pub struct Parser {
src: String,
pub current: uint,
}
impl Parser {
pub fn new(src: String) -> Parser {
Parser { src: src, current: 0 }
}
pub fn current_char(&self) -> char {
self.src.char_at(self.current)
}
fn skip_whitespace(&mut self) {
while self.current_char().is_whitespace() {
self.current += 1;
if self.current >= self.src.char_len() {
return;
}
}
}
fn get_pipe(&mut self) -> Option<Token> {
if self.current_char() == '|' {
self.current += 1;
self.skip_whitespace();
Some(Token::Pipe)
} else {
None
}
}
fn get_ampersand(&mut self) -> Option<Token> {
if self.current_char() == '&' {
self.current += 1;
self.skip_whitespace();
Some(Token::Ampersand)
} else {
None
}
}
fn get_redirect_to(&mut self) -> Option<Token> {
if self.current_char() == '>' {
self.current += 1;
self.skip_whitespace();
Some(Token::RedirectTo)
} else {
None
}
}
fn get_redirect_from(&mut self) -> Option<Token> {
if self.current_char() == '<' {
self.current += 1;
self.skip_whitespace();
Some(Token::RedirectFrom)
} else {
None
}
}
fn get_str(&mut self) -> Option<Token> {
let mut i = self.current;
while !KEYWORDS.contains_char(self.src.char_at(i)) {
i += 1;
}
if i == self.current {
None
} else {
let result = Some(Token::Str(self.src.slice_chars(self.current, i).to_string()));
self.current = i;
self.skip_whitespace();
result
}
}
}
impl std::iter::Iterator<Token> for Parser {
fn next(&mut self) -> Option<Token> {
if self.current >= self.src.char_len() {
return None;
}
let mut result: Option<Token> = self.get_pipe();
if result.is_some() { return result; }
result = self.get_ampersand();
if result.is_some() { return result; }
result = self.get_redirect_to();
if result.is_some() { return result; }
result = self.get_redirect_from();
if result.is_some() { return result; }
result = self.get_str();
if result.is_some() { return result; }
None
}
}
use std::io;
mod parse;
fn main() {
loop {
let input = std::io::stdin().read_line().ok().expect("Failed to read.");
let mut parser = parse::Parser::new(input);
for token in parser {
println!("{}", token);
}
}
}