Decodificando o GNU Coreutils (2018)
(maizure.org)- 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,
gotoe 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(),ftse 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,yese 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,
gotoe funções longas comswitch/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,zsheksh- 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
- 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:
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:tdekillsignifica queL,letnão têm argumento, enquantonesexigem argumento - Em todos os utilitários,
getopt_long()é usado para retornar a próxima opção optindaponta para a posição do próximo argumento no arrayargv[]optargé um ponteiro de caractere para o valor do argumento da opção
- Opções curtas usam o prefixo
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()ouxfts_open()cria uma estruturaFTSa partir de um caminho- Nós de arquivo ou diretório na árvore são representados pela estrutura
FTSENT - Ao chamar
fts_read(), umFTSENTé 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 comofwrite(), aparecem funções não padronizadas comofull_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()emddecwrite()emsplit
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 emsystem.hque 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 padronizadasPROGRAM_NAMEdefine o nome oficial do utilitário e é usado na verificação de versãoAUTHORSdefine os autores do utilitário e é usado na verificação de versãoemit_try_help()imprime uma sugestão de ajuda após uma saída de falhaemit_ancillary_info (PROGRAM_NAME)imprime informações adicionais de ajuda após a saída específica do comandoinitialize_main(&argc, &argv)lida com a expansão interna de curingas no VMS e é removido na maioria dos outros sistemas operacionaisatexit(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 booleanodo { ... } 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çõesexit()eerror()
- Em 1995, o GNU gettext project adicionou Native Language Support, introduzindo a macro
- Mudanças por utilitário se dividem em correções de bugs, novos recursos e otimizações
join,sorteuniqeram vulneráveis a ataques de overflow até um patch de 2016dfrecebeu a opção--outputem 2013, adicionadayesteve 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 comarch,direvdir - 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
LISTFdo CTSS, por volta de 1963, depois encurtado paralsLISTFera um dos 18 utilitários originais do artigo de design de 1962
- A sintaxe peculiar de
ddlembra a job control language do OS/360 do início dos anos 1960 sorté o único utilitário que usa multithreadingfmtmostra otimização de linhas e parágrafos com custo de funçãoyesimplementa saída de alto desempenho com buffers de memória alinhados a páginadfé mais rápido porque usa metadados de dispositivo, enquantoduprecisa verificar todos os arquivoscksumtem dois pontos de entrada: execução normal e geração de tabela CRC-32echonão tem condição de falha- O design de
testeexprdifere bastante da estrutura típica de utilitários sufoi originalmente mantido em coreutils/shellutils
Implementações que valem a visita
- shuf e shred usam o ISAAC cipher) para números aleatórios
- shuf usa Reservoir sampling
- sum usa checksums legados System V e BSD
- expr usa avaliação de expressões com associação à esquerda
- shred usa Secure overwrite
- cksum calcula CRC-32 checksum
- sort usa uma variação de Merge sort
- factor é um utilitário que vale examinar pelos elementos de implementação matemática
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
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
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 oenvno 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
As práticas listadas pelo autor são, em programas grandes, geralmente criticadas por prejudicarem a manutenibilidade conforme a escala aumenta
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), ooddo coreutils é instalado comogod(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ãohttps://maizure.org/projects/decoded-gnu-coreutils/yes.html
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 lista de utilitários básicos bem divertida. Uso UNIX há muito tempo, mas nunca tinha ouvido falar de coisas como
shred,shufefactorDá vontade de rodar uma vez
e ver quanto tempo leva até ele se matar. Claro, em uma máquina virtual ou em um equipamento que dê para reflashear facilmente
dd, mas ele foi até o fim, o que foi bem decepcionanteEu esperava que travasse ou pelo menos perdesse a conexão, mas o kernel, o
sshde obashainda estavam na memória, então voltei para um prompt em que não dava para fazer nada direitoETXTBUSY), então oshrednão consegue estragar a si mesmo nem ofindGosto do fato de que
/bin/truepode, 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
trueé que ele ficou muito mais complexo do que precisavaPrimeiro, um exercício: O
trueliteralmente começou assim. Bem zenA 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
truefinalmente ganhou código, e esse código eraexit 0Depois 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 otruedo 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
true, parece não haver impacto, a menos que o comentário esteja confundindoSe 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?
Por exemplo, usar
copy_file_range()em vez deread()/write()para evitar copiar dados para o espaço de usuário. É mais engenharia de software do que ciência da computaçãoPosso ter perdido a ideia central deste site, mas já não existem man pages ou páginas info para cada um deles?