The Catcher in the Cli / CLIでつかまえて
これは何?
標準出力、標準入力、標準エラー出力をキャッチするOSS。どれをキャッチしてどれをキャッチしないかも設定できる。
リンクはこちら
由来はサリンジャーの"The Catcher in the rye" (ライ麦畑でつかまえて)
攻殻機動隊が大好きなので小説嫌いな自分でも頑張って読んだ作品
使い方
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
保存したファイル
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で分解する。
分解したものを引数に指定した関数で煮るなり焼くなり好きにしてくれという感じ。
この関数をCatchWithCtx
とCatch
メソッドでラップしており、それを外部から呼び出して使う。
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
が終了したらcatcher
のOutBulk
フィールドに保存し、チャネルにtrue
を送信。
os.Stdout
を元にもどして、終了させる
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で標準入力を受け取っている
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)
}
}
}
とまあこんな感じ。
catcher
にInterval
とRepeat
を設定して繰り返し処理にしているのはbufferに無限に堆積しないようにこのようにしてみた
最後に
良かったら使ってください
Discussion