Curso: interfaces para scripts em shell – Aula 1

Por décadas, o conceito de interfaces para scripts em shell foi vendido apenas como uma curiosidade limitada às interfaces gráficas com o usuário (GUI). Porém o conceito de interfaces vai muito além disso, assim como as capacidades do shell para implementá-las.


Para aprender mais sobre o shell

Formas de apoio


Aula 1 - Introdução

Por décadas, o conceito de interfaces para scripts em shell foi vendido apenas como uma curiosidade limitada às interfaces gráficas com o usuário (GUI). Porém o conceito de interfaces vai muito além disso, assim como as capacidades do shell para implementá-las.

A rigor, em termos de implementação de interfaces com recursos internos do shell, tudo que podemos fazer são interfaces para a linha de comandos (CLI) e algumas categorias de interfaces de terminal (TUI), como menus e prompts de entradas de dados. Interfaces gráficas e diálogos mais elaborados para o terminal terão que ser feitos com programas especializados: o trabalho do shell estará limitado a controlar e trocar dados com esses programas.

Sendo assim, além de não corresponder exatamente ao que "o shell pode fazer", a ênfase nas interfaces gráficas, como se fossem "recursos matadores" do shell, acaba por obscurecer o que realmente precisamos saber sobre a implementação de interfaces para os nossos scripts. Aliás, muitos dos mecanismos fundamentais para interfaces CLI e TUI sequer são apresentados como tal, o que dificulta a contextualização desses mecanismos e os descaracteriza como algo que precisa ser planejado cuidadosamente nas decisões do projeto.

1.1 - O que é uma interface?

Interfaces são estruturas e mecanismos projetados para mediar a interação entre dois sistemas originalmente incompatíveis. Em sistemas computacionais, nós podemos dizer que as interfaces mediam a troca de dados e instruções entre programas ou entre um utilizador humano e os programas.

Neste sentido, o próprio shell é uma interface entre o utilizador e o sistema operacional. Para ser ainda mais específico, o shell é a interface padrão de sistemas operacionais unix-like (sistemas parecidos com o Unix), o que inclui o GNU/Linux, o macOS e toda a família BSD.

1.2 - O shell como interface

A filosofia Unix, na visão de Douglas McIlroy, o próprio criador do encadeamento por pipes, pode ser resumida desta forma:

  • Escreva programas que façam apenas uma coisa, mas que a façam bem feita.
  • Escreva programas que trabalhem juntos.
  • Escreva programas que manipulem cadeias de texto, pois esta é uma interface universal.

Das três recomendações, a última é a que mais nos interessa para o estudo das interfaces: as cadeias de texto como uma interface universal. Isso significa que, para sistemas unix-like, a interação do usuário com o shell, além da própria interação entre os programas, dar-se-á através de fluxos de textos.

Na prática, digitar uma linha de comando e teclar ENTER é enviar um fluxo de texto para o shell; de acordo com o processamento do nosso comando, o shell (ou o programa invocado através dele) poderá apresentar resultados no terminal, o que também será um fluxo de texto. Tradicionalmente, esses dois fluxos são chamados de fluxos padrão.

Deste modo, o fluxo da entrada de comandos digitados pelo usuário através do teclado (outro tipo de interface, a propósito) é chamado de entrada padrão, enquanto a saída de dados impressos no terminal é chamada de saída padrão.

Nota: nós dizemos que os dados são "impressos" no terminal porque, originalmente, os terminais eram máquinas de teletipo (TeleTYpe, ou TTY): basicamente equipamentos compostos de um teclado e uma impressora.

Fluxos de dados padrão

No sistema operacional, a entrada padrão é implementada na forma de um dispositivo associado ao terminal chamado stdin, ao passo que a saída padrão é implementada no dispositivo stdout.

No GNU/Linux, que é o que nos interessa, nós podemos visualizar esses dispositivos assim:

