Comando jq: filtrar e transformar JSON no Linux

Imagem destacada do artigo sobre o comando jq: o filtro jq '.[]' em destaque, com a frase «JSON domado na linha de comandos»

Peça a uma API a lista de utilizadores, exporte os eventos de autenticação de um serviço na cloud ou abra os alertas de um EDR: a resposta vem quase sempre em JSON. E o JSON, ao contrário de um log de texto, tem estrutura — objetos, arrays, vários níveis de aninhamento. O grep e o sed só veem caracteres; partir um JSON com eles é frágil e acaba mal à primeira chaveta fora do sítio.

O jq entende essa estrutura e permite filtrar, transformar e remodelar dados com uma linguagem própria, compacta e expressiva. Neste artigo percorremos a linguagem pela ordem do vídeo — campos, arrays, pipes e select, construção de saída, funções de transformação e opções da linha de comandos —, sempre com o mesmo ficheiro de exemplo.

Veja o guia em vídeo

Prefere ver antes de experimentar? No canal HJFR-IT no YouTube mostramos o jq opção a opção, com o resultado de cada comando no ecrã — narrado em português e com legendas.

jq — Guia Completo: processar JSON a partir do terminal — canal HJFR-IT no YouTube. Se o áudio não arrancar em português, abra as definições do vídeo (ícone da roda dentada) → Faixa de áudio → Português.

O que faz o jq

O jq lê documentos JSON (de ficheiros ou da entrada padrão), aplica-lhes um filtro e escreve o resultado. Cada filtro recebe um valor e produz zero, um ou vários valores, e os filtros encaixam uns nos outros. É a ligação entre o curl e o resto de um script: o curl vai buscar a resposta, o jq tira dela o que interessa.

jq [OPÇÕES] 'FILTRO' [FICHEIRO...]
comando | jq [OPÇÕES] 'FILTRO'

Escreva o filtro sempre entre plicas, para que a shell não interprete os $, os | nem as aspas. Nos exemplos usamos um users.json fictício, a resposta de uma API de identidade; o campo falhas conta os logins falhados nas últimas 24 horas:

{
  "name": "hjfr",
  "endereco": {"cidade": "Lisboa", "pais": "PT"},
  "users": [
    {"name": "aluno",  "role": "user",  "falhas": 2,  "mfa": true,  "ip": "192.0.2.10",   "email": "aluno@example.com"},
    {"name": "ana",    "role": "admin", "falhas": 0,  "mfa": true,  "ip": "192.0.2.11",   "email": "ana@example.com"},
    {"name": "backup", "role": "svc",   "falhas": 41, "mfa": false, "ip": "203.0.113.66", "email": "backup@example.com"},
    {"name": "rui",    "role": "admin", "falhas": 37, "mfa": false, "ip": "203.0.113.66", "email": "rui@example.com"},
    {"name": "sara",   "role": "user",  "falhas": 5,  "mfa": true,  "ip": "198.51.100.7", "email": "sara@example.com"}
  ]
}

O básico: o ponto e o acesso a campos

O filtro mais simples é um ponto, que significa «a entrada inteira». Parece inútil, mas o jq formata e indenta o JSON: uma resposta que chegou espremida numa só linha fica legível, e com cores no terminal.

  • jq . users.json — o documento inteiro, formatado.
  • jq '.name' users.json — o valor do campo name: "hjfr".
  • jq '.endereco.cidade' users.json — os campos encadeiam-se para descer níveis: "Lisboa". Um campo inexistente devolve null, sem erro.
  • jq -r '.name' users.json — o mesmo valor, cru: hjfr.

Repare: por omissão o jq escreve JSON, e uma cadeia em JSON vem entre aspas. Para usar o valor num script, quase sempre o queremos cru — é o que faz o -r (de raw), por exemplo em ORG=$(jq -r '.name' users.json). O ponto para navegar e o -r para o valor limpo aparecem em quase todos os comandos jq do dia a dia.

Arrays: iterador, índices e fatias

A peça central do jq é o iterador .[]: desmonta um array e produz cada elemento em separado. Quase tudo o resto se constrói a partir dele.

  • .users[] — emite os cinco objetos, um a um, já fora do array.
  • .users[0] — o primeiro elemento (os índices começam em 0; .users[-1] é o último).
  • .users[2:4] — uma fatia com os elementos 2 e 3 (o fim é exclusivo), devolvida como array.
  • .users[].name — iterador mais campo: o nome de todos os utilizadores de uma vez.
