Canvas 1 Layer 1

기초 문법과 작동 원리

들어가며: 커맨드라인 JSON 프로세서 jq

JSONJavaScript Object Notation은 가장 널리 사용되는 데이터 포맷 중 하나입니다. JSON은 구조화된 데이터를 간결하고 일관적으로 표현할 수 있습니다. 많은 API 서비스에서 JSON 포맷을 사용하고 있으며, 자바스크립트JavaScript에서 네이티브 요소처럼 다룰 수 있기 때문에 자바스크립트의 인기와 함께 더욱 많이 사용 되고 있습니다.

jq는 이 JSON 포맷의 데이터를 다루는 커맨드라인 유틸리티입니다. JSON은 간결하지만 사람이 직접 읽기에 적합한 데이터 형식은 아닙니다. JSON 데이터에서 필요한 정보를 추출하거나 변형하기 위해서는 프로그래밍 언어에서 데이터를 파싱하고, 조작하는 과정을 거쳐야합니다. 자바스크립트에서 JSON 데이터를 직접 읽어드릴 수 있다고 하더라도 이는 상당히 번거로운 일입니다. jq는 이러한 작업을 커맨드라인에서 간단하게 수행할 수 있도록 도와주는 JSON 프로세서입니다.

jq는 프로그래밍 언어는 아니지만, JSON 데이터를 다루기 위한 다양한 기능들을 제공합니다. 여기서는 바로 사용할 수 있는 jq의 기본적인 사용법들을 소개하도록 하겠습니다.

jq 설치

jq는 패키지 매니저로 설치하거나 빌드된 바이너리를 직접 다운로드 받아 사용할 수 있습니다.

패키지 매니저를 사용한 jq 설치

맥OSmacOS에서는 홈브류Homebrew를 사용해 jq를 설치할 수 있습니다.

$ brew install jq

리눅스에서는 각 배포판 별 패키지 관리자를 사용해 설치할 수 있습니다. 관리자 권한이 필요합니다.

$ apt-get install jq # Debian or Ubuntu
$ dnf install jq     # Fedora
$ zypper install jq  # OpenSUSE
$ pacman -Sy jq      # Arch

설치가 정상적으로 되었는지 확인해봅니다.

$ jq --version
jq-1.5

바이너리를 직접 다운로드 받기

jq는 C 프로그래밍 언어로 작성된 프로젝트입니다. 패키지 매니저를 사용하면 특정한 버전을 사용하는 게 어렵습니다. 원하는 버전이 있는 경우 소스 코드를 직접 빌드해서 사용하거나 미리 빌드되어있는 파일을 공식 사이트에서 다운로드 받아 사용할 수도 있습니다.

사용하는 운영체제에 맞는 바이너리를 다운로드 받아 $PATH에 포함된 디렉터리에 복사해줍니다. 예를 들어 맥OS의 경우 다음과 같이 직접 다운로드 받아 설치할 수 있습니다.

## 바이너리 파일 다운로드. 바이너리 주소는 https://stedolan.github.io/jq/download/에서 확인
$ wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-osx-amd64

## 실행 권한 부여
$ chmod +x jq

## $PATH에 포함된 경로로 복사
$ mv jq /usr/local/bin

## 정상적으로 설치되었는지 확인
$ jq --version
jq-1.5

운영체제에 따른 바이너리 파일의 차이를 제외하면 리눅스 배포판에서도 비슷한 방법으로 설치할 수 있습니다.

jq에 JSON 데이터를 입력하는 방법

jq는 입력을 처리하는 기능을 가지고 있습니다. 입력을 어떻게 처리할 지에 대한 정보를 첫 번째 인자로 받습니다. 이 인자는 jq 고유의 문법으로 작성됩니다. 점(.)은 동일값Identity 필터입니다. 입력받은 내용을 그대로 출력하라는 의미를 가지고 있습니다. 이를 인자로 넘겨 실행해보겠습니다.

$ jq '.'

명령어를 실행하면 jq로 제어권이 넘어가고 아무일도 일어나지 않습니다. jq는 입력을 기다리고 있습니다. {"foo": "bar"}를 입력해봅니다.

$ jq '.'
{"foo": "bar"}
{
  "foo": "bar"
}