:~$ ls -l /dev/std*
lrwxrwxrwx 1 root root 15 abr 12 01:00 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 abr 12 01:00 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 abr 12 01:00 /dev/stdout -> /proc/self/fd/1

Como podemos ver acima, além de stdin e stdout, nós também temos o fluxo de mensagens de erros (stderr), que trafega por um dispositivo próprio. Também podemos observar que esses dispositivos são apenas ligações simbólicas para outros dispositivos com os nomes 0, 1 e 2: os chamados descritores de arquivos.

Se nós seguirmos as ligações, eis o que teremos:

:~$ ls -l /proc/self/fd
lrwx------ 1 blau blau 64 abr 12 02:55 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 12 02:55 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 12 02:55 2 -> /dev/pts/1

Novamente, os descritores de arquivos 0, 1 e 2 também são links simbólicos: desta vez, para o dispositivo /dev/pts/1:

:~$ ls -l /dev/pts/1
crw--w---- 1 blau tty 136, 1 abr 12 02:58 /dev/pts/1
↑
+--- Note que é um arquivo do tipo 'c',
     um "dispositivo caractere".

Dispositivos são interfaces

Os dispositivos em /dev são arquivos virtuais, parte do sistema de arquivos sysfs, que fornecem uma interface para a estrutura de dados pela qual o kernel gerencia, entre outras coisas, dispositivos de hardware.

Mas, interface com quem?

Interface entre as aplicações no espaço de usuário e os recursos a que só o kernel tem acesso (espaço do kernel).

Nota: este assunto é abordado com mais detalhes na segunda aula do Curso Shell GNU.

Deixando o aprofundamento para mais tarde, o que nos importa agora é saber que os fluxos de texto (que passaremos a chamar de "fluxos de dados") são manipulados através de dispositivos de terminal. No caso dos exemplos acima, por um dispositivo pts, ou pseudo-terminal, que é, resumidamente, um dispositivo de terminal implementado para acesso no espaço de usuário: diferente dos dispositivos tty, ou emuladores de terminal, que são implementados no espaço do kernel.

Também vale notar que o shell sempre troca fluxos de dados conosco e com o sistema operacional através da mediação de um dispositivo de terminal, ou seja: dispositivos de terminal são interfaces. Para ser ainda mais preciso, todos os programas, inclusive os que possuem uma interface gráfica (GUI), terão acesso a um dispositivo de terminal para, no mínimo, receber os fluxos de dados produzidos pelo usuário na entrada padrão.

Observe:

# Obtendo o PID do meu editor de textos gráfico (Geany)...

:~$ pidof geany
3314

# Filtrando apenas os descritores de arquivos 0, 1 e 2...

:~$ ls -l /proc/3314/fd | grep -E '\b(0|1|2) ->'
lrwx------ 1 blau blau 64 abr 12 08:35 0 -> /dev/tty1
l-wx------ 1 blau blau 64 abr 12 08:35 1 -> /home/blau/.xsession-errors
l-wx------ 1 blau blau 64 abr 12 08:35 2 -> /home/blau/.xsession-errors

Veja que, embora seja um programa com interface gráfica, o descritor de arquivos 0 (stdin, entrada padrão) é uma ligação simbólica para o dispositivo de terminal /dev/tty1. Os demais fluxos padrão são ligações para um arquivo no meu diretório pessoal (.xsession-errors), mas apenas porque o programa foi escrito de forma a redirecionar os fluxos de stdout e stderr para esse arquivo.

Isso acontece porque, em sistemas unix-like:

  • Todo programa executado implica na criação de um processo;
  • Todo processo recebe acesso aos três fluxos de dados padrão;
  • Os três fluxos de dados padrão estão conectados, a priori, a um dispositivo de terminal.

Processos como interfaces

Da mesma forma que o sistema de arquivos virtual sysfs é uma interface para a estrutura de dados de dispositivos gerenciados pelo kernel, o sistema de arquivos virtual procfs é a interface para a estrutura de dados dos processos que estão sendo gerenciados pelo kernel.

