Top
Photo Drawings Software Writing Reading Memo Study Profile Bookmark 
Amethyst DumLambda PetitLambda Yamanba JSharp JFlat OOPS yash KumonoIto ML Toys 

Yamato Shijimi

'Yamato Shijimi' means 'Yamatodani's Small Shell'.
'Yamato Shijimi' is abbreviated to 'YASH'.

目次

  1. 機能
  2. セットアップ
  3. 実装

機能

  1. フォアグラウンド/バックグラウンド実行
  2. パイプ/リダイレクト
  3. "(command)"によるサブシェルでの実行
  4. いくつかの組み込みコマンド/組み込み変数
  5. "`command`"によるコマンド置換
  6. ジョブの停止/再開
これらの機能を用いて、次のようなコマンド実行が可能である。

また、自分で実装したものではないのだが、readlineライブラリを使用し ているのでキーバインド設定、ファイル名補完などが可能となってい ることを補足しておく。

組み込みコマンド

現時点では以下のコマンドを用意している。

cd
カレントディレクトリを変更する。
export
環境変数を設定する。
exit
yashを終了する。
jobs
ジョブの情報を表示する。
printenv
環境変数を出力する。
fg
番号で指定したジョブをフォアグラウンドにする。
bg
停止しているジョブを番号で指定し、バックグラウンドで実行する。
abort
実行時エラーをわざと発生させる。

組み込み変数

現時点では、以下の組み込み変数が使用できる。
$?
直前にフォアグラウンド実行したプロセスの終了コード
$$
プロセスID

おもな”未実装”機能

組み込みコマンドのリダイレクト

既知の不具合

手書きパーサバージョンでは、コマンドの先頭にでたらめな変数名を指定すると異常終了する。
例)
      > $DADADA
      Segmentaion fault
      

Flex・Bisonバージョンではこのバグは修正されている。

障害ではないが、コマンドの置換処理でリストや文字列の複製を繰り返しおこなっているのが問題。 たとえば

echo a
と実行しただけでmallocを55回呼び出している。この様子はdeclarations.hMEMORY_DEBUG#defineしてコンパイルすれば確認できる。

セットアップ

ファイル

手書きパーサを使用したバージョンと、FlexおよびBisonを使用したバージョンの二種類がある。 手書きパーサを使用したバージョンにはバグが残っているが、更新しない。 今後はFlexおよびBisonを使用したバージョンを更新していく。
パッケージ
yash-2.0.tar.gz(手書きパーサ使用)
yash-3.0.tar.gz(Flex Bison 使用)
ソースのみ
Version 2.0
yash.2.0.c(ver. 2.0のソース)
Version 3.0
yash.c
parser.y
token.l
declarations.h

インストール

上記のパッケージファイルをダウンロードし、 多くのGNUソフトと同様の手順でインストールする。 ./configureでprefixのデフォルトは/usr/local。 prefixで指定されたディレクトリにbin,infoのディレクトリが作成され、 そこにyashとyash.infoがコピーされる。
    gtar zxf yash-2.0.tar.gz
    cd yash-2.0
    ./configure --prefix=INSTALL_DIR
    gmake
    gmake install
      

インストール時の問題点

infoファイルのインストール方法

infoファイルをインストールさせる方法が不明。 オプションなしでinfoコマンドで起動した際に表示される一覧にyashも表示させたい。 make install時にinstall-infoを起動させれ ばよいようだがどこに記述すべきか不明。

アンインストール

インストール時にtarballを展開して作成されたディレクトリに移動し、
    gmake uninstall
      
を実行する。

実装

YASHを構成する関数は大きく分けて以下の5種類に分類される。
コマンド解析
コマンド実行
ジョブ管理
ユーザインターフェイス
組み込みコマンド

プログラム構造

コマンドライン構文

