字句解析
先日友人とご飯を食べてるときにシェルの話になって,「シェル作るときって字句解析がメンドくさいよねー」って話になりました.シェルというと,fork だの dup だの pipe だのがミソみたいな感じですけど,個人的に一番メンドい印象があるのが字句解析だったりします.UNIX C プログラミングとかにもシェルの簡易実装例があるんですけど,そのソースはこんな感じで,空白をデリミタとして分割するだけの形になっています.
void parse( char *buf, char **args ) { while ( *buf != NULL ) { while ((*buf == ' ') || (*buf == '\t')) *buf++; *args++ = buf; while ((*buf != NULL) && (*buf != ' ') && (*buf != '\t')) buf++; } }
しかし実際には,クオートがあったり,エスケープがあったりといろいろメンドい.でも,boost の tokenizer 使えばかなり楽になるんじゃね?とか思ったので,ちょっと実装してみることにした.
tokenizer
Boost の tokenizer は,その名の通り,文字列からトークンを切り出すことができる.これ一つで,なんかわけわからんけどクオートもエスケープもよしなに処理してくれるから,ホントにすばらしい!
実際にエスケープまで処理させる場合は,TokenizerFunction として escaped_list_separator を使う必要があります.Tokenizer は以下のようになっていますけど,char_delimiters_separator の代わりに escaped_list_separator を使えば良い.
template < typename TokenizerFunc = char_delimiters_separator<char>, typename Iterator = std::string::const_iterator, typename Type = std::string > class tokenizer
デリミタはスペースとタブ,クオートをするのはシングルクオテーションとダブルクオテーションというように,複数指定してやらないといけない場合は,次のコンストラクタを使います.
escaped_list_separator(string_type e, string_type c, string_type q)
簡単に字句解析部分のみを作るとこんな感じ.
#include <iostream> #include <boost/tokenizer.hpp> #include <string> int main() { typedef boost::tokenizer< boost::escaped_list_separator<char> > lexer_t; boost::escaped_list_separator<char> els( "\\", " \t", "\"\'" ); std::string s = "echo 'hello world'"; lexer_t lexer( s, els ); for( lexer_t::iterator beg = lexer.begin(); beg != lexer.end(); ++beg ) { std::cout << *beg << std::endl; } return 0; }
実行すると,確かにクオテーションが処理されている...
さぁシェルだ!
今回はシェルを一つのクラスとして作ったので,宣言は次のようになりました(shell.h).
#include <vector> #include <iostream> #include <boost/tokenizer.hpp> #include <string> class Shell { typedef boost::escaped_list_separator<char> separator_t; typedef boost::tokenizer< separator_t > tokenizer_t; typedef tokenizer_t::iterator iterator; typedef tokenizer_t::const_iterator const_iterator; typedef std::vector< std::string > strvec_t; private: std::string str; char *args[64]; separator_t separator; tokenizer_t tokenizer; bool is_background; iterator begin() { return tokenizer.begin(); } iterator end() { return tokenizer.end(); } void tokenize( const std::string& str ); void reset_flag(); public: Shell() : separator( "\\", " \t", "\"\'" ), tokenizer( str, separator ), is_background( false ) { } virtual ~Shell() { } void execute( const std::string& str ); void prompt() { std::cout << "> " << std::flush; } };
今のところ,バックグラウンド実行くらいしかサポートしてなくて,リダイレクトもパイプも使えない.
字句解析部分は tokenize メソッドとしています.tokenizer クラスに文字列を assign し,そこからトークンを切り出します.tokenize の中では,切り出したトークンを exec に渡せるように char のポインタ配列の形に整形するところまで行うようにしました.Vector にトークンを入れているのはムダだけど,もうそういうのはムシだ.
void Shell::tokenize( const std::string& str ) { strvec_t vec; tokenizer.assign( str ); for ( iterator it = begin(); it != end(); ++it ) { if ( it->at( it->length() - 1 ) == '&' ) { is_background = true; } else if ( ! it->empty() ) { vec.push_back( std::string( *it ) ); } } int i = 0; for ( strvec_t::const_iterator cit = vec.begin(); cit != vec.end(); ++cit ) { args[i++] = const_cast< char* >( cit->c_str() ); } args[i] = NULL; }
実際にコマンドを実行するのは execute メソッドで,fork で子プロセスを作って exec を呼び出すという,まぁこのあたりは定型な感じですね.
void Shell::execute( const std::string& str ) { tokenize( str ); int pid, status; if ( (pid = fork() ) < 0 ) { perror( "fork" ); exit( 1 ); } if ( pid == 0 ) { execvp( *args, args ); perror( *args ); exit( 1 ); } if ( ! is_background ) { while( wait( &status) != pid ) ; } reset_flag(); }
main 関数は無限ループだ!!!! シグナル処理とかもムシだ!!!!!!
int main() { Shell shell; std::string line; while( 1 ) { shell.prompt(); getline( std::cin, line ); shell.execute( line ); } return 0; }