Sendo assim, no contexto do sistema operacional, sempre que nos referimos a um programa em execução, estamos nos referindo à estrutura de dados que o representa: o seu processo.

O assunto "processos" é abordado com mais detalhes nas aulas 3 e 4 do Curso Shell GNU. Para nós, neste momento, o que importa é entender que os fluxos de dados são associados individualmente a cada processo em execução (cada um tem acesso aos seus), mas a comunicação de instruções entre eles (e com eles) se dá através de sinais.

A lista dos sinais e suas respectivas instruções pode ser obtida com o comando interno do Bash kill -l. O comando exibe os 31 sinais disponibilizados pelo kernel mais os 30 sinais real time, que o Bash implementa para sinalizar a ocorrência de eventos customizados para os processos.

:~$ kill -l
 1) SIGHUP         2) SIGINT         3) SIGQUIT        4) SIGILL         5) SIGTRAP
 6) SIGABRT        7) SIGBUS         8) SIGFPE         9) SIGKILL       10) SIGUSR1
11) SIGSEGV       12) SIGUSR2       13) SIGPIPE       14) SIGALRM       15) SIGTERM
16) SIGSTKFLT     17) SIGCHLD       18) SIGCONT       19) SIGSTOP       20) SIGTSTP
21) SIGTTIN       22) SIGTTOU       23) SIGURG        24) SIGXCPU       25) SIGXFSZ
26) SIGVTALRM     27) SIGPROF       28) SIGWINCH      29) SIGIO         30) SIGPWR
31) SIGSYS        34) SIGRTMIN      35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10   45) SIGRTMIN+11   46) SIGRTMIN+12   47) SIGRTMIN+13
48) SIGRTMIN+14   49) SIGRTMIN+15   50) SIGRTMAX-14   51) SIGRTMAX-13   52) SIGRTMAX-12
53) SIGRTMAX-11   54) SIGRTMAX-10   55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

:~$ /bin/kill -L
 1 HUP      2 INT      3 QUIT     4 ILL      5 TRAP     6 ABRT     7 BUS
 8 FPE      9 KILL    10 USR1    11 SEGV    12 USR2    13 PIPE    14 ALRM
15 TERM    16 STKFLT  17 CHLD    18 CONT    19 STOP    20 TSTP    21 TTIN
22 TTOU    23 URG     24 XCPU    25 XFSZ    26 VTALRM  27 PROF    28 WINCH
29 POLL    30 PWR     31 SYS

Alguns desses sinais são associados a atalhos de teclado:

Atalho SIGSPEC SIGNUM Descrição
Ctrl+C SIGINT 2 Interrompe a execução do processo.
Ctrl+Z SIGTSTP 20 Suspende a execução do processo e o coloca em segundo plano.
Ctrl+S SIGSTOP 19 Pausa a execução do processo.
Ctrl+Q SIGCONT 18 Continua a execução de um processo pausado por SIGSTOP.
Ctrl+\ SIGQUIT 3 Sinal padrão para termina um processo por ação do usuário e realizar um dump ("despejo") do conteúdo da memória em um arquivo.

Porém, o mais importante para o nosso estudo de interfaces é que o shell oferece recursos para monitorar a ocorrência de sinais (comando interno trap), o que nos permite programar rotinas orientadas a eventos.

Nota: para mais detalhes, visite a aula 4 do Curso Shell GNU.

E o meu script com isso?

Se você acompanhou atentamente até aqui, deve estar claro que, quando falamos no shell como uma interface, há muito mais do que simples comandos digitados e respostas impressas no terminal. Também deve estar clara a multitude de interfaces a que temos acesso através do shell: tanto no modo interativo, que é a interface que o shell oferece quando executado em um terminal, quanto no modo não-interativo -- em especial, através de scripts.

