Criando aplicações de linha de comando amáveis


Turicas aka Álvaro Justen


22 de outubro de 2025

PythonBrasil 2025 - São Paulo/SP

$ whoami

Turicas, prazer! =)

Sigam-me os bons:

{twitter,
bsky.social,
github,
youtube,
slideshare,
instagram}
/turicas

alvaro@pythonic.cafe

Software Livre & Python

(desde 2004/2005)

       

## Agenda - Definições e contexto histórico - CLI como Usuário(a) (+ exercícios) - Características amáveis - Características detestáveis - O módulo `argparse` (+ exercícios) - Resumo: Boas Práticas - Sugestões pós-tutorial
## Alinhamento de Expectativas - Esse **não é um tutorial para iniciantes em programação** - É necessário: - Saber lógica de programação - Saber programar em Python - Saber usar o terminal de sistemas Unix-like (`cd`, `ls`, `grep` etc.) - Ter Python (3.9+) e um editor de código instalados - Sistema operacional: Unix-like (GNU/Linux, *BSD, Mac OS X) ou Windows com WSL - "Terminal" do Windows não será o foco - Todos(as) são bem-vindos(as), mas o foco para dúvidas será nas pessoas que já sabem os conceitos acima - Os exercícios exigirão muita atenção, digitação e serão cumulativos. Esteja focado(a) - Por conta do tempo, não conseguiremos ir muito profundamente em todos os assuntos, mas existem sugestões com links
# Definições e Contexto Histórico

Graphical User Interface (GUI)

htop
Exemplo: Gnome Files

Terminal User Interface (TUI)

htop
Exemplo: htop
Quais outros exemplos de TUI vocês conhecem?

Command-Line Interface (CLI)

Comando cat
Exemplo: cat
## Command-Line Interface (2) - Comando é invocado com parâmetros, executa, mostra a saída e nos devolve o prompt - CLI não tem "user" - Crie o seu programa para que ele seja usado por outros programas! - "_Human first, machine friendly_" - CLI não é TUI - TUI não é só "janela no terminal": existem programas interativos, como o shell do Python
## Unix ### O SO mais influente das últimas décadas - Criado em 1969 por Ken Thompson e Dennis Ritchie - A linguagem C foi criada para escrever o Unix! - Portável par várias máquinas - Conexão lenta entre terminais e computadores - Richard Stallman cria o conceito de _free software_ (software livre) em 1983 e começa o GNU (GNU's Not Unix), o sistema operacional livre compatível com o Unix - Em 1975 funda a [Free Software Foundation](https://fsf.org/) - Em 1988 o POSIX é iniciado, com o objetivo de padronizar as diversas implementações Unix-like
# CLI como Usuário(a)
## Pedindo ajuda - Alguém **não conhece** o comando `ls`? - Quem conhece mais de 5 opções? - `ls --help` - `ls -h` - `--help` e `-h` é convenção
## Manuais ❤️ - `man [seção] comando` - `help comando_embutido` (no `bash`, `cd` é um comando embutido e não um programa, como o `ls`) - Busca: `man -k termo` - Convenções: - **bold text**: digite exatamente como está - _italic text_: troque com o valor apropriado - `[-abc]`: argumentos entre `[` e `]` são opcionais - `-a|-b`: opções que não podem ser usadas juntas - `argument ...`: argumento pode ser especificado N vezes - Exemplo: `man ls` (`q` para sair) - Exemplo: `man grep` (`/` para buscar) - `/EXIT` + ENTER
## Códigos de saída - O programa retorna o código de saída (inteiro) para o sistema operacional, como se fosse uma função - `0` = deu tudo certo! - `1-255` = algum erro aconteceu - Cada número representa algo específico **apenas para aquele programa**! - Testem o `grep` com algum arquivo ``` # Encontrado $ grep pythonic index.html

alvaro@pythonic.cafe

alvaro@pythonic.cafe

$ echo $? 0 # Não encontrado $ grep zig index.html $ echo $? 1 ```

Quem não lê manual

RTFM

