45
16

More than 3 years have passed since last update.

jqより便利そうなTUIツールtsonが良さげな件

Posted at

ども、ゴリラです。

みなさんjqを使っていますか?とても便利なのでおそらく多くの方はjqを使っていると思います。
ぼくもその一人ですが、最近JSONをツリー状にして編集、フィルタリング、保存できたら便利では?と思い立ってtsonっていうTUIツールを作りました。
意外と便利だったので、紹介していきます。

どんな感じ?

ウホウホウ
tson-demo.gif

対応OS

Mac/Linuxのみになります。(Windowsだと画面が崩れます)
将来的にはWindowsにも対応するつもりです。

機能

便利だろうと思った機能を実装しました。次になります。

  • ファイル、URLからJSONを読み込み、ツリー化
  • フィルタリング
  • 編集、追加、削除
  • ファイルに保存
  • 外部エディタを使ってJSONを編集する

使い方

とてもシンプルです。以下の3つの方法があります。

  • ファイルから直接読み込む:tson < file.json
  • パイプ|で標準出力から読み込む:curl http://gorilla/api | tson
  • -url引数を使ってURLのレスポンスから読み込む:tson -url http://gorilla/api

URLから読み込みについて

-urlで指定したURLからJSONを読み込むことができますが、これは簡易なHTTP GETなため複雑なことはできません。
GET以外のPOSTやヘッダーをつけてリクエストを発行したいときはcurlといったコマンドを使用して|でtsonに流し込むと良いでしょう。

具体的な使い方について

tsonの便利なところはインタラクティブにJSONを編集できるところです。TUIツールならではですね。
個人的におすすめなキーバインドをいくつか紹介します。より詳細なキーバインドはこちらを参照してください。

絞り込み

/で検索ダイアログが表示されるので、検索したい文字を入力してください。
入力するたび結果が即反映されます。特に長いJSONの場合キー名を入力して絞り込みするときに便利でしょう。

image.png

折りたたみ

Hでvalueノードをすべて折りたたみ、Keyノードだけ残します。これは長いJSONの全体像を把握するのに役に立ちます。

  • 折りたたむ前
    image.png

  • 折りたたんだ後
    image.png

現在のkeyノードのみ折りたたみたい場合はhを使用します。
valueを知りたいときはlで現在のノードを展開できます。また全体を展開したいときはLを使用します。

外部エディタを使ってJSONを編集

e$EDITORに設定されているエディタを使用してJSONを編集することができます。
エディタで編集したJSONを保存して終了すると、制御はtsonに戻り編集した結果も反映されます。

  • 編集前
    image.png

  • エディタを使って編集
    image.png

  • 編集後
    image.png

ただ、エディタを使用してtsonも戻ったあとに1キーストロークが反応しなくなります。
つまり1回目に入力したキーが効かないです。これはtviewが使用しているtcellのバグのようです。
現在まだ修正されていないのとtcell作者があんまり直す気がないので、修正されるのを待つよりもこちらが直したほう早いと思いますが、tcellナニモワカラナイので、時間はかかりますが自分が頑張って治そうと思います。

ノード間ジャンプ

ctrl-j/ctrl-kでノード間のジャンプができます。子ノードの数が多く、次のノードに移動したいときに便利です。
例えば、次のようにrangeノードからindexノードにジャンプするときに使用したりします。

  • 移動前

  • 移動後

ファイルに保存

sで現在のツリー状態をファイルに保存できます。もちろん出力もJSONです。
例えばtson -url https://gorilla/apiでレスポンスを確認したあとにそれを出力したいときに役たちます。

  • 出力前

  • 出力後

実装

すこし実装について書いていきます。実際ソースコードを読んでもらった方がわかりやすいのかもしれません。
まず、JSONは以下の型が定義されています。

false/null/true/object/array/number/string

これはRFC 8259で詳しく定義されているので、興味ある方は読んでみてください。
ちなみに、numberはintfloatが含まれています。

JSONの読み込みからツリー生成

JSONからツリーを作成するときの大まかの処理の流れは次になります。

  1. JSONをGoの型にパース(json.Unmarshal()を使用)
  2. objectかarrayかliteralかで処理を分岐し、objectとarrayの中身を再帰的に処理していく

objectとarrayはarray、object、literalを持つことができるので、
どのように型を判定して適切な処理を行うのか、この再帰処理がかなり難航しました。

JSONの読み込み

JSONを読み込むときに、空インターフェイスを用意します。なぜならJSONがどんな形でも対応できるようにするためです。

b, err := ioutil.ReadAll(in)
if err != nil {
    log.Println(err)
    return nil, err
}

var i interface{}
if err := json.Unmarshal(b, &i); err != nil {
    return nil, err
}

あとはjson.UnmarshalがJSONをよしなにGoの型にしてくれます。
JSONの型に対応するGoの型は次になります。