{"foo": "bar"} 을 입력하면 입력받은 내용을 가다듬어서 출력해줍니다. 이와 같이 jq를 실행하고 데이터를 직접 입력하는 게 첫 번째 입력방법입니다. 이 때 입력받은 내용을 그대로 출력하는 것은 아니고 JSON 데이터 구조를 파싱해서 저장한 다음 다시 출력하는 방식이라서 원래의 입력이 복원되지는 않습니다.

두 번째 방법은 파일을 지정하는 방법입니다. 다음과 같이 JSON 데이터를 foobar.json에 저장하고, jq의 두 번째 인자로 넘겨줍니다. 결과는 첫 번째 입력 방식과 같습니다.

$ echo '{"foo": "bar"}' > foobar.json
$ jq '.' foobar.json
{
  "foo": "bar"
}

세 번째 방법은 파이프 연산자로 다른 프로세스의 출력을 넘겨받아 jq의 입력으로 보내는 방법입니다.

$ echo '{"foo": "bar"}' | jq '.'
{
  "foo": "bar"
}

파이프를 사용하는 방법은 특히 강력합니다. 일례로 웹API에서 반환받은 JSON 데이터를 별도로의 과정을 거치지 않고 곧바로 처리할 수 있습니다. 깃헙Github API를 호출한 결과를 jq에 넘겨보겠습니다.

$ curl -s 'https://api.github.com/users/octocat' | jq '.'
{
  "login": "octocat",
  "id": 583231,
  ...

이 글에서도 주로 파이프를 사용해서 입력을 전달합니다. jq를 사용하는 가장 일반적인 패턴이니 꼭 익혀두시기 바랍니다.

동일값Identity 필터를 사용한 JSON 정형화

jq의 가장 기본적인 활용법은 JSON 데이터를 정형화해서 출력하는 일입니다. 코드의 정형화를 프리티 프린트Pretty Print라고 부르기도 합니다. JSON은 스펙상 의미있는 문자열 사이에 공백문자나 개행이 들어갈 수 있습니다. 따라서 의미가 완벽히 같은 서로 다른 JSON 문서가 존재할 수 있습니다. 예를 들어 {"foo" : "bar"}{ "foo" : "bar" }는 완전히 같은 의미를 가진 서로 다른 JSON 문서입니다. jq는 이를 사람이 보기 좋게 정형화하고, 의미가 같다면 같은 구조로 출력해줍니다. 정형화를 위해 동일값 필터(.)을 사용할 수 있습니다.

$ echo '{"foo" : "bar"}' | jq '.'
{
  "foo": "bar"
}

$ echo '{    "foo"    : "bar"  }' | jq '.'
{
  "foo": "bar"
}

기계 입장에서 이러한 변형에 특별한 의미가 있지는 않습니다. 위에서 예로 든 두 데이터와 jq의 출력 결과는 모두 같은 의미를 가지고 있습니다. 프로그래밍 언어나 jq 입장에서는 이를 파싱하고 나면 결국 같은 구조와 의미를 가지고 있습니다. 이러한 변형을 하는 가장 큰 이유는 사람이 직접 데이터를 읽기 위해서입니다.

특히 JSON 데이터가 한 줄로 기록되어있거나 공백문자가 전부 제거된 경우 사람이 읽는 것은 매우 어렵습니다. 예를 들어 다음과 같은 데이터가 있다고 가정해봅니다. 이 정도는 아주 복잡한 데이터는 아닙니다만, 한 눈에 잘 들어오지 않습니다.

{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"6d583450be96208b0",
"EndpointID":"d6138701512082a8d3","Gateway":"172.17.0.1","IPAddress":"172.17.0.2",
"IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,
"MacAddress":"02:55:ac:11:00:02","DriverOpts":null}}

이 JSON 데이터를 jq에 넘겨주면 다음과 같이 예쁘게 출력해줍니다.

jq는 JSON 데이터를 다루는 강력한 기능들을 가지고 있지만 단순히 정형화 용도로도 자주 사용됩니다. 일반적인 활용법이므로 | jq '.'를 관용구로 외워두면 편리합니다.

노트
파이썬으로 JSON 정형화

jq가 없고 파이썬만 있는 환경에서는 다음 명령어로 JSON 데이터를 정형화할 수 있습니다.

$ echo '{    "foo"    : "bar"  }' | python -mjson.tool
{
    "foo": "bar"
}