YASHが受け付けるコマンドラインの構文は以下の通り。
  oneline  ::= EOL | command EOL                       -- 一回のコマンド入力
  command  ::= job (jobcon job)* jobcon?               -- Jobの集合
  jobcon   ::= `;` | `&`                               -- Jobの同期/非同期の指示
  job      ::= process (`|` process)*                  -- Job controlの対象
  process  ::= (strlist | '(' command ')' ) redirect*  -- execの対象
  redirect ::= `>` str | `<` str | '>>' str            -- リダイレクト
  strlist  ::= str str*                                -- strの列(解析時にベクトル化する)
  str      ::= [^ \t\n><;&]+                           -- 文字列
           |   '"' [^"]* '"'                           -- 二重引用符で囲まれた文字列
           |   ''' [^"]* '''                           -- 一重引用符で囲まれた文字列
           |   '`' [^"]* '`'                           -- バッククォートで囲まれた文字列
      
command -> job -> process の階層構造に対応して、データ構造、解析関数、 実行関数を構成している。

データ構造

Command,Job,Processに対応してそれらの構文解析情報、実行情報を保持 する構造体を定義している。
Redirectに関する情報を保持する構造体も定義している。
typedef enum e_EIODir{/* リダイレクトの種類を指す定数 */
  EIODir_IN,             /* 入力リダイレクト */
  EIODir_OUT,            /* 出力(上書き)リダイレクト */
  EIODir_APPEND          /* 出力(追加)リダイレクト */
}EIODir;

struct t_Redirect{
  int fileno;            /* リダイレクトの対象となるファイル記述子 */
  Str filename;          /* リダイレクト先のファイル名 */
  EIODir dir;            /* 入力/出力(上書き)/出力(追加)の区別 */
};

struct t_Process{
  pid_t pid;             /* プロセスID */
  int  subshell;         /* サブシェルの場合,0以外。それ以外ならば0。*/
  union{
    StrList* args;       /* subshell==0の場合:exec時の引数 */
    Command* subcmd;     /* subsehll!=0の場合:サブシェルで実行するコマンド */
  }u;
  List* redirects;       /* Redirect構造体のリスト */
  Job* job;              /* このプロセスを含むJob */
};

struct t_Job{
  List* ps;              /* Processのリスト */
  int bg;                /* バックグラウンド実行の場合 1;フォアグラウンド実行の場合 0 */
  int activechildren;    /* 実行中のProcessの数 */
  int jobid;             /* ジョブ番号 */
  char* text;            /* このジョブを起動した入力文字列 */
};

struct t_Command{
  List* js;              /* 最初のJob */
};

    
たとえば、入力が
export TAR=gtar; (cd src;$TAR c yash | gzip) > tmp/yashsrc.tgz &
であった場合に生成されるオブジェクト構造は以下のようになる。

コマンド解析

(*)このセクションの記述は手書きパーサを使用したバージョンについてのみ対象とする。
入力されたコマンドを再帰下降型解析により解析する。
上記の文法のCommand,Job,Processに対応して解析関数parseCommand,parseJob,parseProcess を定義している。(そのほか、リダイレクト、文字列、文字列の並び、特殊記号を解析する関数も定義している。)
これらの解析関数は以下のParseResult構造体によって解析情報を受け渡す。

struct t_ParseResult{
  int status;            /* 解析成功/失敗を示すフラグ */
  char* lastpos;         /* 解析成功時,解析した範囲の直後を指す。 */
  union {
    /* 各構文要素に対応するデータへのポインタ */
    Str str;
    StrList* strlist;
    Redirect* redirect;
    Process* proc;
    Job *job;
    Command* cmd;
    char* message;        /* 解析エラー時にメッセージが設定される。 */
  }u;
};
    

各解析関数は、文法構造に沿って、生成規則を構成する文法記号に対応する解析関数を呼び出して解析を進める。
たとえば文法上でJobはパイプ記号で区切られたProcessの一つ以上の並びと定義されているので、 parseJobProcessを解析するparseProcessとパイプ記号を解析するparseTermを呼び出すことにより自身の機能を実現している。

ParseResult parseJob(char* pos){
  char* bp = pos;
  ParseResult s,res;

  /* 最初のProcessを解析する。 */
  if(PARSEFAILED(s = parseProcess(pos))){
    PARSEERROR(s);
  }
  /* Job構造体を生成する。 */
  res.u.job = allocJob(s.u.proc);
  
  /* パイプで結ばれたProcessの並びを解析する。 */
  while(1){
    pos = s.lastpos;
    if(PARSEFAILED(s = parseTerm(s.lastpos, PIPE)))
      break;
    if(PARSEFAILED(s = parseProcess(s.lastpos))){
      /* parse error */
      freeJob(res.u.job);
      PARSEERROR(res);
    }
    addProcessToJob(res.u.job, s.u.proc);
  }
  res.lastpos = pos;
  res.u.job->text = ystrndup(bp,pos - bp);
  PARSESUCCESS(res);
}

このコード例から分かるように、各解析関数はPARSESUCCESSまたはPARSEERRORマクロにより上位の解析関数に解析の成功または失敗を通知し、呼び出し側はPARSEFAILEDマクロによって下位の解析関数の解析の成否を判別している。

実行

Command,Job,Processのデータ構造に対応してそれらの実行を受け持つ関数 execCommand,execJob,execProcess を実装している。
パイプ/リダイレクトの処理はおもにexecProcess中でおこなっている。

ジョブ制御

ジョブ制御に関しては、現時点ではバックグラウンドジョブが終了した際に状態を表示する機能しか実装していない。ジョブの停止/再開は実装していない。
フォアグラウンドジョブを実行する場合
各子プロセスをforkした直後に、以下のコードのように、それらに対してwaitpidを呼び出し、すべてが終了するのを待っている。

  /* フォアグラウンド実行の場合,Jobに含まれるすべてのProcessが終了するまで待つ。 */
  if(0 == job->bg){
    item = job->ps->top;
    while(item){
      proc = (Process*)item->data;
      if(proc->pid){
	waitpid(proc->pid,&status,0);
	/* 子プロセスが終了ではなく停止した場合はどうする? */
      }
      item = item->next;
    }
    /* 実行中ジョブリストから削除する。 */
    removeItemFromList(ActiveJobs,jitem,(void (*)(void*))freeJob);
  }
    
バックグラウンドジョブを実行する場合
コマンドライン入力を受け付ける度に、 checkBGJobStatusを呼び出し、新たに終了したバックグラウンドジョブがないかをチェックしている。終了したジョブを検出した場合、その情報をリストに追加しておき、コマンド入力受付直後に出力している。


  while(0 < (childpid = waitpid(-1,&childstatus, WNOHANG))){
    /* 終了した子プロセスに対応するProcess構造体を探す。 */

	    (略)

    if(proc->pid == childpid){
      if(WIFEXITED(childstatus) || WIFSIGNALED(childstatus)){
        /*
          プロセスが終了した場合
        */
        proc->pid = 0;
        if(0 == --job->activechildren){
        /*
          Job終了の通知メッセージをポストする。
        */
          notifyJobStatusChange(job);
        }
      }
    }
  }

ユーザインターフェイス

getInput関数においてコマンド入力を受け付ける。入力を受け取った後、checkBGJobStatusを呼び出して状態が変化したジョブの検出を試み、printJobStatusChangeを呼び出してその情報を出力する。

組み込みコマンド

組み込みコマンドを、以下のようなコマンド名と関数ポインタを関連づけるテーブルによって管理している。

typedef void (*BuiltInFunc)(StrVec,FILE*,FILE*,FILE*);
typedef struct t_BuiltIn BuiltIn;
struct t_BuiltIn{
  char* name;
  BuiltInFunc func;
} ;

/* 組み込みコマンドのテーブル */
BuiltIn BuiltIns[] = {{"exit",builtinExit},
		      {"jobs",builtinJobs},
		      {"export",builtinExport},
		      {"printenv",builtinPrintEnv},
		      {"cd",builtinCd},
		      {NULL,NULL}};


組み込みコマンドを実装する関数はいずれも
void builtinCOMMANDNAME(StrVec,FILE*,FILE*,FILE*)
というシグネチャをもつ。StrVecchar**typedefした別名である。三つのFILE*は、標準入力、標準出力、標準エラー出力として使用するファイルが指定される。
たとえばcdコマンドを実装するbuiltinCdは以下のように実装している。

void builtinCd(StrVec args,FILE* infile,FILE* outfile,FILE* errfile){
  char* target;
  if(args[1]){
    target = args[1];
  }else{
    target = getenv("HOME");
  }
  if(chdir(target)){
      perror("");
  }
}

参考

Linux Debian パッケージに含まれる各種シェルのソース(とくにkiss)
『プログラミングLinux』(アスキー)