[linux] jq

jq 명령어를 몰라서 발생한 일

예전에 어떤 시스템을 만들때 많은 부분을 쉘스크립트로 작성한 적이 있는데 쉘스크립트로는 json 처리를 할 수가 없었다. 정규표현식으로는 json을 파싱하는데 한계가 있다.

그래서 파이썬으로 json을 파싱하는 프로그램을 작성했었는데, jq 명령어를 알았다면, 작성하지 않았을 것 같다.

Tips

Tips는 jq 설명을 모두 읽은 후에 이해된다. 자주 사용할 부분이라서 앞부분에 메모해두었다.

docker inspect와 jq

$  docker inspect c-was-s0101  | jq '.[] | keys'
[
  "AppArmorProfile",
  "Args",
  "HostConfig",
  "HostnamePath",
  "HostsPath",
  "Id",
  "Image",
  "LogPath",
  "Mounts",
  "State"
]
$  docker inspect c-was-s0101  | jq '.[] | .Mounts'
[
  {
    "Type": "bind",
    "Source": "/app/root/log/s0101",
    "Destination": "/project/logs",
    "Mode": "rw",
    "RW": true,
    "Propagation": "rprivate"
  }
  // ...
]

Basic filters

Identity: .

가장 간단한 필터다. 입력 내용을 그대로 출력한다.

echo ' "hello" ' | jq '.'
# 결과는 "hello"

Object Identifier-Index: .foo, .foo.bar

.fooinputJSON['foo']를 찾는다. 없으면 null을 출력한다.

ex1 : 기본 .foo 사용법

{ "foo": 42, "bar": "hello" } | jq '.foo'
# 결과는 42

ex2 : 없으면 null을 출력한다

echo ' { "foo": 42, "bar": "hello" } ' | jq '.foo2'
# 결과는 null

Array Index: .[2]

ex1 : 기본 배열 인덱스

INPUT

[
  { "name": "JSON", "good": true },
  { "name": "XML", "good": false }
]
# 배열의 0번 인덱스를 출력
cat input.txt | jq '.[0]'

OUTPUT

{
  "name": "JSON",
  "good": true
}

ex2 : 음수 인덱스 지정도 가능

# 배열의 뒤에서 두번째 인덱스를 출력
echo '[1, 2, 3]' | jq '.[-2]'
#결과는 2

Array/String Slice: .[10:15]

ex1 : 기본 슬라이싱 문법

# 배열의 2 <= index < 4 번 인덱스를 출력
echo ' ["a", "b", "c", "d", "e"] ' | jq '.[2:4]'
# 결과는 ["c", "d"]

ex2 : 문자열 슬라이싱도 가능

# 문자열의 2 <= index < 4 번 인덱스를 출력
echo ' "abcdefghi" ' | jq '.[2:4]'
# 결과는 "cd"

ex3 : 파이썬과 유사한 슬라이싱 문법

# 배열의 0 <= index < 3 번 인덱스를 출력
echo '  ["a", "b", "c", "d", "e"] ' | jq '.[:3]'
# 결과는  ["a", "b", "c"]

ex4 : 음수 인덱스로 범위를 설정할 수 있다.

# 배열의 뒤에서 2번째부터 끝까지 출력
echo ' ["a", "b", "c", "d", "e"] ' | jq '.[-2:]'
# 결과는 ["d", "e"]

Array/Object Value Iterator: .[]

.[index] 는 배열의 특정 인덱스에 해당하는 항목만 출력하는 문법이다. 콤마로 여러 인덱스를 지정할 수 있다. 인덱스를 지정하지 않으면, 즉 .[]은 모든 인덱스를 출력한다.

ex1 : 반복자 기본 사용법

INPUT

[
  { "name": "JSON", "good": true },
  { "name": "XML", "good": false }
]
jq '.[]'

OUTPUT

{ "name": "JSON", "good": true }

{"name":"XML", "good":false}

ex2 : 빈 배열인 경우 출력결과 없음

echo ' [] ' | jq '.[]'
# 결과는 없음