단일 입력 단일 출력을 다루는 jq 문법 기초

이미 살펴보았듯이 jq는 첫 번째 인자로 jq 고유의 문법으로 이루어진 값을 넘겨받습니다. 지금까지는 동일값 필터(.)만 사용했습니다. 이것만 알아도 jq는 유용합니다. 하지만 jq 문법을 조금만 알아도 활용 범위는 훨씬 더 넓어집니다. 여기서부터는 jq에서 데이터를 추출하거나 간단히 변형하는 방법에 대해서 알아보겠습니다.

jq는 JSON 데이터를 입력 받아 JSON을 출력한다

일반적으로 JSON 데이터는 루트 엘리먼트가 배열이나 객체로 이루어져있습니다. 하지만 JSON 스펙상 루트 엘리먼트가 배열이나 객체일 이유는 없습니다. 이해를 위해 가장 간단한 원시적 타입(null, 문자열, 숫자)으로만 이루어진 JSON부터 생각해보겠습니다.

$ echo 'null' | jq '.'
null

$ echo '"String"' | jq '.'
"String"

$ echo '44' | jq '.'
44

이는 그저 입력받은 JSON 데이터를 다시 출력하는 간단한 명령입니다. 하지만 여기서 중요한 단서가 있습니다. jq가 출력하는 결과는 유효한 JSON 문서라는 점입니다. null, "String", 44는 모두 유효한 JSON 데이터입니다. 예를 들어서 "로 감싸지 않은 문자열을 직접 넘겨주면 jq는 이를 제대로 처리하지 못 합니다.

$ echo 'String' | jq '.'
parse error: Invalid numeric literal at line 2, column 0

String은 올바른 JSON 표현이 아니기 때문입니다. jq가 JSON 데이터를 입력받아 JSON 데이터를 출력한다는 것을 기억하시기 바랍니다. 뒤에서 더 자세히 다루겠지만 jq의 동작 방식을 이해하기 위해서는 이 점을 반드시 기억해야합니다.

노트
홑따옴표 사용하기

