🐕‍🦺

The Catcher in the Cli / CLIでつかまえて

2021/11/24に公開

これは何?

標準出力、標準入力、標準エラー出力をキャッチするOSS。どれをキャッチしてどれをキャッチしないかも設定できる。

リンクはこちら
https://github.com/maru44/catcher-in-the-cli

由来はサリンジャーの"The Catcher in the rye" (ライ麦畑でつかまえて)
攻殻機動隊が大好きなので小説嫌いな自分でも頑張って読んだ作品

使い方

https://github.com/maru44/catcher-in-the-cli/tree/master/_sample

_sample/main.go
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/maru44/catcher-in-the-cli"
)

func main() {
	ctx := context.Background()

	c := catcher.GenerateCatcher(
		&catcher.Settings{
			Interval: 4000,
			Repeat:   catcher.IntPtr(2),
		},
	)

	go func() {
		select {
		case <-time.After(500 * time.Millisecond):
			fmt.Println("bbb")
			fmt.Println("ccc")

			fmt.Fprintln(os.Stderr, "ddddd")
		}
	}()

	c.CatchWithCtx(ctx, writeFile)
}

func writeFile(ts []*catcher.Caught) {
	f, _ := os.OpenFile("./_sample/log.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
	defer f.Close()

	for _, t := range ts {
		f.Write([]byte(t.String() + "\n"))
	}
}

まずGenerateCatcherで初期化して、CatchまたはCatchWithCtxメソッドで利用します。
go run _sample/main.goを実行した結果が以下になります。

標準出力

標準出力
aaa
ddddd
exec: "aaa": executable file not found in $PATH
bbb
ccc
ls
LICENSE
README.md
_sample
_sample2
catcher.go
caught.go
domain.go
go.mod
tools.go

保存したファイル

log.log
Output: bbb
Output: ccc
Input: aaa
Error: ddddd
Error: exec: "aaa": executable file not found in $PATH
Output: LICENSE
Output: README.md
Output: _sample
Output: _sample2
Output: catcher.go
Output: caught.go
Output: domain.go
Output: go.mod
Output: tools.go
Input: ls

log.SetOutput() で十分?
私も正直そう思う

作ってみたかったからつくったのだ
そしたら現時点ではあまり実用性のないものが出来上がってしまった(改良すれば使えるようになるかも???)

何をしているか

メインの処理

contextが終了し、stdin, stdout, stderrを確認し全て終了していたら、catcherを指定のseparatorで分解する。
分解したものを引数に指定した関数で煮るなり焼くなり好きにしてくれという感じ。
この関数をCatchWithCtxCatchメソッドでラップしており、それを外部から呼び出して使う。

catcher.go
func (c *catcher) catch(ctx context.Context, ch chan string, f func(cs []*Caught)) {
	c.Times++
	localCtx, cancel := context.WithTimeout(ctx, time.Millisecond*time.Duration(c.Interval))
	defer cancel()

	chOut := make(chan bool)
	chIn := make(chan bool)
	chError := make(chan bool)

	if c.OutBulk != nil {
		go c.catchStdout(localCtx, chOut)
	}
	if c.InBulk != nil {
		go c.catchStdin(localCtx, chIn)
	}
	if c.ErrorBulk != nil {
		go c.catchStderr(localCtx, chError)
	}

	for {
		select {
		case <-localCtx.Done():
			for {
				if c.IsOver(chOut, chIn, chError) {
					cs := c.Separate()
					f(cs)
					c.Reset()
					c.repeat(ch, c.Times)
					return
				}
			}
		case <-ctx.Done():
			for {
				if c.IsOver(chOut, chIn, chError) {
					cs := c.Separate()
					f(cs)
					c.Reset()
					c.repeat(ch, c.Times)
					return
				}
			}
		}
	}
}

stdout (stderr)

stderrもほぼ同様

io.Readerから読み取ったものを一時保存用のbufferと標準出力に書き込む
contextが終了したらcatcherOutBulkフィールドに保存し、チャネルにtrueを送信。
os.Stdoutを元にもどして、終了させる

catcher.go
func (c *catcher) catchStdout(ctx context.Context, ch chan bool) {
	r, w, err := os.Pipe()
	if err != nil {
		panic(err)
	}
	stdout := os.Stdout
	os.Stdout = w

	for {
		select {
		case <-ctx.Done():
			w.Close()

			var buf bytes.Buffer
			mw := io.MultiWriter(stdout, &buf)
			io.Copy(mw, r)

			c.OutBulk.Text = buf.String()

			os.Stdout = stdout // restore stdout
			ch <- true
			return
		}
	}
}

stdin

bufio.Scannerで標準入力を受け取っている

catcher.go
func (c *catcher) catchStdin(ctx context.Context, ch chan bool) {
	scan := bufio.NewScanner(os.Stdin)

	go func() {
		select {
		case <-ctx.Done():
			ch <- true
			return
		}
	}()

	for scan.Scan() {
		c.InBulk.Text += scan.Text() + c.Separator
		com := strings.Split(scan.Text(), " ")
		out, err := exec.Command(com[0], com[1:]...).Output()
		if err != nil {
			fmt.Fprint(os.Stderr, err, c.Separator)
		} else {
			fmt.Print(string(out), c.Separator)
		}
	}
}

とまあこんな感じ。
catcherIntervalRepeatを設定して繰り返し処理にしているのはbufferに無限に堆積しないようにこのようにしてみた

最後に

良かったら使ってください

GitHubで編集を提案

Discussion