Cachorro no laboratório de química
## Parâmetros e opções - Entenda cada comando como um verbo - Transitivos diretos, indiretos e intransitivos - Ordem não importa: `ls -lh` = `ls -l -h` = `ls -h -l` = `ls -hl` - Forma longa: `ls --width=60` = `ls --width 60` - Forma curta: `ls -w 60` = `ls -w60` - Ordem importa para as opções que precisam de um valor: - `ls -w 60` funciona, `ls -lw 60` funciona, mas `ls -wl 60` não
## Exemplos na stdlib - A biblioteca padrão do Python possui diversos utilitários de linha de comando: - (busque por outros, como `http.server`) ``` $ echo '{"a":123,"b":"aaa"}' > test.json $ cat test.json {"a":123,"b":"aaa"} $ python -m json.tool test.json { "a": 123, "b": "aaa" } $ cat test.json | python -m json.tool { "a": 123, "b": "aaa" } $ python -m mimetypes test.json type: application/json encoding: None ```
# Características amáveis
## Inspirações - [The Art of Unix Programming](http://www.catb.org/esr/writings/taoup/html/), por Eric S. Raymond - [Portable Operating System Interface (POSIX)](https://pt.wikipedia.org/wiki/POSIX) - [Command Line Interface Guidelines (CLIG)](https://clig.dev/) - [Programação Shell Linux](https://www.amazon.com.br/Programa%C3%A7%C3%A3o-Shell-Linux-Julio-Cezar/dp/8574528331), por Julio Cezar Neves - Filosofia Unix, pela cultura de desenvolvimento/uso dos programas (é tipo o [Zen do Python](https://pt.wikipedia.org/wiki/Zen_de_Python))
## Rules of Unix (1/3) - 1- Rule of Modularity: Write simple parts connected by clean interfaces. - 2- Rule of Clarity: Clarity is better than cleverness. - **3- Rule of Composition**: Design programs to be connected to other programs. - **4- Rule of Separation**: Separate policy from mechanism; separate interfaces from engines. - 5- Rule of Simplicity: Design for simplicity; add complexity only where you must. - 6- Rule of Parsimony: Write a big program only when it is clear by demonstration that nothing else will do.
## Rules of Unix (2/3) - **7- Rule of Transparency**: Design for visibility to make inspection and debugging easier. - **8- Rule of Robustness**: Robustness is the child of transparency and simplicity. - **9- Rule of Representation**: Fold knowledge into data so program logic can be stupid and robust. - **10- Rule of Least Surprise**: In interface design, always do the least surprising thing. - **11- Rule of Silence**: When a program has nothing surprising to say, it should say nothing. - **12- Rule of Repair**: When you must fail, fail noisily and as soon as possible.
## Rules of Unix (3/3) - **13- Rule of Economy**: Programmer time is expensive; conserve it in preference to machine time. - 14- Rule of Generation: Avoid hand-hacking; write programs to write programs when you can. - **15- Rule of Optimization**: Prototype before polishing. Get it working before you optimize it. - 16- Rule of Diversity: Distrust all claims for “one true way”. - 17- Rule of Extensibility: Design for the future, because it will be here sooner than you think.
## Rules of Unix - "_If you're new to Unix, these principles are worth some meditation_" - "_Software-engineering texts recommend most of them_" - "_Most other operating systems lack the right tools and traditions to turn them into practice_" - Retirado de [The Art of Unix Programming](http://www.catb.org/esr/writings/taoup/html/), por Eric S. Raymond - Inspirado em Ken Thompson, Doug McIlroy, Rob Pike e na filosofia Unix (Usenet)
“ Those who do not understand Unix are condemned to reinvent it, poorly. ”
-- Henry Spencer (Usenet signature, 1987-11-15)
# Características detestáveis

CLI também é UI

