JSON İşleme Aracı: jq

Şimdi JSON Düşünsün!

json jq bash

JSON dosya formatı artık hayatımızın vazgeçilmezi. Gündelik hayatta hep onunla işimiz oluyor. Peki bir araç olsa, istediğimiz gibi filtreleme yapsak, hem de hiç kod yazmasak?

Sizi harika bir araçla tanıştırmak istiyorum: jq. jq süper hızlı çalışan hafif siklet bir komut satırı aracı. Tüm platformlarda var. macOS kullanıcıları hemen:

$ brew install jq

ile kurabilir. Elle kurmak için sitesinden indirip kurulum yapabilirsiniz. Eğer kurulum yapmak istemezseniz, web tarayıcısı üzerinden de kullanabilirsiniz. Dokümantasyonu ve nasıl kullanılacağına dair bir tuturial’i de bulunuyor.

Hemen ufak ufak girişelim.

$ echo '{"x": 1, "y": 2}' | jq
{
  "x": 1,
  "y": 2
}

$ echo '{"x": 1, "y": 2}' | jq .x
1

Elimizde {key: value} şeklinde bulunan nesnelerde .key mantığıyla veriye ulaşabiliyoruz. Eğer iç-içe yani nested key’ler varsa; .key.key.key şeklinde zincirleme ilerleyebiliyoruz. Bu durum Object Identifier-Index olarak geçer:

$ echo '{"x": {"a": "bu a değeri"}}' | jq .x.a
"bu a değeri"

Eğer key var mı? diye bakmak istersek Optional Object Identifier-Index kullanırız:

$ echo '{"foo": 1, "bar": 2}' | jq '.baz?'
null

Yani .key? şeklinde kullanılır. Eğer key yoksa sonuç null döner. Genelde elimizde hep bir liste yani Array ve içinde de objeler olur:

$ echo '[{"id": 1}, {"id": 2}]' | jq .
[
  {
    "id": 1
  },
  {
    "id": 2
  }
]

Eğer flat-map yapmak istersek, yani aradan virgülü atmak;

$ echo '[{"id": 1}, {"id": 2}]' | jq .[]
{
  "id": 1
}
{
  "id": 2
}

Sadede id’lerin değerlerini alalım:

$ echo '[{"id": 1}, {"id": 2}]' | jq '.[] | .id'
1
2

Yani jq '.[] | .id' şeklinde. Elimizde aşağıdaki gibi bir json olsun:

{
    "user": {
        "id": 1,
        "name": "vigo"
    },
    "items": [
        1,
        2,
        3
    ]
}

İçinden sadece .user’ı alalım:

$ echo '{"user": {"id": 1, "name": "vigo"}, "items": [1,2,3]}' | jq '.user'
{
  "id": 1,
  "name": "vigo"
}

Aynı nesneden kopya bir liste:

$ echo '{"x": 1}' | jq '[. , . , .]'
[
  {
    "x": 1
  },
  {
    "x": 1
  },
  {
    "x": 1
  }
]

İşin sırrı jq '[. , . , .]' bu kısında. . elemanın kendisi oluyor. Elimizde kullanıcıların listesi var mesela:

[
    {
        "id": 1,
        "name": "vigo"
    },
    {
        "id": 2,
        "name": "lego"
    },
    {
        "id": 3,
        "name": "figo"
    }
]

Sadece .name’leri alalım:

$ echo '[{"id": 1, "name": "vigo"}, {"id": 2, "name": "lego"}, {"id": 3, "name": "figo"}]' | jq '.[] | .name'
"vigo"
"lego"
"figo"

Map Özelliği

Tüm programlama dillerinde bulunan, iterable yani bir koleksiyonun içinde tüm elemanları bir fonksiyondan geçirme işlemi. Elimizde sayılardan oluşan bir liste var ver biz koleksiyon içindeki her sayıya bir ekleyeceğiz:

[
    1,
    2,
    3,
    4,
    5
]
$ echo '[1,2,3,4,5]' | jq 'map(. + 1)'
[
  2,
  3,
  4,
  5,
  6
]

map(. + 1) -> . elemanın kendisi, sonrasında + operatör, 1 de ekleyeceğimiz değer. . Identity anlamındadır. Çarpma versiyonu:

$ echo '[1,2,3,4,5]' | jq 'map(. * 2)'
[
  2,
  4,
  6,
  8,
  10
]

