2 pontos por GN⁺ 2023-12-10 | 1 comentários | Compartilhar no WhatsApp
  • JC é uma ferramenta que converte a saída de várias ferramentas CLI, formatos de arquivo e strings comuns em JSON, facilitando o parsing em scripts
  • Recebe entrada via pipe e envia JSON para STDOUT, além de oferecer suporte à magic syntax ao ser prefixado a um comando, como em jc dig example.com
  • A saída padrão usa um schema estrito por parser e converte números, null, booleanos e campos semânticos adicionais; é possível acessar o JSON bruto antes do pré-processamento com -r ou raw=True
  • Também pode ser usado como biblioteca Python, retornando o resultado como dicionário Python, lista de dicionários ou iterable preguiçoso em vez de JSON
  • Para pipelines grandes ou de longa duração, oferece streaming parsers que produzem JSON Lines/NDJSON, além de --slurp, --meta-out, plugins locais de parser e opções para suprimir avisos de compatibilidade de plataforma

O que o JC faz

  • JC (JSON Convert) converte a saída de várias ferramentas CLI, formatos de arquivo e strings comuns em JSON
  • A saída convertida pode ser encadeada via pipe com ferramentas como jq ou jello para processamento adicional
  • Um exemplo é dig example.com | jc --dig, que converte a saída de dig em um array JSON, seguido de jq -r '.[].answer[].data' para extrair apenas os endereços IP
  • O mesmo trabalho também pode ser executado com magic syntax, como em jc dig example.com | jq -r '.[].answer[].data'

Como usar via CLI e biblioteca Python

  • O uso básico na CLI recebe entrada por pipe e gera uma representação em JSON
    • COMMAND | jc [SLICE] [OPTIONS] PARSER
    • cat FILE | jc [SLICE] [OPTIONS] PARSER
    • echo STRING | jc [SLICE] [OPTIONS] PARSER
  • A magic syntax usa o formato jc [SLICE] [OPTIONS] COMMAND ou jc [SLICE] [OPTIONS] /proc/<path-to-procfile>, prefixando jc ao comando ou arquivo em /proc
    • aliases de comando e shell builtins não são suportados
  • Em Python, pode ser chamado como jc.parse('dig', cmd_output)
    • O valor retornado não é uma string JSON, mas pode ser um dicionário Python, uma lista de dicionários ou um iterable preguiçoso no caso de streaming parsers
  • A documentação do pacote Python pode ser consultada com help('jc'), help('jc.lib') ou na documentação online

Representação de saída e schema

  • A representação padrão usa um schema estrito específico para cada parser
    • Números conhecidos são convertidos para valores JSON int ou float
    • Valores None conhecidos são convertidos para JSON null
    • Valores booleanos conhecidos também são convertidos
    • Alguns parsers adicionam campos semânticos extras
  • O JSON bruto antes do pré-processamento pode ser acessado com a opção -r na CLI ou parse(..., raw=True) na biblioteca Python
  • O schema de cada parser pode ser visto nos links de documentação ao lado da lista de parsers
  • A saída JSON é compacta por padrão, e a opção -p permite usar pretty format
  • Também é possível gerar saída em YAML com -y ou --yaml-out

Parsers suportados e exemplos de uso

  • O suporte inclui comandos CLI, formatos de arquivo e parsers de string
  • A lista de parsers no README inclui itens como dig, ls, ping, ps, netstat, ifconfig, csv, xml, yaml, /etc/hosts, /etc/passwd, /proc/, systemctl, git log, jwt, url, semver e arquivos relacionados a x509
  • As saídas de exemplo mostram como várias entradas são convertidas em estruturas JSON
    • A saída de arp é convertida em campos como endereço, tipo de hardware, endereço MAC e interface
    • Arquivos CSV são convertidos em arrays de objetos com os cabeçalhos como chaves
    • /etc/hosts é convertido em IP e arrays de hostname
    • ifconfig é convertido em objetos contendo nome da interface, MTU, endereços IPv4/IPv6, contadores de pacotes e bytes etc.
    • ping é convertido em objetos com contagem de pacotes enviados e recebidos, taxa de perda, tempo de ida e volta e lista de respostas
  • No Ansible, pode ser usado como filter plugin da coleção community.general