$ jq '.users[].name' users.json
"aluno"
"ana"
"backup"
"rui"
"sara"

length, keys e has

  • length — elementos de um array, chaves de um objeto ou caracteres de uma cadeia: .users | length devolve 5.
  • keys — as chaves de um objeto, por ordem alfabética (keys_unsorted mantém a original). Ideal para explorar um JSON desconhecido.
  • has("chave") — true ou false, consoante a chave exista: .users[0] | has("password") confirma que a API não expõe palavras-passe.

Pipes e select: consultas completas

Tal como a shell, o jq tem o seu próprio |, que passa cada resultado de um filtro ao seguinte: .users[] | .name lê-se «para cada utilizador, dá-me o nome». A função que mais se combina com ele é select(condição), que deixa passar só os valores em que a condição é verdadeira. Encadeando etapas, obtém-se uma consulta completa — como numa base de dados, mas sobre JSON:

# Contas com mais de 30 logins falhados (objetos completos)
$ jq '.users[] | select(.falhas > 30)' users.json

# Emails dos administradores
$ jq '.users[] | select(.role == "admin") | .email' users.json
"ana@example.com"
"rui@example.com"

Este padrão — iterar, selecionar, extrair — resolve a maioria das tarefas reais. As condições aceitam ==, !=, <, >, and, or e not.

Construir saída: objetos, arrays e texto

O jq também constrói JSON novo, na forma de que o passo seguinte precisa:

  • .users[] | {user: .name, mail: .email} — as chavetas criam um objeto só com os campos escolhidos, com os nomes que quiser. {name, falhas} é o atalho de {name: .name, falhas: .falhas}.
  • [.users[].name] — os parênteses retos fazem o inverso do iterador: recolhem os resultados num único array.
  • "\(.name): \(.falhas)" — interpolação: dentro de uma cadeia, \(expressão) insere o valor no texto.
$ jq -r '.users[] | "\(.name): \(.falhas)"' users.json
aluno: 2
ana: 0
backup: 41
rui: 37
sara: 5

Exportar para CSV com @csv

@csv converte um array numa linha CSV, com as cadeias entre aspas e escapadas (@tsv usa tabulações). Use-o sempre com -r, senão cada linha sai como uma cadeia JSON cheia de \"; acrescente > contas.csv e o relatório está pronto.

$ jq -r '.users[] | [.name, .role, .falhas] | @csv' users.json
"aluno","user",2
"ana","admin",0
"backup","svc",41
"rui","admin",37
"sara","user",5

Funções de transformação

A biblioteca de funções sobre arrays faz do jq um verdadeiro motor de processamento: agrega, ordena e calcula sem que seja preciso escrever um programa.

map, sort_by, group_by e unique

  • map(f) — aplica f a cada elemento e devolve um array novo: .users | map(.falhas) dá [2, 0, 41, 37, 5].
  • sort_by(.campo) — ordena objetos por um campo, do menor para o maior.
  • group_by(.campo) — junta em subarrays os objetos com o mesmo valor.
  • unique — ordena e remove duplicados: [.users[].ip] | unique devolve os quatro IP distintos.
# Que contas partilham o mesmo IP de origem? (-c: resultado numa linha)
$ jq -c '.users | group_by(.ip) | map({ip: .[0].ip, contas: map(.name)})' users.json
[{"ip":"192.0.2.10","contas":["aluno"]},{"ip":"192.0.2.11","contas":["ana"]},{"ip":"198.51.100.7","contas":["sara"]},{"ip":"203.0.113.66","contas":["backup","rui"]}]

add, cálculos e outras funções

add soma os números de um array (ou concatena cadeias e arrays) e, dividido por length, dá a média: [.users[].falhas] | add devolve 85 e [.users[].falhas] | add / length devolve 17. Há muitas mais:

  • min e max — o menor e o maior valor (min_by e max_by para objetos).
  • reverse — inverte a ordem; sort_by(.falhas) | reverse dá um «top» descendente.
  • flatten — achata arrays aninhados: [["ana","rui"],["backup"]] passa a ["ana","rui","backup"].
  • to_entries — converte um objeto num array de pares {"key", "value"}; from_entries faz o inverso.

Opções da linha de comandos