Eğer çıktıların daha kısa ve kompakt olmasını isterseniz -c parametresini kullanabiliriniz:

$ echo '[1,2,3,4,5]' | jq 'map(. * 2)' -c
[2,4,6,8,10]

Şimdi elimizdeki aynı listeden bir transformasyon yapalım:

$ echo '[1,2,3,4,5]' | jq 'map({"x": .})'
[
  {
    "x": 1
  },
  {
    "x": 2
  },
  {
    "x": 3
  },
  {
    "x": 4
  },
  {
    "x": 5
  }
]

Fonksiyonlar

En sık kullandığım length fonksiyonu:

$ echo '[1,2,3,4,5]' | jq 'length'
5

$ jq length /path/to/file.json

Elementin indeksini bulalım. [1,2,3,4,5] listesinde 2’nin indeksi 1’dir yani 0. element: 1, 1.element de 2’dir ya, bunu jq ile gelen indices fonksiyonunu kullanarak bulalım:

$ echo '[1,2,3,4,5]' | jq 'indices(2)'
[
  1
]

$ echo '[1,2,3,4,5]' | jq 'indices(2) | .[]'
1

String içinde pipe yaparak çıktıyı flat-map yaptık. jq 'komut | .[]' şeklinde. Bir kısım fonksiyon örneği verelim:

$ echo '[2, 4, 6, 8]' | jq 'contains([2])'  # true
$ echo '[2, 4, 6, 8]' | jq 'reverse' -c     # [8,6,4,2]
$ echo '[2, 4, 6, 8]' | jq 'min'            # 2
$ echo '[2, 4, 6, 8]' | jq 'max'            # 8

$ echo '"hello"' | jq 'split("")' -c       # ["h","e","l","l","o"]
$ echo '"hello"' | jq 'test("he*")' -c     # true
$ echo '"hello"' | jq 'test("he*!")' -c    # false
$ echo '"hello"' | jq 'contains("a")' -c   # false
$ echo '"hello"' | jq 'startswith("h")' -c # true
$ echo '"hello"' | jq 'endswith("o")' -c   # true

$ echo '[{"user": {"id": 1, "name": "vigo"}}, {"user": {"id": 2, "name": "ezel"}}]' \
    | jq '.[0] | keys' -c      # ["user"]
$ echo '[{"user": {"id": 1, "name": "vigo"}}, {"user": {"id": 2, "name": "ezel"}}]' \
    | jq '.[0].user | keys' -c # ["id","name"]
$ echo '{"id": 1, "name": "vigo"}' | jq 'has("id")' # true

$ echo '{"id": 1, "name": "vigo"}' | jq 'flatten' -c # [1,"vigo"]
$ echo '[{"user": {"id": 1, "name": "vigo"}}, {"user": {"id": 2, "name": "ezel"}}]' \
    | jq '.[0].user | flatten' -c # [1,"vigo"]

to_entries ile;

$ echo '{"id": 1, "name": "vigo"}' | jq 'to_entries'
[
  {
    "key": "id",
    "value": 1
  },
  {
    "key": "name",
    "value": "vigo"
  }
]

Şeklinde çıktı alırız. Diğer sevdiğim bir fonksiyon da select:

# id’si 1 olanı alalım, select(.id == 1)
$ echo '[{"id": 1, "name": "vigo"}, {"id": 2, "name": "ezel"}]' \
    | jq '.[] | select(.id == 1)'
{
  "id": 1,
  "name": "vigo"
}

Yaşı 10’dan küçükleri filtreleyelim:

[
    {
        "id": 1,
        "name": "vigo",
        "age": 46
    },
    {
        "id": 2,
        "name": "ezel",
        "age": 7
    }
]

ve;

echo '[{"id": 1, "name": "vigo", "age": 46}, {"id": 2, "name": "ezel", "age": 7}]' \
    | jq '.[] | select(.age < 10) | .name' # "ezel"

İşin püf noktası '.[] | select(.age < 10) | .name' kısmında. .[] ile liste işlemi yapacağımızı, tek tek elemanı alacağımızı belirtip elemanı | ile select fonksiyonuna pipe ediyoruz. Fonksiyondan dönenin sadece .name’ini alıyoruz.

jq içinde bir kısım fonksiyonlarla geliyor. Detaylara buradan bakabilirsiniz.

Parantez, Array ve Obje Üreticileri