JSON Go
string string
number int/float64
object []map[string]interface{}
array []interface{}
true true
false false
null nil

ちなみに、ツリーを作成するときにどのノードがどの型なのか、という情報を持たせる必要があります。
なぜなら、この情報はツリーをJSONに書き出すときに使用するからです。

ツリー生成

パース後のinterfaceをtypeを使って型ごとに処理を行います

switch node := node.(type) {
case map[string]interface{}:
    // objectの場合の処理
case []interface{}:
    // arrayの場合の処理
default:
    // literalの場合の処理
}

objectとarrayは基本的にt.AddNode()を使って再帰処理を行います。
それと同時に親ノードがarrayなのかobjectなのかといった情報をReferrenceというオブジェクトにセットしています。
なぜノードの型情報が必要かというと、JSONに書き出すときも再帰的にノードを辿っていきますが、
arrayobjectかによって処理が変わるので、その判定に使用します。

ちなみに、次のコードはarrayの場合の処理です。

case []interface{}:
    for _, v := range node {
        id := uuid.Must(uuid.NewV4()).String()
        switch v.(type) {
        case map[string]interface{}:
            objectNode := tview.NewTreeNode("{object}").
                SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Object})
            nodes = append(nodes, objectNode)
        case []interface{}:
            arrayNode := tview.NewTreeNode("{array}").
                SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Array})
            nodes = append(nodes, arrayNode)
        default:
            nodes = append(nodes, t.AddNode(v)...)
        }
    }

literalの場合、型チェックはreflectパッケージを使用して次のようにチェックします。
ValueTypeはJSONを書き出すときにnullで書き出すのか"null"で書き出すのかといった判定に使用します。

JSONの書き出しはこうった情報がないと正しく書き出せないので、ここがキモになります。

default:
    ref := reflect.ValueOf(node)
    var valueType ValueType
    switch ref.Kind() {
    case reflect.Int:
        valueType = Int
    case reflect.Float64:
        valueType = Float
    case reflect.Bool:
        valueType = Boolean
    default:
        if node == nil {
            valueType = Null
        } else {
            valueType = String
        }
    }

    id := uuid.Must(uuid.NewV4()).String()
    nodes = append(nodes, t.NewNodeWithLiteral(node).
        SetReference(Reference{ID: id, JSONType: Value, ValueType: valueType}))

ちなみに、ValueTypeの定義は以下のようになっています。

type ValueType int

const (
    Int ValueType = iota + 1
    String
    Float
    Boolean
    Null
)

JSONの書き出し

JSONの書き出しですが、こちらはとてもシンプルです。
JSONTypearrayobjectかそれ以外かで処理を分けます。
こちらも再帰的にノードを辿りながらJSONを組み上げていきます。

parseValueValueTypeを見てintなのかstringなのかといった値の種類別で変換処理を行います。

func (g *Gui) makeJSON(node *tview.TreeNode) interface{} {
    ref := node.GetReference().(Reference)
    children := node.GetChildren()

    switch ref.JSONType {
    case Object:
        i := make(map[string]interface{})
        for _, n := range children {
            i[n.GetText()] = g.makeJSON(n)
        }
        return i
    case Array:
        var i []interface{}
        for _, n := range children {
            i = append(i, g.makeJSON(n))
        }
        return i
    case Key:
        v := node.GetChildren()[0]
        if v.GetReference().(Reference).JSONType == Value {
            return g.parseValue(v)
        }
        return map[string]interface{}{
            node.GetText(): g.makeJSON(v),
        }
    }

    return g.parseValue(node)
}

func (g *Gui) parseValue(node *tview.TreeNode) interface{} {
    v := node.GetText()
    ref := node.GetReference().(Reference)

    switch ref.ValueType {
    case Int:
        i, _ := strconv.Atoi(v)
        return i
    case Float:
        f, _ := strconv.ParseFloat(v, 64)
        return f
    case Boolean:
        b, _ := strconv.ParseBool(v)
        return b
    case Null:
        return nil
    }

    return v
}

ざっくりですが、以上がtsonの処理の一部になります。

今後について

tsonは今後も機能を追加していきます。
具体的に言うとPostmanのような、ヘッダーやリクエストボディなどを自由に編集してHTTPリクエストを発行できる機能を追加する予定です。

おまけ

デモの中でgjoというツールを使いましたが、これは画像のようにkey=valueの組み合わせで簡単にJSON文字列を生成できるツールです。

image.png

curl -X POST -H "Content-Type: application/json" https://gorilla/api -d $(gjo name=gorilla)
って感じでcurlと組み合わせて使用できるので、よかったら使ってみてください。

最後に

tsonはまだまだ使いにくいところがあります。
まだまだ改善の余地があるので、この記事を見てGo詳しくなくても、
OSSを作るのをチャレンジしてみたい方や一緒に作ってみたい方はぜひ声かけてください。お待ちしています。

45
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
16