셸 위에서 따옴표 해석은 자주 헷갈리는 부분 중 하나입니다. jq를 사용할 때는 잘못 해석되는 것을 방지하기 위해서 echo나 jq의 인자를 반드시 홑따옴표(')로 감싸는 것을 추천합니다. 셸에서 홑따옴표 안의 문자열은 변형되지 않고 거의 그대로 이용됩니다. 예를 들어 JSON 문자열을 만들고자 할 때 'String', "String"는 모두 의도한 대로 동작하지 않습니다. '"String"'와 같이 홑따옴표 안에서 다시 쌍따옴표로 감싸야만 JSON 문자열로 해석됩니다.

오브젝트 속성 필터Object Identifier-Index

오브젝트의 특정한 속성을 가져오려면 점(.) 뒤에 이름을 붙여주면 됩니다. 예를 들어 오브젝트에서 foo라는 속성의 값만 가져오고 싶다면 .foo를 넘겨줍니다.

$ echo '{"foo": "bar", "hoge": "piyo"}' | jq '.foo'
"bar"

“foo” 속성의 값인 “bar”가 출력되었습니다. 또한 jq의 최종 출력이 유효한 JSON 문서라는 것을 알 수 있습니다.

하지만 속성 이름에 기호가 포함되어있다면 의도한 대로 동작하지 않습니다.

$ echo '{"foo<": "bar", "hoge": "piyo"}' | jq '.foo<'
jq: error: syntax error, unexpected $end (Unix shell quoting issues?) at <top-level>, line 1:
.foo<
jq: 1 compile error

이 문법은 사실 .["속성 이름"]의 축약 버전입니다. 이름에 기호가 포함되어 있을 때는 원래 문법을 사용하면 문제 없이 동작합니다.

$ echo '{"foo<": "bar", "hoge": "piyo"}' | jq '.["foo<"]'
"bar"

값을 제대로 가져오는 것을 확인할 수 있습니다. 그렇다면 오브젝트 안의 오브젝트 안의 오브젝트의 속성은 어떻게 가져올 수 있을까요? .속성이름.속성이름.속성이름처럼 계속 이어붙여주면 됩니다.

$ echo '{"a": {"b": {"c": "d"}}}' | jq '.a.b.c'
"d"

일견 직관적이고 간단해보입니다. 하지만 속성을 이어붙여서 가져오는 방법은 문법적 설탕(Syntax Sugar)입니다. 파이프 연산자를 이해하면 .a.b.c가 어떻게 동작하는 지 좀 더 정확히 알 수 있습니다.

파이프(|) 연산자: 마지막 출력을 다시 입력으로 넘겨주는 연산자

앞서 동일값 필터(.)를 사용해 JSON 데이터를 정형화했습니다. 동일값 필터는 입력 받은 내용을 그대로 출력하는 연산자입니다.

$ echo '"foobar"' | jq '.'
"foobar"
파이프와 동일값 필터를 사용해 입력을 그대로 출력하는 예제
파이프와 동일값 필터를 사용해 입력을 그대로 출력하는 예제

jq 문법에도 셸에서 사용하는 것과 마찬가지로 파이프 연산자가 있습니다. 마지막 출력을 입력으로 넘겨받아 다시 처리합니다. 동일값 필터(.)로 처리한 내용을 파이프로 넘겨서 동일값 필터(.)로 처리하면 그 결과는 어떻게 될까요? 한 번 실행해보겠습니다.*

* 이 때 파이프(|) 연산자가 jq에 넘겨지는 첫 번째 인자 문자열의 일부가 되어야한다는 점에 주의가 필요합니다. jq . | .과 같이 입력하면 셸 파이프로 jq .의 결과를 점(.) 명령어에 넘기라는 의미가 되어버리므로 의도한 대로 동작하지 않을 것입니다.

$ echo '"foobar"' | jq '. | .'
"foobar"
파이프와 동일값 필터를 여러번 반복해도 결과는 같습니다
파이프와 동일값 필터를 여러번 반복해도 결과는 같습니다

결과는 그대로입니다. 생각해보면 당연한 결과입니다. 동일값 필터는 입력받은 내용을 그대로 출력하기 때문에, 몇 번이고 반복해서 실행하더라도 결과는 달라지지 않습니다.

$ echo '"foobar"' | jq '. | . | . | . | .'
"foobar"

파이프 연산자는 jq를 이해하는데 핵심적인 부분입니다. jq는 데이터를 입력받아 어떤 처리를 합니다. 그리고 이 결과를 다시 파이프로 넘겨 어떤 처리를 합니다. 이 과정을 원하는 만큼 반복할 수 있습니다.

다시 오브젝트 속성 필터를 살펴보겠습니다. {"a": {"b": {"c": "d"}}}에서 “d” 값을 가져오기 위해서 오브젝트 속성 필터를 이어붙였습니다.

$ echo '{"a": {"b": {"c": "d"}}}' | jq '.a.b.c'
"d"

이를 한 단계 씩 나눠서 생각해보겠습니다. 먼저 a 속성을 가져옵니다. 한 줄에 출력하기 위해서 -c(컴팩트) 옵션을 사용하겠습니다.

$ echo '{"a": {"b": {"c": "d"}}}' | jq -c '.a'
{"b":{"c":"d"}}

출력 결과를 입력으로 jq를 사용해서 b 속성을 가져옵니다.

$ echo '{"b":{"c":"d"}}' | jq -c '.b'
{"c":"d"}

다시 출력 결과를 입력으로 jq를 사용해서 c 속성을 가져옵니다.

$ echo '{"c":"d"}' | jq -c '.c'
"d"

최종 결과인 “d”가 출력되었습니다. 이를 파이프 연산자로 재구축 하면 다음과 같습니다.

$ echo '{"a": {"b": {"c": "d"}}}' | jq '.a | .b | .c'
"d"
속성 연산자를 파이프를 사용해 작성한 예제
속성 연산자를 파이프를 사용해 작성한 예제

'.a | .b | .c'를 해석하면 다음과 같습니다. 입력에 대해서 오브젝트 속성 필터로 a 속성을 찾고 이 결과를 다음 필터에 넘겨줍니다. 이 결과는 a 속성의 값인 {"b":{"c":"d"}}가 됩니다. 두 번째 필터에서는 이 객체를 입력으로 받아 b 속성을 찾습니다. 그리고 그 결과를 다시 다음 필터로 넘겨줍니다. 이를 다시 입력으로 받아 c 속성을 찾아 그 결과를 반환합니다. 이를 .a.b.c라고 줄여서 쓸 수 있습니다.

배열 인덱스 필터: [n]

배열 인덱스 필터는 배열에서 n번째 값을 가져오는 필터입니다.

$ echo '[0, 11, 22, 33, 44 ,55]' | jq '.[4]'
44

배열이 객체 안에 있는 경우엔 파이프 연산자를 사용해서 배열의 값을 가져올 수 있습니다.

$ echo '{"data": [0, 11, 22, 33, 44 ,55]'} | jq '.data | .[4]'
44