Bad UI
## Cria serviço com o nome `--help` ``` $ dokku postgres:create --help Waiting for container to be ready Creating container database Securing connection to database =====> Postgres container created: --help =====> --help postgres service information Config dir: /var/lib/dokku/services/postgres/--help/data Config options: Data dir: /var/lib/dokku/services/postgres/--help/data Dsn: postgres://postgres:6272ceeb9b57335182ad6aef639df714@dokku-postgres---help:5432/__help Exposed ports: - Id: 343581fde373f4bb3c22e68b5d6ab4d4f4d5668b953d4c32275caddc967704aa Internal ip: 172.17.0.8 Initial network: Links: - Post create network: Post start network: Service root: /var/lib/dokku/services/postgres/--help Status: running Version: postgres:17.5 ```
## Ignora parâmetro, sem mostrar erro ``` dokku plugin:install https://github.com/dokku/dokku-postgres.git blablabla [...] ```
## Parâmetro obrigatório como opção (--) ``` $ tree-sitter complete error: the following required arguments were not provided: --shell Usage: tree-sitter complete --shell For more information, try '--help'. (system) turicas@thinkpad:~ $ tree-sitter complete --help Generate shell completions Usage: tree-sitter complete --shell Options: -s, --shell The shell to generate completions for [possible values: bash, elvish, fish, power-shell, zsh, nushell] -h, --help Print help ```
## Ordem das opções influencia ## Não segue padrão de opções longas (--) ``` find . -name '*.pdf' # Funciona find -name '*.pdf' . # NÃO FUNCIONA! ```
## Erro de documentação Pode ser especificada várias vezes, mas é apresentada como "stringArray": ``` $ docker build --help | grep build-arg --build-arg stringArray Set build-time variables ```
## Inconsistência entre opções curtas e longas ``` mariadb --help | grep -A 2 '\--password' -p, --password[=name] Password to use when connecting to server. If password is not given it's asked from the tty. $ mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD Enter password: ERROR 1045 (28000): Access denied for user 'mariadb'@'localhost' (using password: YES) $ mariadb -u $MARIADB_USER -p=$MARIADB_PASSWORD ERROR 1045 (28000): Access denied for user 'mariadb'@'localhost' (using password: YES) $ mariadb -u $MARIADB_USER --password $MARIADB_PASSWORD Enter password: ERROR 1045 (28000): Access denied for user 'mariadb'@'localhost' (using password: NO) $ mariadb -u $MARIADB_USER --password=$MARIADB_PASSWORD Welcome to the MariaDB monitor. Commands end with ; or \g. Your MariaDB connection id is 41 Server version: 11.7.2-MariaDB-ubu2404 mariadb.org binary distribution ```
## Inconsistência na posição de parâmetro que não existe ``` # Erro $ dokku postgres:logs -f pg17_brasil-io-prd ! Postgres service -f does not exist # Sem erro, mas não funciona como o esperado $ dokku postgres:logs pg17_brasil-io-prd -f PostgreSQL Database directory appears to contain a database; Skipping initialization [...] (não atualiza) # Funciona como o esperado (equivalente ao `tail -f`) $ dokku postgres:logs pg17_brasil-io-prd -t PostgreSQL Database directory appears to contain a database; Skipping initialization [...] (atualiza) ```
# O módulo `argparse`
## Por que `argparse` e não `X`? - Dar vida (facilmente) a funções/classes que já você tem - É uma evolução de soluções anteriores (`getopt` e `optparse`) - Possui funcionalidades suficientes para uma boa interface - Não adiciona dependências externas ao projeto - Pode usar para criar _management commands_ do Django (que ajudam bastante!)
## Por que a insistência em usar stdlib? - Você é **responsável** pelas dependências que adiciona - E em geral você não tem controle sobre elas! - Módulos da stdlib do Python são **MUITO mais testados** do que bibliotecas de terceiros - Evitar _hypes_ (quem trabalha com tecnologia adora uma, desnecessariamente) - Evita ataques de _supply chain_ e problemas que você não pode resolver. Busque sobre: - Heartbleed (2014) - OpenSSL - Leftpad (2016) - npm
“ Those who do not understand Python stdlib are condemned to reinvent it, poorly (and waste Internet traffic and disk space). ”
-- Álvaro Justen (Python Brasil, 2025-10-22)

Processo POSIX

CLI: stdin + stdout + stderr

Processo CLI
Orhun Parmaksız - Introducing Ratatui: A Rust library to cook up terminal user interfaces - FOSDEM 2024

Processo POSIX (2)