ex3 : 콤마로 원하는 인덱스의 값을 출력

echo '[1, 2, 3, 4, 5]' | jq '.[1,2,3]'
# 결과는 다중 출력이다
2
3
4

ex4 : 객체에 대해서 .[]은 모든 값을 출력

echo '{ "name": "JSON", "good": true }' | jq '.[]'
# 결과는 다중 출력이다
"JSON"
true

ex4 : 객체에 숫자 인덱스는 불가능

echo '{ "name": "JSON", "good": true }' | jq '.[0]'
# 결과는 Cannot index object with number

값이 배열인 항목을 출력

ex1 : .key형태로 배열 출력

echo '{ "projects": ["jq", "wikiflow"] } ' | jq '.projects'
# 결과는 ["jq", "wikiflow"]

ex2 : .key[]형태로 배열 출력

echo '{ "projects": ["jq", "wikiflow"] } ' | jq '.projects[]'
# 결과는 다중 출력이다
"jq"
"wikiflow"

Pipe

echo '[ { "name": "JSON", "good": true } ]' | jq '.[] '
# 결과는 { "name": "JSON", "good": true }
echo '[ { "name": "JSON", "good": true } ]' | jq '.[] | .name'
# 결과는 "JSON"

Parenthesis

echo 1 | jq '(. + 2) * 5'
# 결과는 15

Types And Values

배열 만들기

echo '{ "user": "stedolan", "projects": ["jq", "wikiflow"] }' | jq '[ .user, .projects[] ]'
# 출력은 ["stedolan", "jq", "wikiflow"]

참고로 만약 필터를 아래와 같이 작성했다면,

jq '.user, .projects[]'

출력은 3개의 결과물이 만들어진다.

"stedolan"
"jq"
"wikiflow"

객체 만들기

JSON에서 처럼, {}는 객체를 생성한다 ( {"a":42, "b":17} )

ex: 값은 모든 표현식이 가능하다.

echo ' { "bar": 42, "baz": 43 } ' | jq '{ foo: .bar }'
# 결과는 {"foo": 42}

ex: 축약된 형태의 표기법

INPUT

{ "user": "stedolan", "title": "JQ Primer" }
jq '{ user: .user, title: .title }'

# or

jq '{ user, title }'

OUTPUT

{ "user": "stedolan", "title": "JQ Primer" }

ex: 다중 출력

표현식 중 하나가 다중 출력 형태인 경우 다중 객체가 만들어진다.

{ "user": "stedolan", "titles": ["JQ Primer", "More JQ"] }
jq '{user, title: .titles[]}'

OUTPUT

{"user":"stedolan", "title": "JQ Primer"}
{"user":"stedolan", "title": "More JQ"}

ex : 키부분의 표현식

{ "user": "stedolan", "titles": ["JQ Primer", "More JQ"] }
jq '{ (.user) : .titles }'

OUTPUT

{ "stedolan": ["JQ Primer", "More JQ"] }

다음과 같이 필터를 작성한 경우

"f o o" as $foo | "b a r" as $bar | {$foo, $bar:$foo}

출력은 아래와 같다.

{ "f o o": "f o o", "b a r": "f o o" }

Recursive Descent : ..

..은 재귀적으로 아래로 내려가면서 모든 값을 생성한다.

ex1 : 재귀적으로 모두 출력

INPUT

{
  "title": "JQ Primer",
  "user": {
    "age": 12,
    "home": {
      "address1": "seoul"
    }
  }
}
jq '..'

OUTPUT

{
  "title": "JQ Primer",
  "user": {
    "age": 12,
    "home": {
      "address1": "seoul"
    }
  }
}

"JQ Primer"

{
  "age": 12,
  "home": {
    "address1": "seoul"
  }
}

12

{ "address1": "seoul" }

"seoul"

ex2 : age부분만 찾기

jq ' .. | .age? '

OUTPUT

null
12
null
jq ' .. | .age? | select ( . != null )? '

Builtin operators and functions

Addition +

+는 두 개의 필터에 대해 그들을 같은 입력으로 적용하고, 결과물을 더한다.