이 때 .data | .[4].data.[4]로 줄여쓸 수 없습니다. .data가 배열이기 때문에 줄여쓰려면 .data[4]와 같이 줄여써야합니다.

노트
명시적으로 파이프 연산자 사용하기

이 차이가 아직 헷갈리게 느껴진다면 파이프 연산자를 사용해 명시적으로 사용하는 편이 좋습니다. 파이프 연산자를 사용하는 경우 앞의 출력을 점(.)으로 넘겨받습니다. 따라서 .[4]는 입력으로 받은 배열 데이터에 [4] 필터(배열의 다섯번째 값)를 적용한다는 의미가 됩니다. 점(.)은 입력 그 자체로 풀어서 해석해볼 수 있습니다. '.data | .[4]'에서 앞의 .data를 풀어써보면 다음과 같습니다.

{"data": [0, 11, 22, 33, 44 ,55]'}.data

조금 헷갈릴 수 있지만 객체 뒤에 붙은 점(.)은 동일값 필터가 아닌, 속성 연산자의 역할만 합니다. 즉, 필터의 첫 글자가 점(.)인 경우는 동일값 필터가 되고, 그 뒤에 문자열이 따라오면 동일값 필터와 속성 연산자 두 가지 역할을 모두 하게 됩니다. 여기서는 주어진 객체에서 data 를 가져오라는 의미를 가지고 있습니다. 실제로 이 내용을 jq에서 그대로 실행할 수 있습니다. -n 옵션을 사용하면 입력을 받지 않고 jq를 실행할 수 있습니다. 위의 내용을 그대로 jq에 넘겨보겠습니다.

$ jq -cn '{"data": [0, 11, 22, 33, 44 ,55]}.data'
[0,11,22,33,44,55]

배열이 반환되는 것을 알 수 있습니다. 이번엔 뒤의 .[4]를 생각해보겠습니다. 여기서 .[0,11,22,33,44,55]와 같습니다. 따라서 이를 풀어 쓰면 [0,11,22,33,44,55][4]가 됩니다. 이 내용도 jq에 그대로 넘겨보겠습니다.

$ jq -cn '[0,11,22,33,44,55][4]'
44

44가 출력됩니다. .은 입력을 그대로 가지고 있는 특별한 필터입니다. 특정 맥락에서 .이 어떤 의미로 사용되었는지, 그리고 동일값 필터라면 무엇이 들어있을지 생각하면서 사용해보면 jq를 이해하는데 많은 도움이 됩니다.

다수의 입력과 다수의 출력을 다루는 jq 문법

jq는 객체 뿐만 아니라 배열을 다루는 것도 가능합니다. 하지만 배열은 조금 어려운 주제입니다. 배열은 하나의 값이 아니라 다수의 값을 가지고 있습니다. 값들을 가진 어떤 배열이 있을 때, 배열을 벗겨내면 1개의 출력이 아니라 n개의 출력이 생성됩니다. 배열보다 배열 인덱스를 먼저 다룬 데는 이유가 있습니다. 배열 인덱스는 결과적으로 하나의 입력과 출력을 다루는 기능입니다. 하지만 배열은 하나의 입력으로부터 다수의 출력을 다루게 됩니다. jq에서 다수의 입출력을 다룬다는 의미를 한 단계씩 쫓아가보도록 하겠습니다.

이미 살펴보았듯이 배열 인덱스 문법을 사용하면 배열에서 n번째 값을 가져올 수 있습니다.

$ echo '["a", "b"]' | jq '.[0]'
"a"
배열 인덱스는 배열을 입력 받아 하나의 값을 반환합니다
배열 인덱스는 배열을 입력 받아 하나의 값을 반환합니다

이 때 인덱스 없이 대괄호([])만을 사용하면 배열의 모든 값을 가져올 수 있습니다. 여기서 중요한 점은 배열이 아니라 배열의 모든 값을 가져온다는 점입니다.

$ echo '["a", "b"]' | jq '.[]'
"a"
"b"
배열 반복자는 배열을 입력으로 받아 배열의 요소 수 n개의 출력을 생성합니다
배열 반복자는 배열을 입력으로 받아 배열의 요소 수 n개의 출력을 생성합니다

첫 번째 줄에는 “a”, 두 번째 줄에는 “b”가 출력됩니다. 눈치 채셨겠지만 "a"\n"b"는 유효한 JSON이 아닙니다. 앞서 jq는 JSON을 입력받아 다시 유효한 JSON을 반환한다고 이야기한 바 있습니다. 하지만 이 결과는 앞서 제시한 명제에 대한 반례라고 할 수 있습니다. 앞서 제시했던 명제를 좀 더 가다듬을 필요가 있습니다.

즉, "a"\n"b"는 하나의 출력이 아니라 개행으로 나누어진 두 개의 출력입니다. 이를 별개로 보면 문자열을 루트 요소로 하는 2개의 JSON 문서라고 이해할 수 있습니다. 즉, "a""b"는 각각 유효한 JSON 문서입니다. 출력과 반대로 입력에 대해서도 마찬가지입니다.

예를 들어 "a" "b"는 유효한 JSON이 아니지만 jq에서는 올바른 입력입니다. jq는 이를 두 개의 JSON 입력으로 받아들입니다. 이를 동일값 필터를 사용해 출력해보겠습니다.

$ echo '"a" "b"' | jq '.'
"a"
"b"
다수의 입력과 다수의 출력
다수의 입력과 다수의 출력

2개의 JSON 데이터를 입력받아 2개의 JSON 결과를 출력합니다. 좀 더 나아가보겠습니다. nameage 필드를 가진 객체 하나로 구성된 2개의 데이터를 jq에 넘겨 .name 속성을 가지고 와보겠습니다.

$ echo '{"name": "john"} {"name": "merry", "age": 24}' | jq '.name'
"john"
"merry"

각 입력에 대해서 .name 필터를 적용한 결과가 각각 출력되는 것을 알 수 있습니다. 이해를 돕기 위해 이를 나눠서 생각해보겠습니다. 이 결과는 다음 두 명령어를 합쳐놓은 것과 같습니다.

$ echo '{"name": "john"}' | jq '.name'
"john"

$ echo '{"name": "merry", "age": 24}' | jq '.name'
"merry"

즉, 두 개의 입력에 대해서 인자로 넘겨준 필터는 별도로 처리가 이루어집니다. 이번에는 age 속성을 가져와보겠습니다. 첫 번째 객체에는 age 속성이 없고, 두 번째 객체에는 있습니다.

$ echo '{"name": "john"} {"name": "merry", "age": 24}' | jq '.age'
null
24

이 예제에서도 두 입력이 별개로 처리되는 것을 알 수 있습니다.

배열 반복자Array iterator

다시 배열 연산자로 돌아가보겠습니다. 이번에는 두 객체가 하나의 배열로 전달되었다고 가정해봅니다. 두 데이터를 대괄호로 감싸고 객체 사이에 쉼표를 넣어주면 배열을 루트 요소로 하는 하나의 JSON 데이터가 됩니다. 이 입력을 동일값 필터에 넘겨봅니다.

$ echo '[{"name": "john"}, {"name": "merry", "age": 24}]' | jq -c '.'
[{"name":"john"},{"name":"merry","age":24}]

출력 값 역시 하나입니다. 배열 연산자가 등장할 차례입니다. 위에서 설명했듯이 배열 연산자를 사용하면 배열을 벗겨내고 모든 값을 각각 출력합니다. 따라서 배열의 요소수만큼 출력이 만들어집니다. 여기서 다시 파이프 연산자를 통해서 이 결과를 입력으로 전달한다고 해보겠습니다. 이 경우 하나의 입력을 받아 2개의 출력이 만들어지고, 각각의 출력은 각각 파이프를 통해 다음 필터로 전달됩니다. .[] | .의 경우를 생각해봅니다. 결과적으로 이는 .[]과 같습니다.

$ echo '[{"name": "john"}, {"name": "merry", "age": 24}]' | jq -c '.[] | .'
{"name":"john"}
{"name":"merry","age":24}

하나의 입력이 다수의 출력이 됩니다. 배열 연산자를 정확히는 배열 반복자Array iterator 라고 부릅니다. 출력이 분리된 각각의 객체에서 name 속성을 찾아보겠습니다.

$ echo '[{"name": "john"}, {"name": "merry", "age": 24}]' | jq -c '.[] | .name'
"john"
"merry"
배열 반복자를 사용해 다수의 출력을 만들고 다시 필터로 처리하는 예제
배열 반복자를 사용해 다수의 출력을 만들고 다시 필터로 처리하는 예제

이 코드가 작동하는 원리만 이해해도 큰 고비는 넘겼다고 할 수 있습니다.

객체 값 반복자Object value iterator

[]은 객체의 값들을 반복하는 데도 사용할 수 있습니다.

$ echo '{"name": "merry", "age": 24}' | jq '.[]'
"merry"
24

배열 컨스트럭션: 다수의 출력을 하나의 배열로 만들기

배열 반복자와 반대로 다수의 출력을 하나의 배열로 만드는 것도 가능합니다. 이 역시 배열 표기법([])을 사용합니다. 하지만 이번에는 사용하는 방법이 조금 다릅니다. 배열로 필터를 감싸주면 해당하는 출력이 배열이 되어 반환됩니다. 예를 들어 '.[] | [.name]'으로 바꾸면 각각의 출력이 배열로 만들어집니다.

$ echo '[{"name": "john"}, {"name": "merry", "age": 24}]' | jq -c '.[] | [.name]'
["john"]
["merry"]

말그대로 각각의 출력이 배열이 되었습니다. 이는 처음에 말했던 다수의 출력을 하나의 배열로 만드는 것과는 조금 차이가 있습니다. "john""merry"를 하나의 배열로 만드려면 어떻게 해야할까요? 잠깐 고민해보시고 아래 예제를 살펴보시기 바랍니다.

$ echo '[{"name": "john"}, {"name": "merry", "age": 24}]' | jq -c '[.[] | .name]'
["john","merry"]
다수의 입력을 다시 하나의 배열로 만드는 jq 예제
다수의 입력을 다시 하나의 배열로 만드는 jq 예제

달라진 점이 보이시나요? 인자 전체를 배열로 감싸주었습니다. 이렇게 하면 [ ... ] 안에서 출력이 분리 되었을 때 최종적으로 하나의 배열로 만들어줍니다. 여기서 중요한 것이 [ ... ] 안에서 출력이 분리되어야한다는 점입니다. 예를 들어 처음부터 여러개의 입력을 받은 경우에는 이 방법을 사용할 수 없습니다.

쉼표(,) 연산자

쉼표(,) 연산자를 사용하면 명시적으로 하나의 입력을 명시적으로 나눌 수 있습니다.

$ echo '{"name": "merry", "age": 24}' | jq -c '.name , .age , .'
"merry"
24
{"name": "merry", "age": 24}

입력이 나눠지는 만큼 같은 입력이 전달됩니다. 위의 예제에서는 쉼표 2개로 입력을 3개로 나눴습니다. 세 입력 모두 처음에 입력으로 넘어온 값이 전달 됩니다. 첫번째 출력은 속성 연산자로 name 키의 값을 출력합니다. 두번째 출력은 속성 연산자로 age 키의 값을 출력합니다. 마지막 출력은 입력을 그대로 출력합니다.

이렇게 3개로 나눠진 출력을 배열 컨스트럭터로 하나의 배열로 만들 수 있습니다.

$ echo '{"name": "merry", "age": 24}' | jq -c '[.name , .age , .]'
["merry",24,{"name":"merry","age":24]}]
쉼표 연산자로 분리한 출력을 다시 하나의 배열로 만드는 예제
쉼표 연산자로 분리한 출력을 다시 하나의 배열로 만드는 예제