Como instalar

  • jc pode ser instalado de várias formas
    • pip3 install jc
    • repositórios de pacotes do sistema operacional
    • binários por arquitetura no GitHub Releases
  • Exemplos de instalação por pacote de sistema incluem:
    • Debian/Ubuntu: apt-get install jc
    • Fedora: dnf install jc
    • openSUSE: zypper install jc
    • Arch: pacman -S jc
    • macOS: brew install jc
    • FreeBSD: instalação via ports
    • Ansible filter plugin: ansible-galaxy collection install community.general
    • FortiSOAR connector: instalação pelo FortiSOAR Connector Marketplace

Principais opções

  • -a / --about: mostra informações sobre jc e o parser em JSON ou YAML
  • -d / --debug: exibe mensagens de rastreamento quando há problemas de parsing; -dd fornece depuração mais detalhada
  • -h / --help: mostra a ajuda, e jc -h --parser_name exibe a documentação do parser
  • -M / --meta-out: adiciona à saída metadados como timestamp, nome do parser, magic command e código de saída do magic command
  • -q / --quiet: suprime avisos do parser; -qq ignora erros de streaming parsers
  • -s / --slurp: agrupa entradas de múltiplas linhas em um array
  • -u / --unbuffer: desativa o buffering da saída
  • -B / --bash-comp, -Z / --zsh-comp: gera scripts de completion para Bash ou Zsh

Slice e Slurp

  • Slice processa apenas parte das linhas de entrada com a sintaxe START:STOP, semelhante ao slicing do Python
  • Por exemplo, em uma tabela com cabeçalho e rodapé, é possível converter apenas os dados centrais em JSON com jc 1:-1 --asciitable ou jc 1:4 --asciitable
  • Slices positivos e slices vazios são os mais eficientes em memória, enquanto slices negativos usam mais memória
  • Slurp é um recurso para parsers de string que recebem uma única linha, permitindo emitir múltiplos itens de uma vez em um array
    • Exemplo: processar um arquivo com vários endereços IP, um por linha, usando jc --slurp --ip-address
  • A magic syntax de /proc oferece suporte automático a slurp ao selecionar múltiplos arquivos
    • Ao converter vários arquivos de /proc, um campo _file é adicionado para indicar a qual arquivo cada objeto de resultado corresponde
  • Ao usar --meta-out com slurp, a saída vem envolvida em uma estrutura com array result e objeto _jc_meta

Códigos de saída e metadados

  • Erros fatais internos do jc geram código de saída 100; caso contrário, retorna 0
  • Ao usar magic syntax, o jc armazena o código de saída do programa analisado e o soma ao código de saída do próprio jc
    • Se ifconfig retornar 1 e jc retornar 0, o código combinado será 1
    • Se ifconfig retornar 0 e jc retornar 100, o código combinado será 100
    • Se ambos tiverem erro, o código combinado será 101
  • Em magic syntax, ao usar --meta-out ou -M, o objeto _jc_meta inclui informações do magic command e o código de saída

Cores e variáveis de ambiente

  • A variável de ambiente JC_COLORS permite definir as cores de key name, keyword, number e string
    • O formato é JC_COLORS=<keyname_color>,<keyword_color>,<number_color>,<string_color>
    • Os valores possíveis são black, red, green, yellow, blue, magenta, cyan, gray, brightblack, brightred, brightgreen, brightyellow, brightblue, brightmagenta, brightcyan, white, default
  • Ao definir a variável de ambiente NO_COLOR, a saída colorida é desativada
  • A opção -C tem prioridade sobre a variável NO_COLOR e sobre a opção -m, forçando a saída com cores

