概要
複数のコマンドを同時にwatchしたかったのでvhwatchというコマンドをGoで作った
使い方
vhwatch 'ls -1' 'echo test' 'date'
vhwatch -c 1 'ls -1' 'echo test' 'date'
vhwatch -c 3 'echo 1' 'echo 2' 'echo 3' 'echo 4' 'echo 5' 'echo 6' 'echo 7'
インストール
go get -u github.com/jiro4989/vhwatch
あるいはGitHub Releaseからバイナリをダウンロード
実装
ロジックはわりと単純で、引数(実行させたいコマンド文字列)の数だけ
分割スクリーンの矩形構造体を生成して、逐次コマンドを実行してその結果を画面に描画しているだけです。
コマンド実行はそのうち並列実行にする予定です。
画面描画には https://github.com/nsf/termbox-go を使用しています。
func NewPanes(c, tw, th int, cmds []string) (ret Panes) {
cmdLen := len(cmds)
for i, cmd := range cmds {
pw := paneWidth(c, tw) // 1ペインあたりの幅
ph := paneHeight(c, th, cmdLen) // 1ペインあたりの幅
x := paneX(c, pw, i)
y := paneY(c, ph, i)
// ペインの下のペインが空いていたら縦幅を拡張
h := ph
if cmdLen-1 < i+c && (cmdLen-1)/c != i/c {
h += ph
}
p := Pane{
Name: cmd,
X: x,
Y: y,
Width: pw,
Height: h,
Command: cmd,
}
ret = append(ret, p)
}
return
}
func paneWidth(c, w int) int {
return w / c
}
func paneHeight(c, h, cnt int) int {
mod := cnt % c
div := cnt / c
if 0 < mod {
div++
}
return h / div
}
func paneX(c, w, i int) int {
return w * (i % c)
}
func paneY(c, h, i int) int {
return h * (i / c)
}
コマンドラインオプションパーサーには https://github.com/spf13/cobra を使用しています。
func init() {
cobra.OnInitialize()
RootCommand.Flags().IntP("col", "c", 2, "column count")
RootCommand.Flags().IntP("interval", "n", 2, "seconds to wait between updates")
}
var RootCommand = &cobra.Command{
Use: "vhwatch",
Short: "vhwatch is Vertical/Horizontal Watch",
Example: "vhwatch -c 3 'echo test' 'date' 'ls -1' 'ls -lah'",
Version: Version,
Long: `
vhwatch provides watching multiple commands execution.
Repository: https://github.com/jiro4989/vhwatch
Author: jiro4989
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Help()
return
}
f := cmd.Flags()
col, err := f.GetInt("col")
if err != nil {
panic(err)
}
interval, err := f.GetInt("interval")
if err != nil {
panic(err)
}
// termboxの初期化
if err := termbox.Init(); err != nil {
panic(err)
}
defer termbox.Close()
termbox.SetInputMode(termbox.InputEsc)
termbox.Flush()
// 各ペイン毎にコマンドを定期実行
go mainloop(col, args, time.Duration(interval))
// Ctrl-Cで終了されるまで待機
waitKeyInput()
},
}
func mainloop(col int, args []string, interval time.Duration) {
const fc = termbox.ColorDefault
const bc = termbox.ColorDefault
for {
// 端末の幅を取得
w, h := termbox.Size()
// コマンドを描画するペインを取得
panes := NewPanes(col, w, h, args)
for _, p := range panes {
var out []byte
c, err := shellwords.Parse(p.Command)
if err != nil {
panic(err)
}
switch len(c) {
case 0:
// 空の文字列が渡された場合
return
case 1:
// コマンドのみを渡された場合
out, err = exec.Command(c[0]).Output()
default:
// コマンド+オプションを渡された場合
// オプションは可変長でexec.Commandに渡す
out, err = exec.Command(c[0], c[1:]...).Output()
}
if err != nil {
panic(err)
}
p.DrawHeader()
p.DrawText(out, Offset{Y: 1}, fc, bc)
}
termbox.Flush()
time.Sleep(interval * time.Second)
termbox.Clear(fc, bc)
}
}
func waitKeyInput() {
for {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
switch ev.Key {
case termbox.KeyCtrlC, termbox.KeyCtrlD:
return
}
}
}
}
実装の問題点
メモリ消費
普通に逐次実行してその結果をメモリに全部蓄えていて一気に描画してるので
コマンド実行結果が巨大だとメモリ消費がヤバそう。
この辺もうちょい省メモリに並列でいい感じにしたい。
マルチバイト文字がずれる
echo ああ
とかをvhwatchしたらずれた・・・
近いうちになおします。
直しました。
https://github.com/jiro4989/vhwatch/issues/1
まとめ
わりと以前から自分が欲しかった機能なので、
ドッグフーディングしつつ改善していこうと思ってます。
(screenとかのコマンドをいちいち使うのは面倒)
もし使ってくださる方がいらっしゃって
バグとか機能要望ありましたらissuesまでお願いします