$  echo ' {"a": 7} ' | jq '.a + 1'
# 결과는 8
$  echo ' {"a": [1,2], "b": [3,4]} ' | jq '.a + .b'
# 결과는 [1,2,3,4]
$  echo ' {"a": 1} ' | jq '.a + null'
# 결과는 1
$  echo ' {} ' | jq '.a + 1'
# 결과는 1
$  echo ' null ' | jq '{a: 1} + {b: 2} + {c: 3} + {a: 42}'
# 결과는 {"a": 42, "b": 2, "c": 3}

Substraction -

$  echo ' {"a":3} ' | jq '4 - .a'
# 결과는 1
$  echo ' ["xml", "yaml", "json"] ' | jq '. - ["xml", "yaml"]'
# 결과는 ["json"]

Multiplication, division, modulo: * / %

$  echo ' 5 ' | jq '10 / . * 3'
# 결과는 6
echo 0 | jq ' ( 1 / . ) '
# 결과는 cannot be divided because the divisor is zero

문서에는 0으로 나누면 에러가 발생한다고 적어놓고, 아래의 경우에는 에러가 발생하지 않는 것처럼 적혀있다. 실제로 에러가 발생한다.

echo ' [1,0,-1] ' | jq ' .[] | ( 1 / . ) '
# 출력 결과
1
jq: error (at <stdin>:1): number (1) and number (0) cannot be divided because the divisor is zero

끝에 물음표를 붙이면 에러가 무시된다. 에러가 발생한 부분은 출력하지 않는다.

echo ' [1,0,-1] ' | jq ' .[] | ( 1 / . )?'
# 출력 결과
1
-1
echo ' "ab" ' | jq ' . * 3 '
# 결과는 "ababab"
echo ' "a, b,c,d, e" ' | jq '. / ", "'
# 결과는 ["a","b,c,d","e"]
echo 'null' | jq '{"k": {"a": 1, "b": 2}} * {"k": {"a": 0,"c": 3}}'
# 결과는 객체의 머지임 {"k": {"a": 0, "b": 2, "c": 3}}

length

echo '[1,2,3]' | jq 'length'
# 결과는 3
echo '"hello"' | jq 'length'
# 결과는 5
echo '"가나다라"' | jq 'length'
# 결과는 4
$  echo ' ["xml", "yaml", "json"] ' | jq 'length'
# 결과는 3
echo null | jq 'length'
# 결과는 0
echo '100' | jq 'length'
# 결과는 100

utf8bytelength

내 리눅스에서는 동작하지 않는다

echo '"\u03bc"' | jq 'utf8bytelength'

OUTPUT

jq: error: utf8bytelength/0 is not defined at <top-level>, line 1:
utf8bytelength
jq: 1 compile error

keys, keys_unsorted

echo '{"abc": 1, "abcd": 2, "Foo": 3}' | jq 'keys'
# 결과는 ["Foo", "abc", "abcd"]
echo '[42,3,35]' | jq 'keys'
# 결과는 [0,1,2]

has(key)

echo ' {"foo": 42} ' | jq 'has("foo")'
# 출력은 true
echo ' {"foo": 42} ' | jq 'has("bar")'
# 출력은 false
echo ' [1,2,3] ' | jq 'has(2)'
# 출력은 true
echo ' [1,2] ' | jq 'has(2)'
# 출력은 false

in

echo ' "foo" ' | jq ' in({"foo": 42})'
# 출력은  true
echo ' ["foo", "bar"] ' | jq '.[] | in({"foo": 42})'
# 출력은 다중 출력이다
true
false
echo ' 0 ' | jq ' in ([100,200]) '
# 출력은 true
echo ' [2, 0] ' | jq ' .[] | in ([100,200]) '
# 출력은 다중 출력이다
false
true

map(x), map_values(x)

map을 알아보기 전에 예제를 먼저 살펴보자