Streaming parsers

  • A maioria dos parsers lê toda a STDIN para a memória, faz o parsing e então serializa a saída como documento JSON
  • Alguns streaming parsers processam a entrada linha por linha assim que a recebem e produzem JSON Lines ou NDJSON
    • Exemplos: ls-s, ping-s
    • Isso pode reduzir bastante o uso de memória em saídas grandes, como ls -lR /, e em alguns casos também acelerar o processamento
  • Streaming parsers não podem ser usados com magic syntax
  • Para evitar que erros de parsing quebrem o pipe em pipelines de longa duração, use -qq ou ignore_exceptions=True em Python
    • Linhas bem-sucedidas incluem _jc_meta.success: true
    • Linhas com falha incluem _jc_meta.success: false, error e line
  • Se a saída demorar a aparecer entre pipes por causa do buffer do sistema operacional, use -u para saída sem buffer
    • Porém, em streams grandes, a saída sem buffer pode ser mais lenta
  • Em Python, streaming parsers recebem entradas iteráveis e retornam objetos iteráveis, permitindo processamento preguiçoso

Plugins de parser

  • Um parser plugin local pode ser colocado na pasta jc/jcparsers do diretório de dados do aplicativo
    • Linux/unix: $HOME/.local/share/jc/jcparsers
    • macOS: $HOME/Library/Application Support/jc/jcparsers
    • Windows: $LOCALAPPDATA\jc\jc\jcparsers
  • O plugin é um arquivo de módulo Python padrão
  • jc/parsers/foo.py ou jc/parsers/foo_s.py podem ser usados como template
  • O nome do arquivo do plugin deve ser um nome de módulo Python válido, começar com letra e conter apenas caracteres alfanuméricos e underscore
  • Plugins locais podem sobrescrever o parser padrão

Localidade, fuso horário e compatibilidade

  • Para melhores resultados, recomenda-se definir LC_ALL como C ou en_US.UTF-8
  • Em alguns sistemas antigos, o locale C não oferece suporte à codificação UTF-8, o que pode degradar a saída UTF-8 para ASCII com sequências de escape \\u
  • Alguns parsers adicionam campos calculados de timestamp epoch
    • Se o nome do campo não tiver o sufixo _utc, ele é tratado como timestamp ingênuo baseado no fuso horário local
    • Se o fuso UTC for detectado no texto da saída do comando, o timestamp será timezone-aware e a chave receberá o sufixo _utc
    • Fusos diferentes de UTC não são suportados como timestamps aware
  • Alguns parsers convertem saídas específicas de plataforma, então ao executá-los em plataformas não suportadas um aviso é exibido
  • Mesmo em plataformas não suportadas, ainda é possível fazer parsing de arquivos de saída gerados em outro sistema; nesse caso, o aviso pode ser suprimido com -q ou quiet=True em Python
  • As plataformas testadas incluem Centos 7.7, Ubuntu 18.04/20.04, Fedora32, macOS 10.11.6/10.14.6, NixOS, FreeBSD12, Windows 10, Windows Server 2016 e Windows Server 2019