Processo CLI
## Exercícios - Exemplos inspirados na CLI da biblioteca [mercados](https://pypi.org/project/mercados) - Palestra domingo às 9h - Baseados no arquivo `banco_central.py` (simplificação de `mercados.bcb`) - Passos de bebê
## `banco_central.py` - Baixe-o em [`banco_central.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/banco_central.py) - Entenda as funções e a `dataclass` implementadas no arquivo: - Baixa séries temporais da API, extrai e guarda em uma lista de dataclasses - Converte a lista para CSV - Converte a lista para markdown - Desenho das funcionalidades e forma de uso: - Uma das partes mais importantes do desenvolvimento da CLI - Como você desenharia uma CLI para contemplar essas funcionalidades? - O que é importante para o(a) usuário(a)? - Qual seria a melhor forma de chamá-la e passar as opções?
## Usando `sys.argv` - Arquivo: [`00_sys_argv.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/00_sys_argv.py) - Quais são os pontos negativos dessa solução? - Em termos de usabilidade - Em termos de robustez - Respostas: - ⬆️ Divisão em _model_, _view_ e _controller_ - ⬇️ Mensagem de erro não intuitiva quando usuário(a) não passa parâmetros - ⬇️ Sem `--help`
## Usando `argparse` - Arquivo: [`01_argparse.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/01_argparse.py) - `git diff --no-index 00_sys_argv.py 01_argparse.py` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ `argparse` vai PARAR o programa se os argumentos não forem extraídos com sucesso - ⬆️ `-h` e `--help` - Usuário(a) agora sabe os nomes do parâmetros - ⬆️ Código de saída coerente
## Usando `choices` - Arquivo: [`02_choices.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/02_choices.py) - `git diff --no-index 01_argparse.py 02_choices.py` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Refatoração: [`02_choices_2.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/02_choices_2.py) - `git diff --no-index 02_choices.py 02_choices_2.py` - Respostas: - ⬆️ Validação de dados (programa sempre executará de forma "segura")
## Usando opções - Arquivo: [`03_opcoes.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/03_opcoes.py) - `git diff --no-index 02_choices.py 03_opcoes.py` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Melhoria (`os.environ`): [`03_opcoes_2.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/03_opcoes_2.py) - `FORMATO_BC=csv python 03_opcoes_2.py IPCA` - `git diff --no-index 03_opcoes.py 03_opcoes_2.py` - Respostas: - ⬆️ Exige menos parâmetros - ⬆️ _Sane defaults_ - ⬆️ Integra-se ao restante do sistema (env vars)
## Opções: use _kebab-case_ - Para opções longas de múltiplas palavras, separe as palavras com hífens (e não com _underscore_) - O `argparse` criará os argumentos com os hífens trocados por _underscores_: ``` parser = argparse.ArgumentParser() parser.add_argument("--codigo-negociacao") args = parser.parse_args() print(args.codigo_negociacao) ```
## Usando `type` - Arquivo: [`04_type.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/04_type.py) - `git diff --no-index 03_opcoes.py 04_type.py` - Execute: - `python 04_type.py -m 5 IPCA` - `python 04_type.py IPCA -m 5` - `python 04_type.py IPCA -m teste` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ Flexibilidade no controle dos resultados - ⬇️ Não possui controle pela data (início/fim)
## Usando `help`, `description`, `metavar` e `version` - Arquivo: [`05_help.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/05_help.py) - `git diff --no-index 04_type.py 05_help.py` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ Mais clareza na documentação/`--help` - ⬇️ Não possui controle pela data (início/fim)
## `stdout` vs `stderr` - Toda saída que pode ir para outros programas (ou arquivos) vai no `stdout` - Mensagens de erro e/ou status vão no `stderr` - Arquivo: [`06_stderr.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/06_stderr.py) - `git diff --no-index 05_help.py 06_stderr.py` - Qual a diferença entre: - `python 06_stderr.py IPCA` - e `python 06_stderr.py IPCA > ipca.md`? - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ Mensagem no `stderr` dá mais transparência
## `action="store_true"` - Guarda _flags_ (opções que não recebem argumentos) - Arquivo: [`07_store_true.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/07_store_true.py) - `git diff --no-index 06_stderr.py 07_store_true.py` - Qual a diferença entre: - `python 07_store_true.py IPCA` - e `python 07_store_true.py --quiet IPCA` - (ou `python 07_store_true.py -q IPCA`)? - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ Flexibilidade em poder não ver a mensagem de status
## Opções conflitantes (1/2) - Arquivo: [`08_mutual_exclusion.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/08_mutual_exclusion.py) - `git diff --no-index 07_store_true.py 08_mutual_exclusion.py` - Tente executar: - `python 08_mutual_exclusion.py --verbose IPCA -m 10` - `python 08_mutual_exclusion.py --quiet IPCA -m 10` - `python 08_mutual_exclusion.py --verbose --quiet IPCA -m 10`
## Opções conflitantes (2/2) - Implementação de grupo exclusivo: [`08_mutual_exclusion_2.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/08_mutual_exclusion_2.py) - `git diff --no-index 08_mutual_exclusion.py 08_mutual_exclusion_2.py` - Tente executar: - `python 08_mutual_exclusion_2.py --quiet --verbose -m 10 IPCA` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ Validação de regra de negócio não permite que o usuário entre com informações conflitantes
## Usando tipo personalizável - Arquivo: [`09_custom_type.py`](https://github.com/turicas/slides/blob/gh-pages/tutorial-cli/src/09_custom_type.py) - `git diff --no-index 08_mutual_exclusion_2.py 09_custom_type.py` - Tente executar: - `python 09_custom_type IPCA -i xxxx` - Logo depois: `echo $?` - O que melhoramos com relação à anterior? Quais são os pontos negativos dessa solução? - Respostas: - ⬆️ Mais flexibilidade no controle dos resultados
## Sugestões de Exercícios (1/2) - Implemente um parâmetro posicional `arquivo` com `required=False` - Se especificado, salva o resultado (que iria pra stdout) no arquivo - Use `Path(arquivo).parent.mkdir(exist_ok=True, parents=True)` - Verifique (`try`/`except`) se houve erro de _timeout_ na função `series_temporais` - em caso positivo, termine imediatamente o programa com um código de erro (ex: `exit(10)`) - Transforme a definição do `parser` em uma função (da linha `parser = ...` à linha anterior a `args = parser.parse_args()`) e escreva testes automatizados para ele
## Sugestões de Exercícios (2/2) - Mova o código da CLI para o arquivo `banco_central.py` e coloque-o dentro de `if __name__ == "__main__":`, assim você terá uma biblioteca e CLI em um único módulo - Altere o programa para aceitar atalhos para as mesmas séries, como "ipca", "ipca15", "ipca-15" e "selic" - Como você implementaria cache (para evitar múltiplas requisições)? - Dica: existem pastas padrão para cache em cada sistema operacional
## Conteúdo Extra - Além do `action="store_true"` e _mutual exclusion_, busque [na documentação](https://docs.python.org/3/library/argparse.html) sobre: - `action="append"` - `action="count"` - `nargs="?"` - `nargs="*"` - `nargs="+"` (diferente de `action="append"`) - `parser.add_subparsers()` (para subcomandos, como o `git`)
# Resumo: Boas Práticas
## Boas Práticas (1) - Não mostre mensagens em caso de sucesso (exceto se `--verbose` ou `--debug` forem especificadas) - Se encontrar algum problema, exiba um erro imediatamente - Retorne `0` se o programa executou com sucesso e `1-255` se falhou - Seja consistente com os códigos de saída - Crie diretórios pais antes de salvar arquivos: - `Path(arquivo).parent.mkdir(exist_ok=True, parents=True)`