echo [1,2,3] | jq '[ .[] | . + 10 ]'
# 출력은 [11, 12, 13]
echo [1,2,3] | jq 'map(. + 10)'
# 출력은 [11, 12, 13]
echo ' {"a": 1, "b": 2, "c": 3} ' | jq 'map_values( . + 1 )'
# 출력은 {"a": 2, "b": 3, "c": 4 }

path(path_expression)

배열 경로 표기법의 이해

path 함수

echo null | jq 'path(.a[0].b)'
# 결과는 ["a", 0, "b"]

getpath(PATHS)

echo ' {"a":{"b":10, "c":11}} ' | jq 'getpath(["a","b"], ["a","c"])'

# or

echo ' {"a":{"b":10, "c":11}} ' | jq '.a.b, .a.c'
# 결과는 다중 출력
10
11
echo null | jq 'getpath(["a","b"])'
# 결과는 null

setpath(PATHS; VALUE)

echo null | jq 'setpath(["a","b"]; 1)'
# 출력은 {"a": {"b": 1}}
echo '{"a":{"b":0}}' | jq 'setpath(["a","b"]; 1)'
# 출력은 {"a": {"b": 1}}
echo null | jq 'setpath([0,"a"]; 1)'
# 출력은 [{"a":1}]

del(path_expression)

echo '{"foo": 42, "bar": 9001}' | jq 'del(.foo)'
# 출력은 {"bar": 9001}
echo ' ["foo", "bar", "baz"] ' | jq 'del(.[1, 2])'

# 출력은 ["foo"]

delpaths(PATHS)

echo '{"a":{"b":1},"x":{"y":2}}' | jq 'delpaths([["a","b"]])'
# 출력은 {"a":{},"x":{"y":2}}

to_entries

key,value 형태로 만들어준다. 출력 결과를 보면 이해가 된다.

echo '{"a": 1, "b": 2}' | jq 'to_entries'
# 출력은 [{"key":"a", "value":1}, {"key":"b", "value":2}]

from_entries

key,value 형태로부터 객체를 만든다. 출력 결과를 보면 이해가 된다

echo '[{"key":"a", "value":1}, {"key":"b", "value":2}]' | jq 'from_entries'
# 출력은 {"a": 1, "b": 2}

with_entries

출력 결과를 보면 이해가 된다.

echo '{"a": 1, "b": 2}' | jq 'with_entries(.key |= "KEY_" + .)'
# 출력은 {"KEY_a": 1, "KEY_b": 2}

select(boolean_expression)

특정 조건이 맞는 항목만 출력한다.

echo '[1,5,3,0,7]' | jq 'map(select( . >= 2 ))'
# 출력은 [5,3,7]
echo '[{"id": "first", "val": 1}, {"id": "second", "val": 2}]' | jq '.[] | select(.id == "second")'
# 출력은 {"id": "second", "val": 2}

paths

echo '[1,[[],{"a":2}]]' | jq 'paths'
# 출력은 다중 출력
[0]
[1]
[1,0]
[1,1]
[1,1,"a"]
paths(numbers) 숫자값이 있는 항목만
echo '[1,[[],{"a":2}]]' | jq 'paths(numbers)'
# 출력은 다중 출력
[0]
[1,1,"a"]
paths(scalars), leaf_paths 말단 노드만
echo '[1,[[],{"a":2}]]' | jq 'paths(scalars)'
# 출력은 다중 출력
[0]
[1,1,"a"]
echo '[1,[[],{"a":2}]]' | jq 'leaf_paths'
# 출력은 다중 출력
[0]
[1,1,"a"]

flatten, flatten(depth)

echo '[1, [2], [[3]]]' | jq 'flatten'
# 출력은 [1, 2, 3]
echo '[1, [2], [[3]]]' | jq 'flatten(1)'
# 출력은 [1, 2, [3]]
echo '[1, [2], [[3]]]' | jq 'flatten(0)'
# 출력은 [1,[2],[[3]]]
echo '[[]]' | jq 'flatten'
# 출력은 []
echo '[{"foo": "bar"}, [{"foo": "baz"}]]' | jq 'flatten'
# 출력은 [{"foo": "bar"}, {"foo": "baz"}]