# パイプってなに? - シェルもどきを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でパイプを実現できた。