Evite

Coisas a evitar
## Boas Práticas (2) - Separe _model_, _view_ e _controller_: - _Model_: dataclasses representando seus dados - _View_: interface com o usuário (argparse, parâmetros, stdin/stdout/stderr etc.) - _Controller_: funções/classes/módulos que executam a tarefa de fato - **ATENÇÃO**: evite `print` ou `input` no _controller_ (em geral isso é de responsabilidade da _view_) - Teste cada uma dessas camadas de maneira independente
## Boas Práticas (3) ### Importe somente o necessário - Esqueça a PEP8 por um instante - Exemplo: rows 0.4.1 (~700ms) vs 0.5.0+dev0 (~100ms) - `time ls --help` executa em 3ms! (é feito em C) - `touch empty.py; time python empty.py` executa em ~20ms (sem nenhum código Python) - O `--help` deve estar abaixo de 100ms (acima disso a lentidão é perceptível e não justificável para uma mensagem de ajuda)
# Sugestões pós-tutorial
## Outras Bibliotecas ### (ordem alfabética - não é recomendação!) - [argparse-manpage](https://pypi.org/project/argparse-manpage/) - [blessed](https://blessed.readthedocs.io/en/latest/terminal.html) - [click](https://pypi.org/project/click/) - [cmd (stdlib)](https://docs.python.org/3/library/cmd.html) - [curses (stdlib)](https://docs.python.org/3/howto/curses.html) (não funciona em Windows) - [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) (ex: pytmux, pyvim) - [PyTermGUI](https://ptg.bczsalba.com/) - [rich](https://github.com/Textualize/rich) - [textual](https://github.com/Textualize/textual) - [tqdm](http://pypi.python.org/project/tqdm) - [typer](https://pypi.org/project/typer/) - [urwid](https://urwid.org/index.html)
## Conteúdos complementares - [The Art of Unix Programming](http://www.catb.org/esr/writings/taoup/html/), por Eric S. Raymond (livro, Inglês) - [Portable Operating System Interface (POSIX)](https://pt.wikipedia.org/wiki/POSIX) - [Command Line Interface Guidelines (CLIG)](https://clig.dev/) (artigo, Inglês) - [Programação Shell Linux](https://www.amazon.com.br/Programa%C3%A7%C3%A3o-Shell-Linux-Julio-Cezar/dp/8574528331), por Julio Cezar Neves (livro, Português) - [Terminal under the hood](https://yakout.io/blog/terminal-under-the-hood/) (artigo, Inglês) - [Mastering Linux Man Pages](https://youtu.be/RzAkjX_9B7E?si=HlkR29qRqOybRcdy) (vídeo, Inglês)
## Slides: [bit.ly/turicas-cli-pybr25](http://bit.ly/turicas-cli-pybr25) ## Exemplos de código: [bit.ly/turicas-cli-pybr25-src](http://bit.ly/turicas-cli-pybr25-src)

Dúvidas?



{twitter,
bsky.social,
github,
youtube,
slideshare,
instagram}
/turicas

alvaro@pythonic.cafe