No modo interativo, a interface com o usuário é implementada pelo próprio shell através de um prompt de comandos, por exemplo:

blau@debxp:~$ ▉

Mas não é só isso! Também são elementos da interface com o usuário fornecida pelo shell:

  • Os comandos internos;
  • Suas palavras reservadas;
  • Estruturas de decisão e repetição;
  • Operadores e demais componentes léxicos;
  • Mecanismos diversos, como expansões, redirecionamentos, pipes e captura de estados de saída.

Exceto pelo prompt, todos esses elementos estão disponíveis para a criação de scripts que executarão o shell no modo não-interativo, que é só outra forma de iniciar uma sessão do shell.

No modo não-interativo, apenas no sentido da digitação de comandos linha a linha, não acontece uma interação direta com o utilizador. Cada linha de comando está previamente escrita no script e, quando houver alguma interação, será para exibir resultados de dados processados ou para a entrada de dados que serão levados em conta na decisão do que será executado a seguir.

Em suma, nos bastidores da execução de um script, ainda é o mesmo shell que nós utilizamos pelo terminal, mas toda forma de interação com o utilizador, com o sistema operacional ou com outros processos precisa ser planejada e descrita explicitamente no código.

Em função disso, é fundamental conhecer as técnicas e os mecanismos para:

  • Implementar a forma de invocação definida no projeto;
  • Receber dados como argumentos na invocação do script;
  • Exibir mensagens no terminal;
  • Receber dados digitados pelo usuário durante a execução do script;
  • Receber e enviar dados de/para arquivos;
  • Receber e enviar dados por/para outros processos;
  • Receber dados por menus interativos;
  • Capturar e enviar sinais de/para outros processos.

Em última análise, é disso que se trata a criação de interfaces para scripts em shell, e não há como planejar uma interface adequada a cada tipo de projeto sem um mínimo de domínio das técnicas e mecanismos em torno de conceitos como:

  • Parâmetros posicionais;
  • Impressão no terminal;
  • Fluxos de entrada e saída de dados;
  • Redirecionamentos e pipes;
  • Sinalização de processos...

Além do próprio domínio da lógica e das estruturas de linguagem do shell: afinal, quando falamos em "criar interfaces", isso só faz sentido no contexto da programação de scripts.

Sem dominar as técnicas e mecanismos envolvidos na implementação de interfaces para scripts em shell, programar deixa de ser o execício racional e consciente de um saber para tornar-se pura repetição e cópia, com resultados que estarão sempre à mercê da sorte.

1.3 - Classes de interfaces para scripts

Todas as decisões sobre que tipo de interface implementar devem ser tomadas estritamente com vistas aos requisitos do projeto. Nós precisamos conhecer, ao menos de forma geral, os tipos de interface que poderemos adotar nos scripts, mas nunca em termos de julgamento sobre qual delas é melhor ou pior: de novo, a melhor interface é a que atende aos requisitos do projeto.

Interface para a linha de comando (CLI)

Este é o grupo de interfaces mais fundamental e comum para scripts. Nas interfaces CLI, toda atuação possível, pela parte do utilizador, se dá na invocação do script, recorrendo ou não a outros mecanismos do shell relacionados aos fluxos de dados. Se a passagem de dados e opções de execução forem necessárias, ela será feita através da digitação de argumentos na linha do comando; se os dados processados tiverem que ser redirecionados para arquivos ou processos, isso será feito através de operadores do shell também na linha do comando.

Por exemplo, o meu script upt, que informa de várias formas o tempo de atividade do sistema (uptime):

:~$ upt -h
upt (0.0.1) - Tempo de atividade do sistema.

USO
    upt [OPÇÕES]

DESCRIÇÃO
    Exibe o tempo de atividade do sistema.

OPÇÕES
    -d  Exibe separação com vírgulas.
    -p  Exibe prefixo antes do tempo de atividade.
    -s  Data e hora do início da atividade.
    -h  Exibe esta ajuda e sai.

