Try   HackMD

パイプってなに? - シェルもどきをgoで自作する #10

おさらい

これまで

シェルもどき「oreshell」を自作している。
前回はリダイレクションの実装をした。

今回はパイプの実装、動作確認。

パイプ(Pipe)とは

Unix系オペレーティングシステムのパイプ(pipe)、もしくはパイプライン (pipeline) とは、複数のプログラムの入出力をつなぐための仕組み(プロセス間通信)の一つである。独立した複数のプログラムを組み合わせる事で、多様かつ複雑なデータ処理を効率よく柔軟に実行できる利点がある。また、現有のソフトウエア資産の再利用が可能になるため、プログラム生産性の面でも利点もある。
wikipedia

パイプの例

$ ls | grep ab | sort

上記のパイプライン例のイメージ図は以下の通り。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

上記のパイプライン例では以下の処理を行っている。

  1. 「ls」の実行結果が標準出力から出てパイプに入り、
  2. そのパイプから「ls」の実行結果が出て「grep ab」の標準入力に入り、
  3. 「grep ab」の実行結果が標準出力から出て次のパイプに入り、
  4. そのパイプから「grep ab」の実行結果が出て「sort」の標準入力に入り、
  5. 「sort」の実行結果が標準出力から出る。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

上記の「ls」「grep ab」「sort」プロセスはすべて並列で実行し、各プロセスは「入力されたデータを処理して出力する」を逐次行う。
「ls」の実行が終わってから「grep ab」を起動するわけではない。「ls」が少し実行して少し出力するとそれを「grep ab」がパイプ経由で受け取って処理して出力する。

「カレントディレクトリにあるファイル名の一覧をある条件で取得したい」と思ったときに、それ専用のプログラムを書かなくても、上記パイプラインコマンドを実行するだけで取得することができる。

パイプの実現方法

Unix系OSの場合はpipeシステムコールが用意されているのでそれを使って実現するのが一般的らしい。(Linuxのpipeシステムコール)
pipeシステムコールを呼ぶと、「パイプに書き込むための口」と「パイプから読み出すための口」のファイルディスクリプタを取得できる。

goではos.Pipe()があるのでoreshellではこれを使って実現する。

今回の構文

以前にリダイレクションの構文を書いたのでそれを拡張する。
oreshellのパイプの構文は以下の通り。

pipeline_sequence ::= simple_command ( '|' simple_command )*

railroad diagram

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

「simple_command」を含めて全部くっつけるとこんな感じ。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

今回の抽象構文木

以前に抽象構文木を書いたのでそれを拡張する。

        
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

今回の実装

パイプライン対応による変更点のみ説明。

字句解析

解析対象の字句として「|」を追加する(説明は省略)。

構文解析

構文に従って字句解析トークン群から抽象構文木を作成する(説明は省略)。

評価

変更点の内、重要な箇所をピックアップして以下に説明。

評価用パイプライン/プロセスを表す構造体の定義

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 }
package process () type PipelineSequence struct { processes []*Process }

上記の構造体を図に表すと以下の通り。

        
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

「★」がついている要素はパイプライン対応で使う要素である。
これらについてはあとで説明する。

評価用パイプライン/プロセス構造体の作成

抽象構文木(ast.PipelineSequence)から

  1. 評価用パイプライン構造体(process.PipelineSequence)を作成し、
  2. それにぶら下がる評価用プロセス構造体(process.Process)群を作成し、
  3. プロセス間のパイプを設定する。
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()でプロセスと次のプロセスとの間にパイプを作成し、

  • プロセスの標準出力と「パイプに書き込むための口」をつなげる
  • 「パイプから読み込むための口」と次のプロセスの標準入力をつなげる

を行う。
また、プロセスと次のプロセスの前後関係を設定する。

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 }

ソースコード

実行例

(ore) > ls
abc.txt  abc2.txt  ac.txt
(ore) > ls | grep ab | sort
abc.txt
abc2.txt

oreshellでパイプを実現できた。