4 pontos por GN⁺ 2023-09-10 | 1 comentários | Compartilhar no WhatsApp
  • Um projeto de explicação de longo prazo que lê a estrutura do código-fonte 8.3 do GNU coreutils utilitário por utilitário, voltado para programadores iniciantes que querem aprender design de ferramentas de linha de comando
  • Não é uma documentação de uso, e sim um material de apoio para leitura do código-fonte, cobrindo junto o namespace de cada comando, fluxo de execução, decisões de design, algoritmos, UNIX e a linhagem inicial do Coreutils
  • Como o coreutils tem muitos programas pequenos em C com propósito único, até padrões de código ásperos sob uma ótica moderna — como variáveis globais, macros, goto e funções longas — precisam ser lidos dentro do contexto histórico
  • O fluxo comum segue inicialização, parsing de opções, processamento de entrada, execução de chamadas de sistema e tratamento de falhas, com getopt_long(), fts e helpers do gnulib aparecendo repetidamente
  • Antes de contribuir ou modificar algo, é preciso verificar primeiro se isso já pode ser reproduzido com ferramentas existentes, se não quebra a compatibilidade retroativa e até que ponto diverge do POSIX

Escopo do projeto de explicação do GNU coreutils

  • É um projeto de longo prazo para explicar todos os utilitários do GNU coreutils 8.3
  • O público-alvo são programadores iniciantes interessados em examinar o design de utilitários de linha de comando
  • Funciona melhor como leitura de apoio ao consultar o source code do utilitário de interesse
  • Este material não é um manual de usuário; para uso real, é preciso consultar as man pages
  • O projeto é composto por várias fases
    • Phase 1: páginas concluídas com namespace e visão geral de execução de cada utilitário
    • Phase 2: concluídas as principais decisões de design e algoritmos, linhagem do UNIX e do Coreutils inicial, walkthroughs de código-fonte mais úteis e visualizações da evolução do código
    • Phase indefinite: walkthroughs linha por linha do código de cada utilitário continuam como trabalho de longo prazo
    • Há um repositório no GitHub para coletar notas linha por linha

Páginas de explicação por utilitário

  • Cada nome de comando leva para uma página detalhada de decodificação daquele utilitário
  • Cada página inclui discussão, código-fonte e walkthrough
  • Os utilitários em negrito são itens expandidos na Phase 2
  • A lista inclui comandos do GNU coreutils como arch, base64, cat, chmod, cp, date, dd, df, ls, rm, sort, tail, tr, wc, yes e outros

Premissas importantes antes de ler o código

  • Muitos utilitários do coreutils têm uma história de quase 30 anos e são código alterado por várias pessoas ao longo de muito tempo
  • A maioria são programas pequenos feitos para um único propósito
    • Em geral, são programas de um único arquivo-fonte
    • Não são códigos projetados para sobreviver por muito tempo ou crescer muito além de sua função original
    • Aparecem padrões como variáveis globais, macros, goto e funções longas com switch/loops aninhados
  • Conhecimento de POSIX é importante para entender o código
    • Vale a pena começar pelas Utility Syntax Guidelines
    • O POSIX define a interoperabilidade entre entrada e saída, mas deixa a forma real de execução a cargo da implementação
    • O GNU coreutils pode não seguir o POSIX à risca, mas conceitos como bits de permissão, uid/gid, variáveis de ambiente e status de saída estão profundamente presentes
  • Por questões de portabilidade, o coreutils depende fortemente do gnulib
    • Quase todos os utilitários incluem funções do gnulib projetadas para lidar com problemas comuns em vários sistemas
  • O coreutils é executado pressupondo suporte de shells como bash, zsh e ksh
    • O shell executa o utilitário via fork/clone, passa argumentos, configura o ambiente, redireciona entrada e saída com pipes e preserva o valor de encerramento
  • O GNU coreutils surgiu originalmente de três pacotes — utilitários de shell, texto e arquivos — e utilitários do mesmo tipo compartilham muitos padrões de design

