はじめに
この記事は自作のJSON解析CLIツールjtr
の紹介です。
開発経緯
近年GoやNim, TypeScriptといった静的型付け言語を扱うようになって、PythonやJavaScriptと違って型が強く意識した開発が求められるようになりました。
提供されているWebAPIからJSONを抜き出したり、逆に個人制作のWebアプリからJSONを提供する側に回るとき、typeを定義して各言語のオブジェクトとして扱う機会が多くあります。
特に、別のWebAPIからJSONを利用する際に、長ったらしいJSONを眺めて概要把握し、typeを定義してJSONを各言語のオブジェクトに落とし込む作業に疲れたので、もっと効率化できないかと考えました。
JSONをパースする際に型が意識されるケース
JSONをパースする際に型が意識されるケースをコード例を示しながら説明します。
JSONパース@Go
例として、Goはtypeを定義するとき、タグというメタ情報をtypeに定義しておいて、JSONパースする作業を楽にしてくれます。
引用: u1and0/pnsearch
// Option : ソート列の選択、昇順/降順、AND検索OR検索切り替え
Option struct {
SortOrder string `form:"orderby"`
SortAsc bool `form:"asc"`
OR bool `form:"or"`
}
// Filter : 発注日、納入日フィルター
Filter struct {
Order string `form:"発注"`
Delivery string `form:"納入"`
}
import "github.com/gin-gonic/gin"
func ReturnTempl(c *gin.Context) {
//
// --snip--
//
// Extract query
q := newQuery()
if err := c.ShouldBind(q); err != nil {
msg := fmt.Sprintf("%#v Bad Query", q)
c.JSON(http.StatusBadRequest, gin.H{"msg": msg, "query": q})
return
}
フォームの文字列をc.ShouldBind(q)
でtypeで定義したQuery構造体に変換します。
このとき、form:"orderby"
のようなメタ情報を手がかりにフレームワーク側がいい感じに割り当ててくれます。
JSONパース@Nim
次の例では、配信されたJSONをNim言語のオブジェクトとして扱います。
引用: u1and0/growiapi
## _api/v3/page で取得できるJSONオブジェクトとページの存在、エラーメッセージ
MetaPage* = object
page*: Page
limit: int
exist: bool
error: string
import json
proc initMetaPage*(path: string, limit = 50): MetaPage =
# --snip--
let res: Response = result.get()
case res.status:
of $Http200:
# --snip--
result.page = to(parseJson(jsonStr)["page"], Page)
parseJson(jsonStr)
で文字列をJSON(Nimの型定義としてはJsonNode型)に変換してくれます。
更に、to(JsonNode, T)
テンプレートでJsonNodeを型Tにいい感じに割り当ててくれます。
以上の例から見られるような いい感じに割当ててくれる ようにするためには、事前にtypeで構造体、オブジェクト、そして各プロパティの型を決めておいてあげないといけないわけです。
jqというコマンドがある
短いJSONならいいけど
長いJSONは構造を把握するだけで疲れます。
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jq
[
{
"name": "passy",
"url": "https://github.com/infinitybeond1/passy",
"method": "git",
"tags": [
"password",
"generator",
"cryptography",
"security"
],
"description": "A fast little password generator",
"license": "GPL3",
"web": "https://github.com/infinitybeond1/passy"
},
{
"name": "entgrep",
"url": "https://github.com/srozb/entgrep",
"method": "git",
"tags": [
"command-line",
"crypto",
"cryptography",
"security"
],
"description": "A grep but for secrets (based on entropy).",
"license": "MIT",
"web": "https://github.com/srozb/entgrep"
},
{
...他 27000行ほど
]
jtrの紹介
ここからようやくjtr
コマンドの説明です。
使い方はシンプルで、jq
コマンドの使い方をリスペクトしています。
名前の由来が
jq
+tree
1 =>jtr
ですので、使い方もjq
に似せています。
使用例
標準入力をJSONとしてパースし、型をオブジェクト名の右に添えて標準出力に出力します。
オブジェクトに階層があれば
$ echo {"foo":5.0,"baz":[{"foo":{"bar":100,"baz":"click","cat":null}},],"login":true} | jtr
.
├── foo <float>
├── baz [].
│ └── foo
│ ├── bar <int>
│ ├── baz <string>
│ └── cat <null>
└── login <bool>
プロパティアクセス
jq
と同様に、プロパティを下ってそこからツリー表示します。
下記の例ではbazオブジェクト以下をツリー表示しています。
$ echo '{"foo":5.0,"baz":[{"foo":{"bar":100,"baz":"click","cat":null}}],"login":true}' | jtr '.baz'
[].
└── foo
├── bar <int>
├── baz <string>
└── cat <null>
オプション
今のところ、使えるオプションは--jq
だけ。
jq
表示を同時に出力します。
options:
--jq, -q Display jq view
--help, -h Show help message
--version, -v Show version
巨大なJSONに使ってみた
使用例で見る感じ、どんな結果になるかは想像つくかと思いますが、実際に取り扱う巨大なJSONファイルに対して実行してみた結果を書きます。
test case 1
実行コマンドをjq
, jtr
の順に実行します。
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jq
[
{
"name": "passy",
"url": "https://github.com/infinitybeond1/passy",
"method": "git",
"tags": [
"password",
"generator",
"cryptography",
"security"
],
"description": "A fast little password generator",
"license": "GPL3",
"web": "https://github.com/infinitybeond1/passy"
},
{
"name": "entgrep",
"url": "https://github.com/srozb/entgrep",
"method": "git",
"tags": [
"command-line",
"crypto",
"cryptography",
"security"
],
"description": "A grep but for secrets (based on entropy).",
"license": "MIT",
"web": "https://github.com/srozb/entgrep"
},
{
...他 27000行ほど
]
$ curl -fsSL 'https://raw.githubusercontent.com/nim-lang/packages/master/packages.json' | jtr
[].
├── name <string>
├── url <string>
├── method <string>
├── tags []string
├── description <string>
├── license <string>
├── web <string>
└── doc <string>
まだこれならarrayの各要素が12個なので、jq
でもいけるか。
しかし、jq
とjtr
の出力をよく見比べてください。3要素目以降で"web"要素の次に"doc"という要素があることは、このjq
の2要素だけではわからなかったでしょう。
jtr
ならarrayをすべて走査して、最も大きい要素の解析結果を返してくれます。そのため、すべてのJSONを読み込まなくても、ここでは出てきていない3要素目以降に"doc"というオブジェクトが登場することに気づくことができます。
細かいこと言うと、最初に出てきた要素数最大のオブジェクトを解析します。そのため、例えば3要素目に"doc"を持つオブジェクトが登場して、4要素目以降に"doc"の代わりに"misc"という要素に置き換わっていても、"misc"というプロパティが出てきたことには気づくことがありません。
なお、"doc"オブジェクトが出てくるのは、調べた時点で全2055要素中の147要素だけでした。これは見逃してしまいそうですね。
test case 2
24.9MBの巨大なJSONを扱ってみます。
$ curl -fsSL 'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json' | jq
[
{
"id": "2489651045",
"type": "CreateEvent",
"actor": {
"id": 665991,
"login": "petroav",
"gravatar_id": "",
"url": "https://api.github.com/users/petroav",
"avatar_url": "https://avatars.githubusercontent.com/u/665991?"
},
"repo": {
"id": 28688495,
"name": "petroav/6.828",
"url": "https://api.github.com/repos/petroav/6.828"
},
"payload": {
"ref": "master",
"ref_type": "branch",
"master_branch": "master",
"description": "Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.",
"pusher_type": "user"
},
"public": true,
"created_at": "2015-01-01T15:00:00Z"
},
{
"id": "2489651051",
"type": "PushEvent",
"actor": {
"id": 3854017,
"login": "rspt",
"gravatar_id": "",
"url": "https://api.github.com/users/rspt",
"avatar_url": "https://avatars.githubusercontent.com/u/3854017?"
},
"repo": {
"id": 28671719,
"name": "rspt/rspt-theme",
"url": "https://api.github.com/repos/rspt/rspt-theme"
},
"payload": {
"push_id": 536863970,
"size": 1,
"distinct_size": 1,
"ref": "refs/heads/master",
"head": "6b089eb4a43f728f0a594388092f480f2ecacfcd",
"before": "437c03652caa0bc4a7554b18d5c0a394c2f3d326",
"commits": [
{
"sha": "6b089eb4a43f728f0a594388092f480f2ecacfcd",
"author": {
"email": "5c682c2d1ec4073e277f9ba9f4bdf07e5794dabe@rspt.ch",
"name": "rspt"
},
"message": "Fix main header height on mobile",
"distinct": true,
"url": "https://api.github.com/repos/rspt/rspt-theme/commits/6b089eb4a43f728f0a594388092f480f2ecacfcd"
}
]
},
"public": true,
"created_at": "2015-01-01T15:00:01Z"
},
{
"id": "2489651053",
"type": "PushEvent",
"actor": {
"id": 6339799,
"login": "izuzero",
"gravatar_id": "",
"url": "https://api.github.com/users/izuzero",
"avatar_url": "https://avatars.githubusercontent.com/u/6339799?"
},
"repo": {
"id": 28270952,
"name": "izuzero/xe-module-ajaxboard",
"url": "https://api.github.com/repos/izuzero/xe-module-ajaxboard"
},
"payload": {
"push_id": 536863972,
"size": 1,
"distinct_size": 1,
"ref": "refs/heads/develop",
"head": "ec819b9df4fe612bb35bf562f96810bf991f9975",
"before": "590433109f221a96cf19ea7a7d9a43ca333e3b3e",
"commits": [
{
"sha": "ec819b9df4fe612bb35bf562f96810bf991f9975",
"author": {
"email": "df05f55543db3c62cf64f7438018ec37f3605d3c@gmail.com",
"name": "Eunsoo Lee"
},
"message": "#20 게시글 및 댓글 삭제 시 새로고침이 되는 문제 해결\n\n원래 의도는 새로고침이 되지 않고 확인창만으로 해결되어야 함.\n기본 게시판 대응 플러그인에서 발생한 이슈.",
"distinct": true,
"url": "https://api.github.com/repos/izuzero/xe-module-ajaxboard/commits/ec819b9df4fe612bb35bf562f96810bf991f9975"
}
]
},
"public": true,
...他に 11251行
$ curl -fsSL 'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json' | jtr
[].
├── id <string>
├── type <string>
├── actor
│ ├── id <int>
│ ├── login <string>
│ ├── gravatar_id <string>
│ ├── url <string>
│ └── avatar_url <string>
├── repo
│ ├── id <int>
│ ├── name <string>
│ └── url <string>
├── payload
│ ├── action <string>
│ ├── comment
│ │ ├── url <string>
│ │ ├── id <int>
│ │ ├── diff_hunk <string>
│ │ ├── path <string>
│ │ ├── position <int>
│ │ ├── original_position <int>
│ │ ├── commit_id <string>
│ │ ├── original_commit_id <string>
│ │ ├── user
│ │ │ ├── login <string>
│ │ │ ├── id <int>
│ │ │ ├── avatar_url <string>
│ │ │ ├── gravatar_id <string>
│ │ │ ├── url <string>
│ │ │ ├── html_url <string>
│ │ │ ├── followers_url <string>
│ │ │ ├── following_url <string>
│ │ │ ├── gists_url <string>
│ │ │ ├── starred_url <string>
│ │ │ ├── subscriptions_url <string>
│ │ │ ├── organizations_url <string>
│ │ │ ├── repos_url <string>
│ │ │ ├── events_url <string>
│ │ │ ├── received_events_url <string>
│ │ │ ├── type <string>
│ │ │ └── site_admin <bool>
│ │ ├── body <string>
│ │ ├── created_at <string>
│ │ ├── updated_at <string>
│ │ ├── html_url <string>
│ │ ├── pull_request_url <string>
│ │ └── _links
│ │ ├── self
│ │ │ └── href <string>
│ │ ├── html
│ │ │ └── href <string>
│ │ └── pull_request
│ │ └── href <string>
│ └── pull_request
│ ├── url <string>
│ ├── id <int>
│ ├── html_url <string>
│ ├── diff_url <string>
│ ├── patch_url <string>
│ ├── issue_url <string>
│ ├── number <int>
│ ├── state <string>
│ ├── locked <bool>
│ ├── title <string>
│ ├── user
│ │ ├── login <string>
│ │ ├── id <int>
│ │ ├── avatar_url <string>
│ │ ├── gravatar_id <string>
│ │ ├── url <string>
│ │ ├── html_url <string>
│ │ ├── followers_url <string>
│ │ ├── following_url <string>
│ │ ├── gists_url <string>
│ │ ├── starred_url <string>
│ │ ├── subscriptions_url <string>
│ │ ├── organizations_url <string>
│ │ ├── repos_url <string>
│ │ ├── events_url <string>
│ │ ├── received_events_url <string>
│ │ ├── type <string>
│ │ └── site_admin <bool>
│ ├── body <string>
│ ├── created_at <string>
│ ├── updated_at <string>
│ ├── closed_at <null>
│ ├── merged_at <null>
│ ├── merge_commit_sha <string>
│ ├── assignee
│ │ ├── login <string>
│ │ ├── id <int>
│ │ ├── avatar_url <string>
│ │ ├── gravatar_id <string>
│ │ ├── url <string>
│ │ ├── html_url <string>
│ │ ├── followers_url <string>
│ │ ├── following_url <string>
│ │ ├── gists_url <string>
│ │ ├── starred_url <string>
│ │ ├── subscriptions_url <string>
│ │ ├── organizations_url <string>
│ │ ├── repos_url <string>
│ │ ├── events_url <string>
│ │ ├── received_events_url <string>
│ │ ├── type <string>
│ │ └── site_admin <bool>
│ ├── milestone
│ │ ├── url <string>
│ │ ├── labels_url <string>
│ │ ├── id <int>
│ │ ├── number <int>
│ │ ├── title <string>
│ │ ├── description <null>
│ │ ├── creator
│ │ │ ├── login <string>
│ │ │ ├── id <int>
│ │ │ ├── avatar_url <string>
│ │ │ ├── gravatar_id <string>
│ │ │ ├── url <string>
│ │ │ ├── html_url <string>
│ │ │ ├── followers_url <string>
│ │ │ ├── following_url <string>
│ │ │ ├── gists_url <string>
│ │ │ ├── starred_url <string>
│ │ │ ├── subscriptions_url <string>
│ │ │ ├── organizations_url <string>
│ │ │ ├── repos_url <string>
│ │ │ ├── events_url <string>
│ │ │ ├── received_events_url <string>
│ │ │ ├── type <string>
│ │ │ └── site_admin <bool>
│ │ ├── open_issues <int>
│ │ ├── closed_issues <int>
│ │ ├── state <string>
│ │ ├── created_at <string>
│ │ ├── updated_at <string>
│ │ ├── due_on <null>
│ │ └── closed_at <null>
│ ├── commits_url <string>
│ ├── review_comments_url <string>
│ ├── review_comment_url <string>
│ ├── comments_url <string>
│ ├── statuses_url <string>
│ ├── head
│ │ ├── label <string>
│ │ ├── ref <string>
│ │ ├── sha <string>
│ │ ├── user
│ │ │ ├── login <string>
│ │ │ ├── id <int>
│ │ │ ├── avatar_url <string>
│ │ │ ├── gravatar_id <string>
│ │ │ ├── url <string>
│ │ │ ├── html_url <string>
│ │ │ ├── followers_url <string>
│ │ │ ├── following_url <string>
│ │ │ ├── gists_url <string>
│ │ │ ├── starred_url <string>
│ │ │ ├── subscriptions_url <string>
│ │ │ ├── organizations_url <string>
│ │ │ ├── repos_url <string>
│ │ │ ├── events_url <string>
│ │ │ ├── received_events_url <string>
│ │ │ ├── type <string>
│ │ │ └── site_admin <bool>
│ │ └── repo
│ │ ├── id <int>
│ │ ├── name <string>
│ │ ├── full_name <string>
│ │ ├── owner
│ │ │ ├── login <string>
│ │ │ ├── id <int>
│ │ │ ├── avatar_url <string>
│ │ │ ├── gravatar_id <string>
│ │ │ ├── url <string>
│ │ │ ├── html_url <string>
│ │ │ ├── followers_url <string>
│ │ │ ├── following_url <string>
│ │ │ ├── gists_url <string>
│ │ │ ├── starred_url <string>
│ │ │ ├── subscriptions_url <string>
│ │ │ ├── organizations_url <string>
│ │ │ ├── repos_url <string>
│ │ │ ├── events_url <string>
│ │ │ ├── received_events_url <string>
│ │ │ ├── type <string>
│ │ │ └── site_admin <bool>
│ │ ├── private <bool>
│ │ ├── html_url <string>
│ │ ├── description <string>
│ │ ├── fork <bool>
│ │ ├── url <string>
│ │ ├── forks_url <string>
│ │ ├── keys_url <string>
│ │ ├── collaborators_url <string>
│ │ ├── teams_url <string>
│ │ ├── hooks_url <string>
│ │ ├── issue_events_url <string>
│ │ ├── events_url <string>
│ │ ├── assignees_url <string>
│ │ ├── branches_url <string>
│ │ ├── tags_url <string>
│ │ ├── blobs_url <string>
│ │ ├── git_tags_url <string>
│ │ ├── git_refs_url <string>
│ │ ├── trees_url <string>
│ │ ├── statuses_url <string>
│ │ ├── languages_url <string>
│ │ ├── stargazers_url <string>
│ │ ├── contributors_url <string>
│ │ ├── subscribers_url <string>
│ │ ├── subscription_url <string>
│ │ ├── commits_url <string>
│ │ ├── git_commits_url <string>
│ │ ├── comments_url <string>
│ │ ├── issue_comment_url <string>
│ │ ├── contents_url <string>
│ │ ├── compare_url <string>
│ │ ├── merges_url <string>
│ │ ├── archive_url <string>
│ │ ├── downloads_url <string>
│ │ ├── issues_url <string>
│ │ ├── pulls_url <string>
│ │ ├── milestones_url <string>
│ │ ├── notifications_url <string>
│ │ ├── labels_url <string>
│ │ ├── releases_url <string>
│ │ ├── created_at <string>
│ │ ├── updated_at <string>
│ │ ├── pushed_at <string>
│ │ ├── git_url <string>
│ │ ├── ssh_url <string>
│ │ ├── clone_url <string>
│ │ ├── svn_url <string>
│ │ ├── homepage <string>
│ │ ├── size <int>
│ │ ├── stargazers_count <int>
│ │ ├── watchers_count <int>
│ │ ├── language <string>
│ │ ├── has_issues <bool>
│ │ ├── has_downloads <bool>
│ │ ├── has_wiki <bool>
│ │ ├── has_pages <bool>
│ │ ├── forks_count <int>
│ │ ├── mirror_url <null>
│ │ ├── open_issues_count <int>
│ │ ├── forks <int>
│ │ ├── open_issues <int>
│ │ ├── watchers <int>
│ │ └── default_branch <string>
│ ├── base
│ │ ├── label <string>
│ │ ├── ref <string>
│ │ ├── sha <string>
│ │ ├── user
│ │ │ ├── login <string>
│ │ │ ├── id <int>
│ │ │ ├── avatar_url <string>
│ │ │ ├── gravatar_id <string>
│ │ │ ├── url <string>
│ │ │ ├── html_url <string>
│ │ │ ├── followers_url <string>
│ │ │ ├── following_url <string>
│ │ │ ├── gists_url <string>
│ │ │ ├── starred_url <string>
│ │ │ ├── subscriptions_url <string>
│ │ │ ├── organizations_url <string>
│ │ │ ├── repos_url <string>
│ │ │ ├── events_url <string>
│ │ │ ├── received_events_url <string>
│ │ │ ├── type <string>
│ │ │ └── site_admin <bool>
│ │ └── repo
│ │ ├── id <int>
│ │ ├── name <string>
│ │ ├── full_name <string>
│ │ ├── owner
│ │ │ ├── login <string>
│ │ │ ├── id <int>
│ │ │ ├── avatar_url <string>
│ │ │ ├── gravatar_id <string>
│ │ │ ├── url <string>
│ │ │ ├── html_url <string>
│ │ │ ├── followers_url <string>
│ │ │ ├── following_url <string>
│ │ │ ├── gists_url <string>
│ │ │ ├── starred_url <string>
│ │ │ ├── subscriptions_url <string>
│ │ │ ├── organizations_url <string>
│ │ │ ├── repos_url <string>
│ │ │ ├── events_url <string>
│ │ │ ├── received_events_url <string>
│ │ │ ├── type <string>
│ │ │ └── site_admin <bool>
│ │ ├── private <bool>
│ │ ├── html_url <string>
│ │ ├── description <string>
│ │ ├── fork <bool>
│ │ ├── url <string>
│ │ ├── forks_url <string>
│ │ ├── keys_url <string>
│ │ ├── collaborators_url <string>
│ │ ├── teams_url <string>
│ │ ├── hooks_url <string>
│ │ ├── issue_events_url <string>
│ │ ├── events_url <string>
│ │ ├── assignees_url <string>
│ │ ├── branches_url <string>
│ │ ├── tags_url <string>
│ │ ├── blobs_url <string>
│ │ ├── git_tags_url <string>
│ │ ├── git_refs_url <string>
│ │ ├── trees_url <string>
│ │ ├── statuses_url <string>
│ │ ├── languages_url <string>
│ │ ├── stargazers_url <string>
│ │ ├── contributors_url <string>
│ │ ├── subscribers_url <string>
│ │ ├── subscription_url <string>
│ │ ├── commits_url <string>
│ │ ├── git_commits_url <string>
│ │ ├── comments_url <string>
│ │ ├── issue_comment_url <string>
│ │ ├── contents_url <string>
│ │ ├── compare_url <string>
│ │ ├── merges_url <string>
│ │ ├── archive_url <string>
│ │ ├── downloads_url <string>
│ │ ├── issues_url <string>
│ │ ├── pulls_url <string>
│ │ ├── milestones_url <string>
│ │ ├── notifications_url <string>
│ │ ├── labels_url <string>
│ │ ├── releases_url <string>
│ │ ├── created_at <string>
│ │ ├── updated_at <string>
│ │ ├── pushed_at <string>
│ │ ├── git_url <string>
│ │ ├── ssh_url <string>
│ │ ├── clone_url <string>
│ │ ├── svn_url <string>
│ │ ├── homepage <string>
│ │ ├── size <int>
│ │ ├── stargazers_count <int>
│ │ ├── watchers_count <int>
│ │ ├── language <string>
│ │ ├── has_issues <bool>
│ │ ├── has_downloads <bool>
│ │ ├── has_wiki <bool>
│ │ ├── has_pages <bool>
│ │ ├── forks_count <int>
│ │ ├── mirror_url <null>
│ │ ├── open_issues_count <int>
│ │ ├── forks <int>
│ │ ├── open_issues <int>
│ │ ├── watchers <int>
│ │ └── default_branch <string>
│ └── _links
│ ├── self
│ │ └── href <string>
│ ├── html
│ │ └── href <string>
│ ├── issue
│ │ └── href <string>
│ ├── comments
│ │ └── href <string>
│ ├── review_comments
│ │ └── href <string>
│ ├── review_comment
│ │ └── href <string>
│ ├── commits
│ │ └── href <string>
│ └── statuses
│ └── href <string>
├── public <bool>
├── created_at <string>
└── org
├── id <int>
├── login <string>
├── gravatar_id <string>
├── url <string>
└── avatar_url <string>
ものすごい量の要素数だ...。
しかし、jq
の10000行も見なくても、jtr
の383行を見れば、どんな感じの構造のJSONなのかはわかりますね。
パフォーマンス
ネットワーク速度に依存しないようにlarge-file.jsonのrawcontentをローカルに保存してから実行しました。
行数はwcコマンドで数えます。
実行コマンドは下記です。
$ cat large-file.json | wc -l
$ cat large-file.json | jq | wc -l
$ cat large-file.json | jtr | wc -l
実行速度は、コマンド実行から標準出力に表示され終わるまでの時間です。
実行コマンドは下記です。
$ time cat large-file.json
$ time cat large-file.json | jq
$ time cat large-file.json | jtr
結果を示します。
cat | cat+jq | cat+jtr | |
---|---|---|---|
行数(行) | 11351 | 732457 | 383 |
実行速度(秒) | 3.273 | 11.216 | 2.882 |
出力行が少なければ実行速度が早くなるのはまぁ当然な結果なわけですが、実行速度も申し分なく、型まで判別してくれるのは便利なコマンドではないかな、と思う次第です。
インストール
github releaseからダウンロードしてください。
$ curl -fLO https://github.com/u1and0/jtr/releases/download/v0.2.8/jtr-linux.zip
$ unzip jtr-linux.zip
$ chmod 755 ./jtr
$ ./jtr -v
または、Nimbleを使ってください。
https://nimble.directory/pkg/jtr
$ nimble install jtr
ビルド
Githubからフォークしてください。
u1and0/jtr
実装
実装のコアのコードを示します。
詳細が知りたければGithub上のソースを見てください。
概要
実装はNimの標準パッケージstd/jsonが強力ですのでそこまで苦労しませんでした。
parseJson()
を使うとJsonNodeKind
という型でそのオブジェクトの型を判別できるので、switch-case分岐してタイプを表示するか、Objectを再帰的に探索するか、Arrayを再帰的に探索します。
探索の結果はすべてstring型でつなげていって、最終的にstringを標準出力にecho()
するだけです。
プリミティブ型の探索
要はnullかboolかstringかfloatかintかを判定してくれます。
func rootTree*(jnode: JsonNode): string =
## Tree view for type of plain JSON recursively
##
## ```nim
## doAssert rootTree(parseJson("null")) == "<null>"
## doAssert rootTree(parseJson("true")) == "<bool>"
## doAssert rootTree(parseJson("\"5\"")) == "<string>"
## doAssert rootTree(parseJson($5.0)) == "<float>"
## doAssert rootTree(parseJson($5)) == "<int>"
## ```
case jnode.kind
of JNull: "<null>"
of JBool: "<bool>"
of JString: "<string>"
of JInt: "<int>"
of JFloat: "<float>"
of JArray: arrayTree(jnode)
of JObject: ".\n" & objectTree(jnode)
Objectの探索
Objectを再帰的に潜って解析します。
func objectTree*(jobj: JsonNode, indent = ""): string =
## Tree view for type of object elemnts recursively
##
## ```nim
## let obj = """
## {"foo": "0", "obj": {"bar":1, "baz":"2"}, "name": "ken"}
## """.parseJson
## doAssert objectTree(obj) == """├── foo <string>
## ├── obj
## │ ├── bar <int>
## │ └── baz <string>
## └── name <string>"""
## ```
var
res: seq[string]
i: int
branch, nextIndent: string
let isLastOne = func(i: int): bool = i >= len(jobj)-1
for key, val in jobj.pairs:
if isLastOne(i):
branch = "└── "
nextIndent = indent & " ".repeat(4)
else:
branch = "├── "
nextIndent = indent & "│" & " ".repeat(3)
let types: string = case val.kind
of JNull: " <null>"
of JBool: " <bool>"
of JString: " <string>"
of JInt: " <int>"
of JFloat: " <float>"
of JArray: " " & arrayTree(val, nextIndent)
of JObject: "\n" & objectTree(val, nextIndent)
res.add(&"{indent}{branch}{key}{types}")
i += 1
return res.join("\n")
Arrayの探索
Array型のすべての要素をまとめて表示します。
func arrayTree*(jarray: JsonNode, indent = ""): string =
## Tree view for type of array elemnts recursively
##
## ```nim
## doAssert arrayTree(parseJson("[null,null,null]")) == "[]null"
## doAssert arrayTree(parseJson("[true,false,false]")) == "[]bool"
## doAssert arrayTree(parseJson("[\"cat\", \"dog\"]")) == "[]string"
## doAssert arrayTree(parseJson("[1,2,3]")) == "[]int"
## doAssert arrayTree(parseJson("[1.0,10.0,100.0]")) == "[]float"
## doAssert arrayTree(parseJson("[1,\"2\",3]")) == "[]any"
## doAssert arrayTree(parseJson("[[1,2,3]]")) == "[][]int"
## doAssert arrayTree(parseJson("""
## [
## {
## "foo":{
## "bar": 1,
## "baz": "key"
## }
## },
## {
## "foo":{
## "bar": 2,
## "baz": "signal",
## "cat": null
## }
## }
## ]
## """)) == """[].
## └── foo
## ├── bar <int>
## ├── baz <string>
## └── cat <null>"""
## ```
let typesArr: seq[JsonNodeKind] = collect:
for val in jarray:
val.kind
if all(typesArr, func(x: JsonNodeKind): bool = x == JNull):
return "[]null"
elif all(typesArr, func(x: JsonNodeKind): bool = x == JBool):
return "[]bool"
elif all(typesArr, func(x: JsonNodeKind): bool = x == JString):
return "[]string"
elif all(typesArr, func(x: JsonNodeKind): bool = x == JInt):
return "[]int"
elif all(typesArr, func(x: JsonNodeKind): bool = x == JFloat):
return "[]float"
elif all(typesArr, func(x: JsonNodeKind): bool = x == JArray):
return "[]" & arrayTree(jarray[0]) # TODO 本当にjarray[0]だけでいいのか?seekLargestObject通すべきでは
elif all(typesArr, func(x: JsonNodeKind): bool = x == JObject):
let nextIndent = indent & " ".repeat(2)
let largestObj = seekLargestObject(jarray)
return "[].\n" & objectTree(largestObj, nextIndent)
else: # 全ての型が一致しない場合
return "[]any"
-
tree
コマンドの実装例としてDenoでCLIツールを作ってみる:treeコマンドの実装を参考にさせていただきました。コードができあがっていく過程が見えて大変参考になりました。ここでも再帰処理を使って実装しています。 ↩