Para além do -r, estas são as opções que afinam a forma como o jq lê e escreve:

  • -c (compact) — cada resultado numa só linha: o formato JSON Lines, um objeto por linha, ideal para grep ou wc -l.
  • -s (slurp) — junta toda a entrada num único array, em vez de tratar cada documento à parte. Útil para combinar vários ficheiros ou um registo em JSON Lines.
  • -n (null input) — não lê entrada; o filtro constrói o JSON do zero.
  • --arg nome valor — passa um valor da shell para o jq como a variável $nome, sempre como cadeia e sem aspas dentro de aspas (--argjson para números ou JSON).
  • -e (exit status) — código de saída pelo último resultado: 0 se verdadeiro, 1 se false ou null, 4 se não houver resultado.
# Quantos logins falhados há no registo em JSON Lines?
jq -s 'map(select(.ok == false)) | length' auth.jsonl

# Gerar um alerta com um valor vindo de uma variável
jq -n --arg ip "$IP" '{alerta: "brute-force", ip: $ip}'

# Avisar se o administrador rui não tiver MFA
jq -e '.users[] | select(.name == "rui") | .mfa' users.json > /dev/null || echo "ALERTA: admin sem MFA"

jq em cibersegurança

Triagem de registos na linha de comandos. Registos de auditoria da cloud, alertas de EDR e eventos de autenticação saem hoje em JSON ou JSON Lines. Com o jq, perguntas como «que contas tiveram mais de 30 falhas?» ou «que IP aparece em várias contas?» respondem-se em minutos, no próprio servidor e sem copiar dados sensíveis para outro lado. O group_by(.ip) do exemplo revela o padrão de um password spraying: duas contas atacadas a partir do mesmo endereço.

Extrair indicadores de comprometimento. select com -r produz listas limpas de IP, contas ou hashes, prontas para uma lista de bloqueio ou para um sort | uniq -c. Com -c, cada evento continua a ser JSON válido numa linha; com @csv, a cronologia do incidente passa diretamente para o relatório.

Scripts seguros com –arg. Colar variáveis da shell dentro do filtro, como em jq ".users[] | select(.name == \"$CONTA\")", é um convite à injeção: um valor manipulado altera a própria consulta. Com --arg, o valor entra sempre como dado, nunca como código. Junte-lhe o -e para que as verificações automáticas — um administrador sem MFA, por exemplo — falhem de forma visível.

Experimente no simulador interativo

O simulador usa o mesmo users.json — cinco contas, duas atacadas a partir do mesmo IP — e um pequeno registo auth.jsonl. À esquerda vê cada etapa do filtro e o documento desenhado como árvore, com os nós apanhados realçados, os suspeitos a laranja e os descartados esbatidos; à direita, a saída no terminal. Clique nos filtros e opções, ou use Demonstrar tudo para percorrer a sequência completa do vídeo; Anterior e Seguinte avançam passo a passo e Repor estado volta ao início.

Erros comuns e boas práticas

  • Pedir um campo a um array: .users.name termina com Cannot index array with "name". Itere primeiro: .users[].name.
  • Esquecer o -r: as aspas do JSON acabam dentro de variáveis e nomes de ficheiro.
  • Pôr o filtro entre aspas duplas: a shell expande $ e \ antes de o jq os ver. Filtro entre plicas; valores externos com --arg.
  • Confundir .[] com [ ]: o iterador emite resultados soltos; os parênteses retos recolhem-nos num array, que é o que length ou sort_by esperam.
  • Tratar JSON Lines como um só documento: sem -s, cada linha é processada à parte e length conta as chaves de cada evento, não os eventos.
  • Partir JSON com grep e sed: funciona até a ordem dos campos ou a indentação mudarem. Use o jq para a estrutura e o grep para o texto que dele sai.

Conclusão

O jq transforma o JSON de obstáculo em matéria-prima: o ponto para navegar, o iterador para percorrer arrays, o pipe e o select para consultar, as chavetas e a interpolação para construir, e meia dúzia de funções e opções para agregar e automatizar. Domine o padrão iterar, selecionar, extrair e terá resolvido a maioria das tarefas reais. Para ir buscar o JSON a uma API, veja o nosso artigo sobre o curl e o wget; para pesquisar no texto que o jq produz, o guia do comando grep.

Workshop — Linha de Comandos Linux

Quer passar da leitura à prática, com exercícios reais e acompanhamento? No workshop Linha de Comandos Linux da HJFR trabalha estes e outros comandos da série — da administração do dia a dia à resposta a incidentes.

Este artigo faz parte da série «Comandos Linux, um a um» do blog HJFR — fique atento aos próximos comandos.