Padrões básicos de design

  • A maioria dos utilitários CLI tem, em comum, três etapas
    • uma etapa de configuração que prepara flags, opções, localização etc.
    • uma etapa de parsing de argumentos que lê a entrada e define os parâmetros de execução
    • uma etapa de processamento/execução que prepara a entrada e a passa para uma ou mais chamadas de sistema
  • Durante a execução, o programa pode verificar restrições e falhar em vários pontos
    • Diferentes status de saída podem indicar onde o problema ocorreu
    • EXIT_FAILURE é usado com frequência como estado genérico de falha
  • Após uma falha, o utilitário fornece feedback ao usuário
  • As variações de utilitários se dividem em três grupos
    • Trivial utilities: definem algumas linhas de macros, incluem o código-fonte de outro utilitário e usam macros para forçar determinado controle de fluxo. Exemplos: arch, dir, vdir
    • Wrapper utilities: após a configuração e o parsing de opções, repassam diretamente opções da linha de comando como argumentos de chamadas de sistema. O resultado da chamada de sistema vira o resultado do utilitário, com pouco processamento próprio. Exemplos: link, whoami, hostid, logname
    • Full utilities: incluem configuração, parsing de opções e argumentos, processamento dos dados de entrada e execução de várias chamadas de sistema; a maioria dos utilitários está aqui

Inicialização comum e parsing de opções

  • Todos os utilitários têm um curto procedimento de inicialização no começo de main()
    • initialize_main (&argc, &argv);
    • set_program_name (argv[0]);
    • setlocale (LC_ALL, "");
    • bindtextdomain (PACKAGE, LOCALEDIR);
    • textdomain (PACKAGE);
    • atexit (close_stdout);
  • Esse código de pré-processamento lida com tarefas administrativas, como internacionalização e registro do comportamento de saída ao encerrar, sem afetar a lógica central de cada utilitário
  • A família de ferramentas Getopt tem papel central no parsing de opções da linha de comando
    • Opções curtas usam o prefixo -, e opções longas usam --
    • Opções curtas são definidas como string, e opções longas usam struct
    • Na string de opções curtas, a ausência de argumento é indicada apenas pelo caractere; argumentos obrigatórios recebem :, e opcionais recebem ::
    • A string de opções curtas Lln:s:t de kill significa que L, l e t não têm argumento, enquanto n e s exigem argumento
    • Em todos os utilitários, getopt_long() é usado para retornar a próxima opção
    • optind aponta para a posição do próximo argumento no array argv[]
    • optarg é um ponteiro de caractere para o valor do argumento da opção

Percurso de sistema de arquivos e helpers de chamadas de sistema

  • Em sistemas do tipo Unix, muitas vezes há suporte à biblioteca fts para facilitar o percurso do sistema de arquivos
    • fts_open() ou xfts_open() cria uma estrutura FTS a partir de um caminho
    • Nós de arquivo ou diretório na árvore são representados pela estrutura FTSENT
    • Ao chamar fts_read(), um FTSENT é gerado; esse processo é a travessia da árvore
    • O campo FTSENT->fts_info é usado com frequência para descrever o item e decidir como tratá-lo
  • Além do que a libc fornece, o coreutils usa muitos wrappers de chamadas de sistema e helpers
  • Em operações de escrita, além do wrapper da chamada de sistema write() e de funções da libc como fwrite(), aparecem funções não padronizadas como full_write()
    • full_write() continua tentando escrever enquanto não houver falha grave
    • safe_write() tenta novamente a chamada de sistema write() mesmo se houver interrupção
    • Também existem helpers de escrita usados só por um único utilitário, como iwrite() em dd e cwrite() em split