괄호 연산자

괄호 연산자를 사용하면 연산 우선 순위를 결정할 수 있습니다. 쉼표 연산자로 출력을 분리하는 경우를 생각해보겠습니다.

$ echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '.name , .friends'
"merry"
["mia","ava"]

여기서 두 번째 입력에서 파이프 연산자를 사용해 배열의 첫번째 값을 가져오고 싶다고 가정해보죠. 용감하게 파이프 연산자와 배열 인덱스 필터를 붙여봅니다.

echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '.name , .friends | .[0]'
jq: error (at <stdin>:1): Cannot index string with number

에러가 출력됩니다. 왜 이런 결과가 발생한 걸까요? 두 개의 출력이 파이프로 넘겨졌기 때문입니다. 따라서 위의 코드는 (.name | .[0]), (.friends | .[0])와 같은 의미를 가지고 있습니다. 하지만 .name의 값은 배열이 아니므로 배열 인덱스 필터를 처리하지 못 합니다: Cannot index string with number.

이럴 때 괄호를 사용하면 연산자가 적용되는 우선 순위를 명시적으로 나타낼 수 있습니다.

echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '.name , (.friends | .[0])'
"merry"
"mia"

이 예제에서는 파이프 연산자가 .friends에만 적용된 것을 확인할 수 있습니다.

