12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rustで自作シェルもどきを作る(字句解析編)

Last updated at Posted at 2014-12-20

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固有であってもそうでなくても、より良い書き方などありましたらご教授いただけると幸いです。よろしくお願いします。

parse.rs
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
    }
}
main.rs
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);
        }
    }
}
12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?