# 構文解析と抽象構文木 - シェルもどきをgoで自作する #9 ## おさらい これまで - [シェルってなに?コマンドラインインタプリタってなに? - シェルもどきをgoで自作する#1](https://hackmd.io/@jyami/HJzohRn2D) - [コマンドと引数の分解、環境変数PATHから探索、外部コマンドと内部コマンド - シェルもどきをgoで自作する #2](https://hackmd.io/@jyami/HyeSkkThP) - [字句解析その1 - シェルもどきをgoで自作する #3](https://hackmd.io/@jyami/Hk3bWSMQO) - [字句解析その2 - シェルもどきをgoで自作する #4](https://hackmd.io/@jyami/S1BkltxQu) - [リダイレクションってなに? - シェルもどきをgoで自作する #5](https://hackmd.io/@jyami/S113NQx8u) - [リダイレクションの種類 - シェルもどきをgoで自作する #6](https://hackmd.io/@jyami/rJ3XmClqd) - [リダイレクションの構文 - シェルもどきをgoで自作する #7](https://hackmd.io/@jyami/BJ04J2Upd) - [コマンドプロセスを作成する際のファイルディスクリプタの操作 - シェルもどきをgoで自作する #8](https://hackmd.io/@jyami/Hy7nSciMt) シェルもどき「[oreshell](https://github.com/jyami/oreshell)」を自作している。 現状のoreshellはリダイレクションを行うとエラーになるのでリダイレクションできるように修正する。 前回はファイルディスクリプタ操作部分の実装を検討した。 今回はリダイレクション対応に合わせた字句解析器の修正と「構文解析器」の実装、動作確認。 ## 字句解析器の修正 今回までに行った字句解析についての説明は[コチラ](https://hackmd.io/@jyami/Hk3bWSMQO#%E5%AD%97%E5%8F%A5%E8%A7%A3%E6%9E%90)。 リダイレクションに対応するために、リダイレクション文字、ファイルディスクリプタ番号、リダイレクション先ファイル名の切り出してトークンの羅列を作成できるように字句解析器を修正する。 修正内容の細かい説明は省略。 注意する点は以下の通り。 - コマンド引数(ファイル名など)とリダイレクション文字の間にスペースが入らない場合がある。(区切りは空白文字だけではない) - 例 ``` $ ls a b c d>list ``` 「d」「>」「list」の間に空白はないがそれぞれ別トークンとなる。 - ファイルディスクリプタ番号はその次の文字がリダイレクション文字の時だけ、ファイルディスクリプタ番号として扱う。 - 例 ``` $ ls 3 2 1>list ``` 「3」「2」はコマンド引数(ここではファイル名)、「1」はファイルディスクリプタ番号としてトークンになる。 ## 「構文解析器の実装」ってあるけど「構文解析」ってなに? 一般的なインタプリタは、その入力に対して 1.字句解析 2.構文解析 3.評価 を順に処理を行う。 それぞれの処理を「字句解析器」「構文解析器」「評価器」が行う。 それぞれの出力(結果)が次の処理の入力となる。 関係を以下に図で示す。 ```plantuml file 入力 cloud 字句解析器 #palegreen;line:green;line.dashed;text:green file 字句解析結果 cloud 構文解析器 #palegreen;line:green;line.dashed;text:green file 抽象構文木 cloud 評価器 #palegreen;line:green;line.dashed;text:green file 結果 入力 -> 字句解析器 字句解析器 -> 字句解析結果 字句解析結果 -> 構文解析器 構文解析器 -> 抽象構文木 抽象構文木 -> 評価器 評価器 -> 結果 ``` 構文解析では、「字句解析の結果が定義した構文のとおりであるかどうか」を調べ、構文通りであれば「抽象構文木」と呼ばれるデータ構造を作成する。評価では、抽象構文木が表すデータ構造に基づいて実際の処理の実行を行う。 oreshellの字句解析結果の例は[コチラ](https://hackmd.io/@jyami/S1BkltxQu#%E7%B5%82%E7%AB%AF%E3%81%AE%E5%88%87%E3%82%8A%E5%87%BA%E3%81%97)。 oreshellのリダイレクションの構文は[コチラ](https://hackmd.io/sCenAHRJQuGFHW1L9hSjHA#%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E5%AF%BE%E5%BF%9C%E5%BE%8C%E3%81%AE%E6%A7%8B%E6%96%87)。 ■構文解析とは > プログラミング言語やマークアップ言語などのコンピュータ言語で書かれた文を構成要素に分解し、要素間の関係を元に特定のデータ構造に変換する操作を構文解析という。 > [e-words](https://e-words.jp/w/%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90.html) ■抽象構文木とは > 抽象構文木(英: abstract syntax tree、AST)は、通常の構文木(具象構文木あるいは解析木とも言う)から、言語の意味に関係ない情報を取り除き、意味に関係ある情報のみを取り出した(抽象した)木構造の木である。 > [wikipedia](https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8 ) ■構文木とは > 構文木(こうぶんぎ)とは、構文解析の経過や結果(またはそれら両方)を木構造で表したもの。 > [wikipedia](https://ja.wikipedia.org/wiki/%E6%A7%8B%E6%96%87%E6%9C%A8 ) ## 今回の抽象構文木 今回のoreshellリダイレクション対応では以下の図ような抽象構文木が必要になる。 ```plantuml node SimpleCommand card コマンド名 node CommandSuffix collections コマンド引数 collections リダイレクション群 card 入力または出力方向 card ファイルディスクリプタ番号 card ファイルパス SimpleCommand -- コマンド名 SimpleCommand -- CommandSuffix CommandSuffix -- コマンド引数 CommandSuffix -- リダイレクション群 リダイレクション群 -- 入力または出力方向 リダイレクション群 -- ファイルディスクリプタ番号 リダイレクション群 -- ファイルパス ``` goで書くと以下の通り。 ```go= package ast type Direction int const ( IN Direction = iota OUT ) type Redirection struct { Direction Direction FdNum int FilePath string } type CommandSuffix struct { Args []string Redirections []Redirection } type SimpleCommand struct { CommandName string CommandSuffix CommandSuffix } ``` ## 構文解析器(パーサ)の実装 リダイレクション対応するために上記のような抽象構文木を作成する構文解析器を実装する。 実装の細かい説明は省略。 今回パーサを作ってみて「こうしておいたほうが結局楽だな」と思った点は以下の通り。 - できる限りツリーの各ノード毎にパーサを個別に準備し、ツリーを下りながらパーサを再帰的に呼び出すように作る。 - 字句解析結果からトークン要素を取り出すときは、Peekできると便利。Peekとは次の要素を取り出さずに「覗き見」する処理のこと。いま手に持っている要素をどのように扱うかは次の要素の正体がわからないと決められないことがある。 ## 実装完了 字句解析、構文解析を呼び出している様子 ```go= // 入力文字列をトリム trimedL := strings.Trim(string(line), " ") if len(trimedL) == 0 { continue } // 字句解析 l := lexer.Lex(trimedL) // 構文解析 simpleCommand, err := parser.ParseSimpleCommand(l) if err != nil { fmt.Fprintln(os.Stderr, err) continue } log.Logger.Printf("simpleCommand: %v\n", simpleCommand) // 先頭の単語に該当するコマンドを探して実行する // 内部コマンドか? internalCommand, ok := internalCommands[simpleCommand.CommandName] if ok { // 内部コマンドを実行 err = internalCommand(simpleCommand) } else { // 外部コマンドを実行 err = execExternalCommand(simpleCommand) } ``` [ソースコード](https://github.com/jyami/oreshell/commits/v0.4) ## リダイレクション対応したoreshellの実行例 実装が完了したので動作確認する。 ``` $ go run main.go (ore) > echo "abc" > abc.txt (ore) > cat abc.txt abc (ore) > ls abc.txt 1>list def.txt 2>errlist (ore) > cat list abc.txt (ore) > cat errlist ls: 'def.txt' にアクセスできません: そのようなファイルやディレクトリはありません (ore) > ls abc.txt ghi.txt>list 2> errlist (ore) > cat list abc.txt (ore) > cat errlist ls: 'ghi.txt' にアクセスできません: そのようなファイルやディレクトリはありません (ore) > cat < list abc.txt (ore) > cat <abc.txt>abc2.txt (ore) > cat abc2.txt abc ```