객체 컨스트럭션

출력 결과를 객체로 만드려면, 객체로 만드려는 부분을 중괄호 {}로 감싸주면 됩니다. 중괄호 키와 값을 지정할 수 있습니다. 아래의 예제에서는 “name” 키에 name 속성을 값으로 가지는 객체를 생성합니다.

$ echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '{"name": .name}'
{"name":"merry"}

원본 데이터의 키 값을 그대로 재사용하는 경우 {name}과 같이 줄여서 사용할 수 있습니다.

$ echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '{name}'
{"name":"merry"}

다음 예제에서는 name 속성과 friends의 순서가 바뀐 객체를 만들어보겠습니다.

$ echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '{friends, name}'
{"friends":["mia","ava"],"name":"merry"}

쉼표 연산자를 이해하면 여러 키를 가진 객체를 만드는 것도 쉽게 이해할 수 있습니다. 이번에는 friends 배열을 분리한 새로운 객체를 생성해보겠습니다. 아래의 예제를 참고하면, 쉼표 연산자와 마찬가지를 쉼표를 기준으로 입력이 나눠지는 것을 알 수 있습니다.

$ echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '{"f1": .friends[0], "f2": .friends[1]}'
{"f1":"mia","f2":"ava"}

키값에서도 원본 데이터를 직접 사용할 수 있습니다. 이 때는 키가 들어가는 부분(:의 앞부분)을 소괄호(())로 감싸주어야합니다.

