理系学生日記

おまえはいつまで学生気分なのか

シェルを作ってみた

字句解析

先日友人とご飯を食べてるときにシェルの話になって,「シェル作るときって字句解析がメンドくさいよねー」って話になりました.シェルというと,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++;
  }
}

ISBN:4-7561-0078-3:detail

しかし実際には,クオートがあったり,エスケープがあったりといろいろメンドい.でも,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;
}