Percursos
/
jq
jq
/
Exercícios
/
Lista de Compras
Lista de Compras

Lista de Compras

Exercício de aprendizagem

Introdução

Noções básicas

Vamos assumir que estás familiarizado com a sintaxe e os tipos de dados do JSON.

O jq funciona passando os dados JSON de entrada por uma expressão única (escrita como um pipeline de filtros) para obter os dados transformados que pretendes.

Note

A linguagem jq é implementada pelo programa jq. Este programa oferece várias opções de linha de comandos úteis para controlar como os dados de entrada são consumidos e como a saída é apresentada.

Nos exemplos abaixo vais encontrar:

  • -n ou --null-input

    Normalmente, o programa jq recebe um ficheiro para ler, ou envias-lhe dados. A opção --null-input permite-te gerar dados JSON sem quaisquer dados de entrada.

  • -c ou --compact-output

    O jq imprime a saída formatada por omissão. É extremamente útil para os humanos ver os dados quando estão bem formatados. No entanto, isso não é necessário para as máquinas: a opção --compact-output remove os espaços em branco da formatação para minimizar o tamanho do JSON resultante.

  • -f filename ou --from-file filename

    Lê o programa jq a partir de filename em vez de o fornecer na linha de comandos. Tanto o sed como o awk usam a opção -f para o mesmo fim. Vais ver isto a ser usado nos scripts de teste dos exercícios de prática.

O resto desta lição vai focar-se na linguagem jq.

Filtros e pipes

Os filtros também são conhecidos como expressões.

Um filtro recebe um valor de entrada e produz um valor de saída. Tal como acontece quando trabalhas numa shell do Unix, podes encadear filtros com um pipe | para ligar a saída de um filtro aos dados de entrada de outro.

O filtro de identidade: .

Este é o filtro mais simples. Limita-se a passar o valor de entrada para a saída.

$ echo '[1, 2, 3]' | jq '.'
[
  1,
  2,
  3
]

Arrays

Esta é uma introdução rápida ao trabalho com arrays. Vamos abordar este tema com mais detalhe mais adiante.

Acede-se aos elementos de um array com parênteses retos, e a indexação começa em zero.

$ echo '[10, 20, 30]' | jq '.[1]'
20

Um filtro pode construir um array envolvendo uma expressão em [ e ]

  • com uma lista conhecida de elementos:

    jq -n '[1, 2, 3]'
    
  • para recolher um fluxo de elementos: por exemplo, o range é uma função que produz um fluxo de números

    $ jq -n 'range(10; 70; 15)'
    10
    25
    40
    55
    

    Usar [] recolhe os resultados da expressão num array

    $ jq -c -n '[range(10; 70; 15)]'
    [10,25,40,55]
    

A vírgula é um operador

A vírgula não é apenas sintaxe que separa elementos de um array. A vírgula é um operador que junta fluxos.

Por exemplo, [1, 2, 3] é um filtro que usa o construtor de array [] para recolher o resultado da junção das três expressões 1, 2 e 3.

Reparaste nos pontos e vírgulas em range(10; 70; 15) acima? Como as vírgulas têm um propósito específico na linguagem jq, as funções que recebem vários argumentos usam pontos e vírgulas para separar os argumentos.

Objetos

Uma introdução rápida aos objetos.

Tal como em muitas linguagens de programação, usa pontos para aceder às propriedades de um objeto

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

Também podes usar parênteses retos com objetos, mas nesse caso são precisas aspas para os literais de string. Esta é uma forma de trabalhar com chaves que contêm espaços.

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

Podes construir um objeto com {} e pares key: value. Não são precisas aspas à volta de chaves que sejam strings "simples".

jq -n '{question: (6 * 9), answer: 42}'

produz

{
  "question": 54,
  "answer": 42
}

Para tratar a chave como uma expressão, tens de a envolver em parênteses (o seguinte também produz o mesmo resultado que acima).

echo '[{"key":"question", "value":54}, {"key":"answer", "value":42}]' \
| jq '{(.[0].key): .[0].value, (.[1].key): .[1].value}'

Pipelines

Por exemplo, dado um file.json que contém

{
  "key1": "value1",
  "key2": [5, 15, 25]
}

Vamos calcular o comprimento do array key2:

$ jq '.key2 | length' file.json
3

Estamos a encadear a saída da expressão .key2 como valor de entrada para o length, que, como seria de esperar, produz o número de elementos do array.

Os filtros podem ignorar os dados de entrada

Neste exemplo, os dados JSON de entrada são ignorados e não têm qualquer impacto na saída:

$ echo '{"answer": 42}' | jq '6 * 9'
54

Os filtros podem produzir fluxos de dados

Um filtro pode produzir mais do que um valor. Por exemplo, o filtro .[] produz cada elemento de um array como um valor separado:

$ jq -n -c '[1, 2, 3]'
[1,2,3]

$ jq -n -c '[1, 2, 3] | .[]'
1
2
3

Encadear um filtro destes noutro executa o segundo filtro para cada valor:

$ jq -n -c '[1, 2, 3] | .[] | . * 2'
2
4
6

Isto é como uma iteração implícita. Quando compreenderes esta técnica, vais perceber que se podem escrever filtros jq muito poderosos de forma muito concisa.

Parênteses

Os parênteses servem para agrupar subexpressões e impor a ordem das operações, tal como noutras linguagens. No jq, a necessidade deles pode parecer um pouco surpreendente.

Por exemplo, imaginemos que queremos construir um array com 2 elementos: a raiz quadrada de 9 e e elevado à potência 1. As duas expressões individuais são 9 | sqrt e 1 | exp. Esperamos que a saída seja o array [3, 2.7...]

$ jq -n '[ 9|sqrt, 1|exp ]'
[
  20.085536923187668,
  2.718281828459045
]

Porque é que não obtivemos o que esperávamos? O jq interpreta isso assim:

[ ((9|sqrt), 1) | exp ]

O jq constrói um fluxo de dois elementos (3 e 1) que são ambos entregues ao exp.

Temos de garantir que o exp recebe apenas um número como valor de entrada. Por outras palavras, temos de garantir que o pipe é avaliado antes da vírgula.

$ jq -n '[ 9|sqrt, (1|exp) ]'
[
  3,
  2.718281828459045
]

Funções e operadores

Sem entrar em grandes detalhes (as funções serão tema de outro exercício), aqui ficam algumas funções incorporadas úteis:

  • length

    Dado um array como valor de entrada, produz o número de elementos desse array.

    $ jq -n '[10, 20, 30, 40] | length'
    4
    
  • +

    Este operador faz coisas diferentes consoante o tipo dos seus operandos: soma números, concatena strings, acrescenta arrays, combina objetos.

    $ jq -c -n '
        3 + 4,
        "foo" + "bar",
        ["a", "b"] + ["c"],
        {"m": 10} + {"n": 20}
    '
    7
    "foobar"
    ["a","b","c"]
    {"m":10,"n":20}
    

    O add é uma função que recebe um array e devolve um item com todos os elementos somados segundo as regras do +. [1, 2, 3] | add produz 6.

  • map

    Recebe um array como valor de entrada e um filtro como argumento, e produz um array em que o filtro é aplicado a cada elemento

    $ jq -c -n '[10, 20, 30, 40] | map(. / 5)'
    [2,4,6,8]
    
  • select

    Recebe um valor de entrada e um filtro como argumento:

    • se o filtro aplicado ao argumento der um valor verdadeiro, produz o valor de entrada sem alterações
    • caso contrário, não produz nada (não o valor null, mesmo nada)

    Por exemplo, dados alguns números, seleciona os que são divisíveis por 3

    $ jq -n 'range(10) | select(. % 3 == 0)'
    0
    3
    6
    9
    

    Lembra-te de que o range produz um fluxo de números. O select é invocado uma vez por cada número. Só são produzidos os números que "passam" a expressão.

    Muitas vezes precisas de selecionar elementos de um array. Há algumas formas de o fazer.

    Com o valor de entrada ["Anne", "Bob", "Cathy", "Dave"], seleciona os nomes com comprimento 4.

    • usa o map e o select em conjunto

      map(select(length == 4))
      
    • expande o array em elementos, faz select sobre esse fluxo e recolhe os resultados

      [ .[] | select(length == 4) ]
      

Comentários

Os comentários começam com o caráter # e continuam até ao fim da linha.

Instruções

Ficaste de repente com uma vontade enorme de comer panquecas. Como pessoa moderna com inclinação para a técnica, escreves uma lista de compras dos ingredientes das panquecas em formato JSON. A estrutura da lista é a seguinte:

{
  "name": "name of shopping list",
  "ingredients": [ ...list of ingredients... ],
  "optional ingredients": [ ...list of ingredients... ]
}

Um ingrediente é representado como um objeto JSON, assim:

{
  "item": "flour",
  "amount": {
    "quantity": 2,
    "unit": "cup"
  }
}

Um ingrediente tem uma propriedade opcional chamada "substitute" que contém uma string com outro item que pode ser usado em alternativa.

Vamos ver como podemos examinar a lista de compras com jq

1. Extrair o nome da lista de compras

Escreve uma expressão que devolva o elemento "name" da lista de compras.

2. Contar quantos ingredientes são necessários

Conta apenas os ingredientes "required". Não incluas os ingredientes opcionais.

3. Quanto açúcar?

Escreve uma expressão que devolva a quantidade de açúcar. Só é pedida a parte numérica.

4. Mapear os ingredientes que podem ser substituídos

Alguns dos ingredientes podem ser substituídos (se não tiveres o ingrediente X, podes usar o Y). Devolve um objeto JSON que mapeie o ingrediente recomendado para o seu substituto. Inclui os ingredientes opcionais no mapeamento.

Editar via GitHub A ligação abre numa nova janela ou separador
jq Exercism

Estás pronto para começar Lista de Compras?

Inscreve-te no Exercism para aprenderes e dominares jq com 12 conceitos75 exercícios, e mentoria humana real, tudo grátis.