Copyright (C) 2022  Blau Araujo <blau@debxp.org>

License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Trata-se de um típico script com interface CLI:

# Sem argumentos...
:~$ upt
11 horas 59 minutos

# Com a opção de separação com vírgulas...
:~$ upt -d
12 horas, 6 minutos

# O que é útil para separar a saída em campos...
:~$ upt -d | sed 's/, /\n/'
12 horas
6 minutos

# Também podemos atribuir dados da saída a variáveis...
:~$ read H h M m <<< $(upt)
:~$ echo $H
12
:~$ echo $h
horas
:~$ echo $M
9
:~$ echo $m
minutos

De modo geral, interfaces CLI são perfeitas para que os nossos scripts possibilitem o máximo proveito dos mecanismos do shell para ampliar sua aplicabilidade trabalhando em conjunto com outros programas, o que está 100% de acordo com a filosofia Unix.

Interface com o usuário via terminal (TUI)

Com muita frequência, scripts elaborados com interfaces TUI e GUI são invocados da mesma forma que invocaríamos scripts com interfaces CLI: todas as diferenças ocorrerão durante a execução do script, seja solicitando dados adicionais ao utilizador pela entrada padrão (comando read, por exemplo) ou recorrendo a rotinas ou programas externos responsáveis por desenhar menus ou diálogos no terminal. Portanto, basta que o script solicite uma simples entrada de dados pelo terminal para que possamos classificar a sua interface como TUI.

Podem ser classificadas como interfaces TUI, por exemplo:

  • Scripts que interrompem o fluxo de execução para a digitação de informações;
  • Scripts que implementam um ciclo repetitivo de entrada de dados para serem processados (o chamado REPL);
  • Menus construídos com recursos do shell ou fornecidos por programas especializados (fzf);
  • Diálogos e janelas construídos com recursos do shell, manipulando capacidades do terminal ou com programas especializados (dialog e whiptail).

Eventualmente, o mesmo script pode oferecer a possibilidade de ser utilizado tanto pela interface CLI quanto pela interface TUI, como é o caso desta minha calculadora em Bash e AWK:

:~$ cawk -h
cawk (0.0.3) - REPL calculator in Bash and AWK

USAGE

  cawk [-f PRECISION]
  cawk -e EXPRESSION [-f PRECISION]
  COMMAND | cawk [-f PRECISION]
  cawk [-f PRECISION] < FILE
...

Observe que, executada sem argumentos, ou apenas com a opção de precisão, a calculadora entra em modo REPL, uma TUI. Se os argumentos relativos à expressão que deverá ser efetuada forem informados, ela será executada no modo CLI.

Interface gráfica com o usuário (GUI)

De todas as categorias de interfaces, esta é a única que não pode ser implementada apenas com recursos do shell. Nós sempre dependeremos de programas especializados na exibição de janelas, diálogos e menus no ambiente gráfico do utilizador, que também dependerá desses programas instalados em seu sistema para que possa utilizar os nossos scripts.

Diferente das interfaces CLI e TUI, a decisão de implementar uma interface do tipo GUI deve ser muito bem ponderada para ser justificada. Alguns desses programas são o yad e o zenity, mas existem outros baseados em diferentes toolkits gráficas (Gtk, Qt, Tcl/Tk, etc).

A dependência de programas externos é apenas um dos fatores que devem ser ponderados. Existem muitas lendas em torno da suposta facilidade e amigabilidade das interfaces gráficas, o que pode contaminar a decisão no projeto. Também não devemos cair na ilusão de que interfaces gráficas dariam um ar mais profissional ao script. Não existem scripts profissionais: o profissionalismo está no programador e, principalmente:

  • Na sua capacidade de trabalhar com método;
  • No seu conhecimento dos conceitos e técnicas;
  • Na responsabilidade que assume pelas suas escolhas.

Para aprender mais sobre o shell

Formas de apoio


Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment