# パイプってなに? - シェルもどきをgoで自作する #10 ## おさらい これまで - [シェルってなに?コマンドラインインタプリタってなに? - シェルもどきを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) - [構文解析と抽象構文木 - シェルもどきをgoで自作する #9](https://hackmd.io/@jyami/ByrK1ajIK) シェルもどき「[oreshell](https://github.com/jyami/oreshell)」を自作している。 前回はリダイレクションの実装をした。 今回はパイプの実装、動作確認。 ## パイプ(Pipe)とは > Unix系オペレーティングシステムのパイプ(pipe)、もしくはパイプライン (pipeline) とは、複数のプログラムの入出力をつなぐための仕組み(プロセス間通信)の一つである。独立した複数のプログラムを組み合わせる事で、多様かつ複雑なデータ処理を効率よく柔軟に実行できる利点がある。また、現有のソフトウエア資産の再利用が可能になるため、プログラム生産性の面でも利点もある。 > [wikipedia](https://ja.wikipedia.org/wiki/%E3%83%91%E3%82%A4%E3%83%97_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF)) ### パイプの例 ``` $ ls | grep ab | sort ``` 上記のパイプライン例のイメージ図は以下の通り。 ![](https://i.imgur.com/3YZkUNw.png) 上記のパイプライン例では以下の処理を行っている。 1. 「ls」の実行結果が標準出力から出てパイプに入り、 1. そのパイプから「ls」の実行結果が出て「grep ab」の標準入力に入り、 1. 「grep ab」の実行結果が標準出力から出て次のパイプに入り、 1. そのパイプから「grep ab」の実行結果が出て「sort」の標準入力に入り、 1. 「sort」の実行結果が標準出力から出る。 ![](https://i.imgur.com/unnOCOX.png) 上記の「ls」「grep ab」「sort」プロセスはすべて並列で実行し、各プロセスは「入力されたデータを処理して出力する」を逐次行う。 「ls」の実行が終わってから「grep ab」を起動するわけではない。「ls」が少し実行して少し出力するとそれを「grep ab」がパイプ経由で受け取って処理して出力する。 「カレントディレクトリにあるファイル名の一覧をある条件で取得したい」と思ったときに、それ専用のプログラムを書かなくても、上記パイプラインコマンドを実行するだけで取得することができる。 ### パイプの実現方法 Unix系OSの場合はpipeシステムコールが用意されているのでそれを使って実現するのが一般的らしい。([Linuxのpipeシステムコール](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/pipe.2.html)) pipeシステムコールを呼ぶと、「パイプに書き込むための口」と「パイプから読み出すための口」のファイルディスクリプタを取得できる。 goでは[os.Pipe()](https://pkg.go.dev/os#Pipe)があるので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)にリダイレクションの構文を書いたのでそれを拡張する。 oreshellのパイプの構文は以下の通り。 ``` pipeline_sequence ::= simple_command ( '|' simple_command )* ``` railroad diagram ![](https://i.imgur.com/2anmhqI.png) 「simple_command」を含めて全部くっつけるとこんな感じ。 ![](https://i.imgur.com/4D03kUN.png) ## 今回の抽象構文木 [以前](https://hackmd.io/x_5T7LrjRkKKQkiSkYdqpw?view#%E4%BB%8A%E5%9B%9E%E3%81%AE%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8)に抽象構文木を書いたのでそれを拡張する。 ```plantuml node pipeline_sequence node SimpleCommand1 node SimpleCommand2 card コマンド名 node CommandSuffix collections コマンド引数 collections リダイレクション群 card 入力または出力方向 card ファイルディスクリプタ番号 card ファイルパス pipeline_sequence -- SimpleCommand1 pipeline_sequence -- SimpleCommand2 SimpleCommand1 -- コマンド名 SimpleCommand1 -- CommandSuffix CommandSuffix -- コマンド引数 CommandSuffix -- リダイレクション群 リダイレクション群 -- 入力または出力方向 リダイレクション群 -- ファイルディスクリプタ番号 リダイレクション群 -- ファイルパス ``` ## 今回の実装 パイプライン対応による変更点のみ説明。 ### 字句解析 解析対象の字句として「|」を追加する(説明は省略)。 ### 構文解析 構文に従って字句解析トークン群から抽象構文木を作成する(説明は省略)。 ### 評価 変更点の内、重要な箇所をピックアップして以下に説明。 #### 評価用パイプライン/プロセスを表す構造体の定義 ```go= package process (略) type Pipe struct { reader *os.File writer *os.File } type Process struct { command string argv []string stdin *os.File stdout *os.File redirections *[]ast.Redirection previous *Process next *Process pipe *Pipe osProcess *os.Process } ``` ```go= package process (略) type PipelineSequence struct { processes []*Process } ``` 上記の構造体を図に表すと以下の通り。 ```plantuml node PipelineSequence node Process1 node Process2 card コマンド名 collections コマンド名とコマンド引数群 card "★標準入力" as stdin card "★標準出力" as stdout collections リダイレクション群 card "★前のプロセス" as previous card "★次のプロセス" as next node "★パイプ" as pipe card "★パイプから読み込むための口" as reader card "★パイプに書き込むための口" as writer card OSプロセスハンドル PipelineSequence -- Process1 PipelineSequence --- Process2 Process1 -- コマンド名 Process1 -- コマンド名とコマンド引数群 Process1 --- stdin Process1 --- stdout Process1 --- リダイレクション群 Process1 --- previous Process1 --- next Process1 -- pipe pipe -- reader pipe -- writer Process1 -- OSプロセスハンドル ``` 「★」がついている要素はパイプライン対応で使う要素である。 これらについてはあとで説明する。 #### 評価用パイプライン/プロセス構造体の作成 抽象構文木(ast.PipelineSequence)から 1. 評価用パイプライン構造体(process.PipelineSequence)を作成し、 2. それにぶら下がる評価用プロセス構造体(process.Process)群を作成し、 3. プロセス間のパイプを設定する。 ```go= package process (略) func NewPipelineSequence(pipelineSequence *ast.PipelineSequence) (me *PipelineSequence, err error) { me = &PipelineSequence{} for _, sc := range pipelineSequence.SimpleCommands { p, err := NewProcess(sc) if err != nil { return nil, err } log.Logger.Printf("Process New %+v\n", p) me.processes = append(me.processes, p) } log.Logger.Printf("processes size %d\n", me.size()) for i, p := range me.processes { //プロセス数が1つの場合はパイプ設定対象外 //プロセスが複数でも末尾のプロセスはパイプ設定対象外。 if i+1 < me.size() { log.Logger.Printf("pipe %+v\n", p) err = p.PipeWithNext(me.processes[i+1]) if err != nil { return nil, err } } } return me, nil } ``` #### プロセス間のパイプを設定 process.Process#PipeWithNext()でプロセスと次のプロセスとの間にパイプを作成し、 - プロセスの標準出力と「パイプに書き込むための口」をつなげる - 「パイプから読み込むための口」と次のプロセスの標準入力をつなげる を行う。 また、プロセスと次のプロセスの前後関係を設定する。 ```go= package process (略) func (me *Process) PipeWithNext(next *Process) (err error) { me.pipe, err = newPipe() if err != nil { return err } me.stdout = me.pipe.writer next.stdin = me.pipe.reader me.next = next next.previous = me return nil } ``` [ソースコード](https://github.com/jyami/oreshell/commits/v0.5) ## 実行例 ``` (ore) > ls abc.txt abc2.txt ac.txt (ore) > ls | grep ab | sort abc.txt abc2.txt ``` oreshellでパイプを実現できた。