shiimaxx's blog

最も愛を大切に

インタラクティブにテキスト処理を実行できるツールを作った - txtmanip

GitHub - shiimaxx/txtmanip

外部コマンドを利用したテキスト処理をインタラクティブに実行することができます。
ログ集計などで試行錯誤したり、ワンライナーを作る練習として使えると思います。

デモ

実行している様子は次のとおりです。

catで標準出力に出力したcombined形式のアクセスログの内容をパイプで渡し、インタラクティブモード内でawk '{print $7}'sortuniq -csort -nrhead -5を実行してリクエスト数上位5件のURLを出力しています。
インタラクティブモードを終了すると、インタラクティブモード中に実行したコマンドをパイプで繋げたワンライナーが出力されます。これを実行することで最終的な結果と同じ内容のものを表示することができます。

設定ファイル

TOML形式の設定ファイルに設定を記述します。
現状は、enable_commandsという項目にインタラクティブモード中に実行するコマンドリストを設定することができます。リストにないコマンドは実行することができません。

enable_commands = [
    "awk",
    "cut",
    "grep",
    "head",
    "sed",
    "sort",
    "tail",
    "uniq",
    "wc",
]

実装について

termbox-go

テキストベースのユーザインタフェースを実現するために、GitHub - nsf/termbox-go を使っています。

termbox-goではSetCell で位置と文字(rune)を指定してどこになにを描画するかを定義します。これを手続き的に書いていくと非常に分かりづらいコードになってしまうため、そうならないように表示する内容と描画する処理を分けて書くようにしました。

MainViewとそれに含まれるInputArea(ユーザの入力が表示される部分)とTextArea(テキスト操作の結果が表示される部分)をそれぞれ構造体として定義しています。表示する内容はそれぞれのフィールドの値として保持します。

// MainView represent main view
type MainView struct {
    textArea  TextArea
    inputArea InputArea
    height    int
    width     int
}

...

// InputArea represent input area
type InputArea struct {
    text             []byte
    error            []byte
    cursorPos        int
    cursorInitialPos int
    prompt           []byte
    history          []string
    historyPos       int
}

...

// TextArea represent text area
type TextArea struct {
    text    []byte
    history []string
}

描画は各構造体に実装した描画用のメソッドで実行するようにしました。描画用のメソッドはフィールドに保持している値をもとにSetCellを実行していきます。 以下はTextAreaの描画用メソッドです。

func (v *MainView) DrawTextArea() {
    y := TextAreaPos
    x := 0
    for _, t := range v.textArea.text {
        if t == byte('\n') {
            y++
            x = 0
            continue
        }
        termbox.SetCell(x, y, rune(t), ColFg, ColBg)
        x++
    }
}

また、MainViewFlush()メソッドをで、描画用メソッドをまとめて実行するようにしています。
これをイベント(キーの入力)ごとに実行することで、都度画面の表示内容を更新しています。

func (v *MainView) Flush() error {
    if err := termbox.Clear(ColBg, ColBg); err != nil {
        return err
    }

    termbox.SetCursor(v.inputArea.cursorPos, InputAreaPos)
    v.DrawBorderLine()
    v.DrawInputArea()
    v.DrawInputError()
    v.DrawTextArea()

    return termbox.Flush()
}

課題

元となるテキストやヒストリーをすべて構造体のフィールドの値として保持しており、メモリ効率がよくないと感じています。
また、マルチバイト文字に対応していないので、今後このあたりを改善していこうと思っています。

まとめ

何かありましたらIssue、Pull Requestをいただけると嬉しいです。