Funções comuns e idiomatismos de código

  • Todos os utilitários usam ao menos as três funções main(), usage() e _()
  • usage() exibe ajuda com parâmetros de entrada, significado e sintaxe correta
  • _() é uma macro definida em system.h que liga strings simples ao suporte Native Language Support do GNU gettext.h
    • Em geral, qualquer string exibida ao usuário é envolvida por essa macro
  • Na maioria dos utilitários não triviais, várias linhas de código comum se repetem
    • #include "system.h" define macros dependentes do sistema, variáveis e funções utilitárias não padronizadas
    • PROGRAM_NAME define o nome oficial do utilitário e é usado na verificação de versão
    • AUTHORS define os autores do utilitário e é usado na verificação de versão
    • emit_try_help() imprime uma sugestão de ajuda após uma saída de falha
    • emit_ancillary_info (PROGRAM_NAME) imprime informações adicionais de ajuda após a saída específica do comando
    • initialize_main(&argc, &argv) lida com a expansão interna de curingas no VMS e é removido na maioria dos outros sistemas operacionais
    • atexit(close_stdout) registra uma função para fazer flush e fechar streams com buffer quando o programa terminar
  • Também aparecem idiomatismos em C que podem soar estranhos para iniciantes
    • !! é o operador NOT unário duplo, usado para forçar a conversão de um valor para booleano
    • do { ... } while (0) é um padrão que não forma laço, usado para encapsular macros de múltiplas instruções e tornar a tokenização segura após a substituição do pré-processador

Forma de manutenção e verificações antes de contribuir

  • Projetos ativos como o coreutils costumam evoluir por três fluxos de manutenção
  • Mudanças em todo o projeto são alterações grandes que afetam a estrutura e as dependências de todos os utilitários
    • Em 1995, o GNU gettext project adicionou Native Language Support, introduzindo a macro _() na maioria das linhas de saída de texto
    • Em 1996, a expansão do suporte à internacionalização acrescentou vários códigos de inicialização em main()
    • Em 1995, saídas de uso passaram a incluir uma breve descrição do propósito do utilitário
    • Em 2003, foi adicionado suporte a curingas no VMS, visível na função initialize_main()
    • Em 2016, para evitar warnings do compilador em caminhos de falha, a macro die() substituiu a maior parte das funções exit() e error()
  • Mudanças por utilitário se dividem em correções de bugs, novos recursos e otimizações
    • join, sort e uniq eram vulneráveis a ataques de overflow até um patch de 2016
    • df recebeu a opção --output em 2013, adicionada
    • yes teve a performance melhorada com buffering melhor
  • Manutenção anual inclui ao menos a atualização do ano de copyright de todos os utilitários
    • Também há mudanças administrativas, como atualização do endereço da FSF, sem impacto na execução
  • Antes de contribuir, é recomendável consultar a página do projeto GNU, as diretrizes de contribuição, os recursos rejeitados e os arquivos da mailing list
  • Há três perguntas a verificar antes de escrever código
    • A mesma funcionalidade pode ser reproduzida com ferramentas existentes?
    • A contribuição quebra a compatibilidade retroativa?
    • O comportamento proposto se afasta muito do POSIX?
  • Em caso de dúvida, recomenda-se consultar a comunidade na mailing list

Detalhes interessantes

  • O utilitário mais curto é false, com 2 linhas, empatado com arch, dir e vdir
  • O utilitário executável independente mais curto é true, com 80 linhas, e sua primeira versão chega perto de um programa C mínimo
  • O utilitário mais longo é ls, com 5308 linhas
  • Muitos utilitários remontam ao Research UNIX dos anos 1970, e alguns vão até o Multics
  • O ancestral conceitual mais antigo é o comando LISTF do CTSS, por volta de 1963, depois encurtado para ls
  • A sintaxe peculiar de dd lembra a job control language do OS/360 do início dos anos 1960
  • sort é o único utilitário que usa multithreading
  • fmt mostra otimização de linhas e parágrafos com custo de função
  • yes implementa saída de alto desempenho com buffers de memória alinhados a página
  • df é mais rápido porque usa metadados de dispositivo, enquanto du precisa verificar todos os arquivos
  • cksum tem dois pontos de entrada: execução normal e geração de tabela CRC-32
  • echo não tem condição de falha
  • O design de test e expr difere bastante da estrutura típica de utilitários
  • su foi originalmente mantido em coreutils/shellutils