1 comentários

 
GN⁺ 2023-12-10
Comentários do Hacker News
  • No FreeBSD, esse problema já foi resolvido em certa medida com a libxo:
    É possível obter saída estruturada, como em $ ps --libxo=json | jq
    Não é perfeito: havia suporte a ls, mas ele foi removido por algum motivo, e nem todos os utilitários o suportam
    O jc parece uma excelente solução temporária, oferecendo parsers para muitos comandos, mas tem a limitação de fazer parsing de saída de texto que, para começo de conversa, não foi projetada para isso
    Seria bom se os utilitários convergissem para emitir saída estruturada por meio de uma flag comum; mesmo que tornar isso o padrão, como no PowerShell, seja exagero para Unix/Linux, só uma flag --json padrão já seria um grande avanço
    https://wiki.freebsd.org/LibXo
    https://reviews.freebsd.org/D13959

    • O PowerShell vai um passo além do JSON e dá suporte a objetos mutáveis reais
      Pelo que entendo, ele não apenas passa dados estruturados adiante, mas deixa objetos opacos fluírem pelo pipeline e permite até voltar a etapas anteriores para chamar métodos: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_methods?view=powershell-7.4
      Gosto de wrappers como jc e libxo, e também de shells experimentais como https://www.nushell.sh/, mas eles se concentram mais em transmitir dados do que em objetos com métodos executáveis
      Dados estruturados ainda parecem algo no estilo Unix; se você precisa de objetos de verdade, acho que é hora de abrir Python ou Ruby
      Por melhor que um shell seja, e por melhores que sejam seus recursos de programação, é importante saber quando passar de um script de shell para uma linguagem de programação de verdade
    • O post de blog escrito pelo autor original trata exatamente disso
      O jc é apresentado como uma ferramenta que vem preenchendo esse papel, e dá para entender que ele foi pensado como uma ponte até que o suporte a -j/--json se espalhe amplamente pelas ferramentas Unix
      https://blog.kellybrazil.com/2019/11/26/bringing-the-unix-philosophy-to-the-21st-century/
    • No SerenityOS, de forma parecida, os itens sob /proc retornam dados JSON em vez de arquivos de texto não estruturado
      Uma solução mais estrutural poderia ser permitir que estruturas de dados fossem exportadas a partir de ELF, serializar essas estruturas para a saída do terminal e então fazer com que o usuário as emitisse ou processasse no formato desejado, como JSON ou YAML
    • A libxo é ótima em teoria, mas parece que, em vez de a aplicação passar a estrutura para a libxo e deixar a formatação por conta dela, é preciso implementar diretamente a lógica para cada formato de saída
      Não lembro exatamente qual utilitário era, talvez iostat, mas ele formatava linhas de saída JSON por interpolação de strings e, com uma certa combinação de flags, gerava uma saída completamente quebrada
      Não sei se melhorou hoje, mas quando há uma opção de intervalo eu esperaria algo como JSON lines
      Em termos de usabilidade, acho que PowerShell e kubectl estão muito à frente da libxo
    • A libxo faz parte do sistema base do FreeBSD, mas também pode ser usada de forma geral
      https://github.com/Juniper/libxo
      https://libxo.readthedocs.io/en/latest/
  • A ideia é muito boa, mas fico apreensivo pensando em manutenção
    Considerando versões, mudanças de saída dependendo das flags dos comandos etc., parece um inferno de manutenção; na prática, deve funcionar bem em alguns casos, mas a novidade provavelmente se desgasta rápido quando se vai além dos casos básicos
    Além disso, usar -- nas opções da ferramenta também não parece bom
    Se cada novo parser precisar de uma nova flag, a ajuda ou as páginas de manual podem acabar com milhares de linhas

    • Parece razoável enxergar isso como um shim cujo escopo de responsabilidade vai diminuir à medida que utilitários de linha de comando passem cada vez mais a suportar saída em JSON
      Quando algum utilitário decidir oferecer sua própria exportação em JSON, a partir daí esta ferramenta pode simplesmente delegar para essa funcionalidade
    • Se você ler a documentação mais abaixo, também dá para prefixar o comando com jc, como em jc ls
      O parâmetro --cmd permite processar os dados antes da conversão, então é uma boa ideia
      Por exemplo, você pode querer aplicar grep à lista antes de convertê-la
      Também do ponto de vista de manutenção, a saída dos comandos Unix básicos, se mudasse muito, quebraria não só esta ferramenta como inúmeros scripts; por isso, não acho que vá quebrar com tanta frequência quanto se imagina por causa de atualizações de outros binários
    • Por outro lado, fico dividido
      Um único conjunto comum de parsers bem mantido é melhor do que parsers improvisados de saída espalhados por aí, mas, em situações complexas em que eu precisaria de saída JSON direta, eu preferiria que o parsing em si não acrescentasse problemas
      Para usar isso com tranquilidade, no fim eu provavelmente teria que enviar PRs com casos de teste adicionais para tudo que eu pretendesse usar
      De todo modo, eu teria que escrever esses testes por conta própria
    • Isso exige colaboração
      A única saída é as pessoas enviarem informações de parsing das ferramentas de que precisam, e quem usa conseguir manter tudo atualizado com facilidade
    • Este é um dos melhores usos para LLMs, que demonstraram boa capacidade de transformar texto não estruturado em objetos estruturados
  • O Nushell segue uma abordagem diferente, mas acaba chegando em grande parte ao mesmo ponto: dados estruturados de comandos do shell
    Principalmente de uma forma em que o próprio shell assume esse papel
    http://www.nushell.sh/

    • O Nushell tem a operação from json, então ele combina muito bem com o JC na prática
      Há um tempo gravei um vídeo mostrando alguns bons recursos do Nushell e, por volta dos 19 minutos, falo sobre combiná-lo com jc: https://www.youtube.com/watch?v=KF5dtxVsn1E
    • Depois do primeiro anúncio do Nushell, dei uma olhada nele de vez em quando, mas só um ou dois meses atrás finalmente entendi direito a ideia central
      Eu estava tentando escrever um script para varrer arquivos dentro de diretórios que correspondessem a um certo padrão e remover aqueles cujos horários de modificação estivessem a menos de 10 minutos entre si, e me lembrei de que o Nushell era bom para esse tipo de coisa
      Mexi nele por um instante, finalmente caiu a ficha, e agora estou viciado
      Mesmo dados não estruturados ficam poderosos se puderem ser convertidos e processados em uma forma como lista de registros
  • Em certo sentido, é excelente que arquivos existam em todo lugar; essa é a promessa do Unix e, no Plan 9, isso aparece de forma ainda mais expandida
    Mas o fato de eles serem arquivos não estruturados, ou de cada arquivo ter seu próprio formato, também atrapalha na mesma medida
    Até tentar analisar um único arquivo de log do nginx só com algo como awk pode ser trabalhoso
    Uma das grandes desvantagens é que é difícil executar reescritas de sistemas em larga escala ou mudanças de design no espaço de usuário do Linux
    Quero um shell mais inteligente, gosto de arquivos e também tenho um livro de awk ali do lado, mas acho que já passou da hora de uma melhoria séria em análise de dados
    Seria bom se programas pudessem decidir emitir uma saída estruturada ou não, assim como já decidem se devem renderizar saída colorida

    • Parte do problema é que a saída de comandos é ao mesmo tempo interface de usuário e API
      Como qualquer UI textual pode ser usada como API, o texto fácil de ler para humanos acaba ganhando prioridade
      Por isso, scripting em shell parece criar uma extensão de navegador de terceiros
      Você observa e adivinha, improvisa um parser com base nessas suposições e torce para funcionar
      Seria bom ter uma terceira saída padrão para conteúdo legível por máquina
      O terminal não a exibiria por padrão; ao usar pipe, essa saída seria encaminhada; ela deveria ser JSONL; e as man pages especificariam o contrato
      Assim, a saída padrão poderia continuar sendo para humanos, e o parsing passaria a ser algo feito sabendo que é possível, mas frágil
      Claro que essa é uma ideia que quebra totalmente a compatibilidade retroativa; se fôssemos modernizar o CLI de forma irrealista, reinventando-o desde a base, haveria uma longa lista de coisas que eu gostaria de mudar
    • Há meio século surgiram ideias bem boas, e é frustrante que muita gente as trate como evangelho, não como algo a ser melhorado
      Conseguir melhorias também é realmente difícil, e quando alguém como Lennart tenta remover velhas sobras de décadas, o drama começa imediatamente
      JSON também ainda não é perfeito
      JSON é uma ideia razoavelmente boa, mas, para fazer direito, seria necessário um formato capaz de expor coisas como tipos de dados
      Teria de ser algo mais próximo do PowerShell, para tratar números como números e fazer coisas surpreendentes como calcular a diferença entre datas com $a - $b
    • Uma melhoria mais fácil seria implementar saída JSON como argumento de linha de comando em todas as ferramentas CLI
      Seria bom ver isso acontecer no GNU coreutils
    • Sempre existe a opção do PowerShell
      O problema é que ele está profundamente enraizado em .NET e objetos, o que torna muito difícil integrá-lo com comandos nativos existentes em qualquer plataforma
    • Em parsing de logs de servidor, é uma pena não poder extrair funcionalidades de algo como o logstash
      Porque ele já está essencialmente fazendo a mesma coisa
      Ainda assim, o objetivo final provavelmente é que as ferramentas de nível superior reconheçam o valor disso e forneçam diretamente saída estruturada
  • Legal
    Apoio fortemente ferramentas CLI que ofereçam uma saída JSON razoável, e coisas como https://github.com/WireGuard/wireguard-tools/blob/master/contrib/json/wg-json e o |ConvertTo-Json do PowerShell representam uma grande parte da automação de administração/monitoramento
    Mas aqui razoável carrega muito peso, e a realidade é a realidade
    Como no jeito da LSI/Broadcom StorCLI de acrescentar J depois do comando, ou nos wrappers do PowerShell que escondem COM: tecnicamente é JSON, mas o resultado é absurdamente complexo ou inútil, então muitas vezes se acaba voltando ao remendo de “vamos só aplicar umas regexes na saída em texto puro”
    Ainda assim, pretendo dar uma olhada nisso
    Se o primeiro exemplo, o parsing da saída do dig, representar algo que dá para fazer de forma estável, parece bem interessante

  • Acho que jc dig example.com deveria ser a sintaxe padrão
    Porque dig example.com | jc --dig exige adivinhar depois os flags e parâmetros do comando anterior para poder fazer o parsing da saída

  • O fato de toda saída ser um objeto é uma das minhas partes favoritas do PowerShell
    Sinto falta disso sempre que preciso escrever scripts em bash

  • Respeito a quem decidiu manter isso

    • Respeito também a quem decidiu usar essa ferramenta lidando de verdade com os pontos em que ela faz suposições erradas
    • Fico curioso para saber como vão lidar com problemas de versão
      Algo como aws s3 ls | jc --aws=1.2.3 seria um pesadelo
    • Boa observação
      Isso me lembra o programa file do Linux ou Unix e os heróis que o mantêm
    • Em teoria, se ele pudesse carregar algo como plugins, por exemplo deixando-os como comandos de shell separados, daria para transferir parte do esforço de manutenção para os autores de plugins
  • Fico curioso se há uma lista de ferramentas modernas de linha de comando Unix que aceitam a opção --json
    Também poderia ser útil adicionar esse tipo de informação a este repositório

    • No FreeBSD, praticamente quase tudo oferece suporte por meio da libxo
    • Não há uma lista, mas o ip, substituto moderno do ifconfig, tem suporte a JSON
      lldpctl também tem suporte
      O Ansible fornece detalhes do sistema como JSON chamado facts, e foi projetado para que a automação use isso
    • lsblk aceita a flag --json e pode fornecer muitas informações
      Basta tentar lsblk --json --output-all
      É muito útil quando um script precisa verificar os discos e partições do sistema
    • O TShark, ferramenta CLI complementar do Wireshark, oferece suporte com a flag -T json
    • Talvez não seja a resposta pretendida, mas existe a AWS CLI
  • Projeto interessante
    Mas imaginei que usariam algo como textfsm como parser de primeira etapa
    O textfsm é muito usado para fazer parsing da saída de CLI de equipamentos de rede
    https://github.com/google/textfsm