$ echo '{"name": "merry", "friends": ["mia","ava"]}' | jq -c '{(.name): .friends}'
{"merry":["mia","ava"]}

객체를 생성을 할 때도 입력이 어떻게 나눠지고 다시 합쳐지는 지를 생각해보면 jq를 이해하는 데 많은 도움이 될 것입니다.

jq에서 자주 사용되는 옵션

여기까지 jq의 기본적인 문법들에 대해서 알아보았습니다. 마지막으로 jq에서 자주 사용하는 옵션들을 살펴보고자 합니다.

--compact-output, -c
출력 결과를 한 줄에 모아서 출력해줍니다.
--null-input, -n
입력을 받지 않습니다. 좀 더 정확히는 null을 입력으로 받아서 jq를 실행합니다.
--raw-output, -r
jq를 사용해 JSON의 문자열이 출력되는 경우 쌍따옴표로 감싸진 문자열이 출력됩니다. 이 옵션을 사용하는 경우 쌍따옴표 없이 문자열만 출력합니다. 객체나 배열의 경우 그대로 JSON 형식으로 출력됩니다.
--slurp, -s
다수의 JSON 입력을 받았을 때, 이를 배열로 연결해 하나의 입력으로 처리합니다.
--indent <n>
인덴트를 으로 지정합니다. 기본값은 2이고 최대값은 8입니다.

사용가능한 모든 옵션은 jq 공식 매뉴얼을 참고해주시기 바랍니다.

마치며

jq는 사용하면 사용할 수록 강력하게 느껴지는 도구 중 하나입니다. 아직 이 글에서는 jq의 반의 반도 설명하지 못 한 듯 합니다. jq 공식 매뉴얼을 열어보면 이 외에도 다양한 기능들을 찾아볼 수 있습니다. 이 글에서는 jq의 기본 문법과 작동 원리를 중심으로 설명했습니다만, 이후에는 쿡북 형식으로 jq 활용 방법에 대해서 소개하도록 하겠습니다.

더 읽을거리