Implementações que valem a visita

Informações sobre apoio

  • Não há configuração para receber apoio individual
  • Se quiser compartilhar tempo ou dinheiro, você pode contribuir com a Free Software Foundation

1 comentários

 
GN⁺ 2023-09-10
Opiniões no Hacker News
  • Mesmo dizendo que “muitos desses utilitários têm quase 30 anos e foram modificados por muita gente nesse período”, e que “não foram projetados para se expandir além de sua longa vida útil ou de seu papel”, eu gostaria de ver exemplos do que o autor considera programas pensados para uma longa vida útil
    Tenho curiosidade tanto sobre programas que sobreviveram por 30 anos quanto sobre programas que ele acha que vão durar mais 30

    • A questão é qual é exatamente esse papel. Fico pensando se há limites para o que dá para fazer com essas ferramentas
      Para testar uma pequena linguagem de programação que criei, montei um framework de testes com bash e coreutils; no começo fiquei desconfortável por não usar uma linguagem “de verdade”, mas na prática funcionou muito bem e também executava em paralelo
      A única coisa que não consegui testar foi o argv[0] do programa; nenhuma combinação produzia exatamente o comportamento que eu queria, então enviei uma solicitação de recurso e um patch para o env no coreutils: https://lists.gnu.org/archive/html/coreutils/2023-08/msg0006...
      Parece que será incorporado, ou seja, um recurso novo entrando em um programa antigo
    • Acho que aqui a ideia é “feito para ser desenvolvido ativamente por muito tempo”
      As práticas listadas pelo autor são, em programas grandes, geralmente criticadas por prejudicarem a manutenibilidade conforme a escala aumenta
    • Provavelmente ele quis dizer “longa vida de desenvolvimento”. Ou seja, utilitários que são basicamente escritos uma vez e usados por muito tempo, o que também é uma vantagem do estilo “faça uma coisa e faça bem”
    • Curiosamente, interpretei “long life” como um programa que fica rodando continuamente. Entendi como um contraste com ferramentas que têm entrada clara, execução curta e saída clara
    • Emacs?
  • Materiais que valem consultar:
    Como o GNU coreutils é testado: https://www.pixelbeat.org/docs/coreutils-testing.html
    Exploração de cada comando do coreutils: https://ratfactor.com/slackware/pkgblog/coreutils
    Processamento de texto na linha de comando com GNU Coreutils: https://learnbyexample.github.io/cli_text_processing_coreuti... — meu e-book que aborda mais de 20 ferramentas de processamento de texto

  • Posts relacionados:
    Decoded: GNU Coreutils (2018) - https://news.ycombinator.com/item?id=29871037 - janeiro de 2022, 7 comentários
    Decoded: GNU coreutils (2019) - https://news.ycombinator.com/item?id=26411908 - março de 2021, 38 comentários
    Decoded: GNU Coreutils - https://news.ycombinator.com/item?id=20328650 - julho de 2019, 55 comentários

  • Curiosidade: ao instalar o coreutils via Homebrew no macOS, como o macOS já tem od(1), o od do coreutils é instalado como god(1)

  • Se o autor estiver lendo, há pelo menos um erro. A descrição curta da página do shred[0] na verdade é a descrição do csplit[1], e deveria ser algo como “sobrescreve o conteúdo de um arquivo para ocultá-lo e, opcionalmente, o remove”
    [0]: https://maizure.org/projects/decoded-gnu-coreutils/shred.htm...
    [1]: https://maizure.org/projects/decoded-gnu-coreutils/csplit.ht...

  • Eu não sabia que isso existia; é legal. Mesmo algo simples como yes é bem interessante para ver como é escrito o código básico de um utilitário que escreve na saída padrão
    https://maizure.org/projects/decoded-gnu-coreutils/yes.html

    • Meu ponto é que até um utilitário simples merece ser bem escrito e otimizado
      Por exemplo, é bom ter um exemplo mínimo de como escrever saída muito rapidamente. Como o programa é bem curto, a contagem de linhas não importa muito; se o autor sabe como fazer, acho melhor torná-lo o mais rápido possível para não atrapalhar
    • Uma implementação em Go feita por diversão seria assim:
      package main
      
      import "flag"
      
      func main() {  
      yes := flag.String("m", "y", "message")  
      flag.Parse()  
      for {  
      println(*yes)  
      }  
      }  
      
  • É uma lista de utilitários básicos bem divertida. Uso UNIX há muito tempo, mas nunca tinha ouvido falar de coisas como shred, shuf e factor
    Dá vontade de rodar uma vez

    sudo find / -type f -exec shred {} \;  
    

    e ver quanto tempo leva até ele se matar. Claro, em uma máquina virtual ou em um equipamento que dê para reflashear facilmente

    • Fiz algo parecido com dd, mas ele foi até o fim, o que foi bem decepcionante
      Eu esperava que travasse ou pelo menos perdesse a conexão, mas o kernel, o sshd e o bash ainda estavam na memória, então voltei para um prompt em que não dava para fazer nada direito
    • No Linux, provavelmente há uma boa chance de ir até o fim. Não dá para escrever em executáveis e bibliotecas atualmente em execução (ETXTBUSY), então o shred não consegue estragar a si mesmo nem o find
  • Gosto do fato de que /bin/true pode, na prática, falhar e retornar false. Tecnicamente, chamar “Not /bin/false” acaba sendo mais robusto: https://github.com/coreutils/coreutils/blob/master/src/true....
    Claro que sei que é um caso que quase nunca vai acontecer, mas é engraçado

    • Outro ponto interessante sobre o comando true é que ele ficou muito mais complexo do que precisava
      Primeiro, um exercício:
      touch mytrue  
      chmod u+x mytrue  
      ./mytrue  
      echo "error code for mytrue is $?"  
      
      O true literalmente começou assim. Bem zen
      A primeira mudança veio por causa do jurídico. Todo código precisava ter um aviso de copyright, e arquivos vazios também precisavam. Então um arquivo sem nada virou um arquivo que continha apenas um aviso de copyright, o que levanta a pergunta: “é possível reivindicar copyright sobre o nada?”. A AT&T tentou
      Depois alguém disse que programas deveriam ser bem definidos e não depender de um comportamento acidental do Unix, o que naquele momento era bem razoável. Então o true finalmente ganhou código, e esse código era exit 0
      Depois alguém disse que utilitários de sistema deveriam ser escritos em C em vez de shell para serem mais rápidos, e o OpenBSD ainda tem um bom exemplo dessa forma: http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr....
      Em algum momento, a burocracia do GNU entrou em cena e disse que todos os programas tinham que dar suporte à flag -h; depois, que todos os programas tinham que dar suporte a locale, e por isso o true do GNU hoje tem, surpreendentemente, 80 linhas: https://github.com/coreutils/coreutils/blob/master/src/true....
      Dá até para achar isso aceitável, mas é bastante código para um programa que, por definição, “não faz nada e termina com sucesso”
      http://trillian.mit.edu/~jc/humor/ATT_Copyright_true.html
    • Isso não acontece só quando ele é executado com opções? Se você simplesmente executa true, parece não haver impacto, a menos que o comentário esteja confundindo
  • Se alguém é um programador iniciante tentando desenvolver uma noção de como aplicar estruturas de dados e algoritmos de forma útil, há algo aqui que valha especialmente a pena olhar?

    • A maior parte da esperteza dentro do coreutils está mais em como ele interage de forma eficaz com o kernel do que em estruturas de dados e algoritmos
      Por exemplo, usar copy_file_range() em vez de read()/write() para evitar copiar dados para o espaço de usuário. É mais engenharia de software do que ciência da computação
    • Posso ter entendido errado, mas não vejo que relação estruturas de dados e algoritmos têm com ferramentas de linha de comando
  • Posso ter perdido a ideia central deste site, mas já não existem man pages ou páginas info para cada um deles?

    • Não é informação de uso; este site entra em detalhes sobre como os programas funcionam internamente