Parantez ile aynı matematik işlemlerindeki gibi öncelik ve gruplama işlerini yapabiliriz:

$ echo '1' | jq '(. + 2) * 5'
15

. identity idi, yani 1’in kendisi. (1 + 2) * 5 -> 3 * 5 -> 15.

[] ile Array Construction yani liste üretebiliriz:

{
    "user": "vigo",
    "badges": [
        "python",
        "ruby",
        "golang"
    ]
}

Şimdi bir çıktı üretelim ve tipi liste olsun:

$ echo '{"user": "vigo", "badges": ["python", "ruby", "golang"]}' | jq '[.user, .badges]'
[
  "vigo",
  [
    "python",
    "ruby",
    "golang"
  ]
]

Şimdi {} ile Object Construction yapalım:

$ echo '{"user": "vigo", "badges": ["python", "ruby", "golang"]}' | jq '{user, stack: .badges}'
{
  "user": "vigo",
  "stack": [
    "python",
    "ruby",
    "golang"
  ]
}

Aynı veriden multiple-dict üretelim:

$ echo '{"user": "vigo", "badges": ["python", "ruby", "golang"]}' | jq '{user, stack: .badges[]}'
{
  "user": "vigo",
  "stack": "python"
}
{
  "user": "vigo",
  "stack": "ruby"
}
{
  "user": "vigo",
  "stack": "golang"
}

Durum Kontrolleri

Elimizde şöyle bir veri var:

[
    {
        "name": "vigo",
        "is_admin": true
    },
    {
        "name": "lego",
        "is_admin": false
    }
]

Admin ve standart kullanıcıları görelim:

$ echo '[{"name": "vigo", "is_admin": true}, {"name": "lego", "is_admin": false}]' | jq '.[] | if .is_admin == true then (.name + " is admin")  else (.name + " is standard user")  end'
"vigo is admin"
"lego is standard user"

Eldeki veriyi csv formatına çevirebiliriz:

$ echo '[{"id": 1, "name": "vigo", "age": 46}, {"id": 2, "name": "ezel", "age": 7}]' \
    | jq -r '(map(keys) | add | unique) as $cols | map(. as $row | $cols | map($row[.])) as $rows | $cols, $rows[] | @csv'
"age","id","name"
46,1,"vigo"
7,2,"ezel"

Keza veriyi xpath gibi görüntüleyebiliriz:

$ echo '[{"id": 1, "name": "vigo", "age": 46}, {"id": 2, "name": "ezel", "age": 7}]' | jq -r 'path(..) | map(tostring) | join("/")'

0
0/id
0/name
0/age
1
1/id
1/name
1/age

Konfigürasyon Dosyası

Eğer isterseniz kendiniz özelleştirilmiş fonksiyonlar yapıp kullanabilirsiniz. Bunun için ~/.jq dosyası oluşturmanız yeterli. Şimdi schema diye bir fonksiyon oluşturup ~/.jq dosyasına ekleyelim:

$ echo 'def schema: path(..) | map(tostring) | join("/");' >> ~/.jq

şimdi kullanalım:

$ echo '{"data": {"users": [{"id": 1, "name": "vigo"},{"id": 2, "name": "ezel"}]}}' \
    | jq 'schema'

""
"data"
"data/users"
"data/users/0"
"data/users/0/id"
"data/users/0/name"
"data/users/1"
"data/users/1/id"
"data/users/1/name"

jq bir komut satırı araca olduğu için tüm dosya pipe işlemleri de geçerli oluyor. Kimi zaman pretty-print yani çıktının güzel ve renkli görünmesi için, ya da dosyadan okuyup başka bir yere paslamak için de kullanılır.

$ cat /path/to/file.json | jq
$ cat /path/to/file.json | jq > /path/to/output.json
$ curl -sL 'https://api.github.com/repos/vigo/statoo/commits?per_page=1' | jq

ya da;

$ curl -sL 'https://api.github.com/repos/vigo/statoo/commits?per_page=1' | jq '.[] | .commit.author'
{
  "name": "Uğur Özyılmazel",
  "email": "ugurozyilmazel@gmail.com",
  "date": "2021-08-27T10:14:12Z"
}

Dediğim gibi, jq benim gündelik hayatımda çok sık kullandığım bir araç. Ben sadece bildiğim ve sık kullandığım konuları yazdım. Burada yazdıklarımdan çok daha fazlası var. Umarım sizin de işinize yarar.