Dialogando com o ‘dialog’

Dialogando com o 'dialog'

O dialog é um programa para a linha de comandos que desenha elementos de formulários no terminal utilizando os recursos da biblioteca ncurses (uma API dedicada ao controle do terminal para o desenvolvimento de interfaces em modo texto). Com ele, nós podemos criar scripts que interagem com o utilizador através de caixas de diálogo e botões, em vez das tradicionais mensagens e entradas em texto.

O objetivo deste artigo não é ser um tutorial completo do dialog nem dar exemplos das suas possibilidades de uso. O que nós queremos é entender o funcionamento e o uso da ferramenta, preenchendo, em grande parte, as principais lacunas deixadas pela quase totalidade do material em texto e vídeo que se encontra nas buscas pela internet. Se você ficar atento aos conceitos aqui apresentados, não haverá grandes dificuldades para utilizar o dialog nos seus projetos tendo apenas o manual como apoio.

Conhecendo e obtendo ajuda

Originalmente, o dialog trabalhava com apenas 8 tipos de caixas de diálogo:

  • Caixa sim/não
  • Caixa de menu
  • Caixa de entrada
  • Caixa de mensagem
  • Caixa de texto
  • Caixa de informação
  • Caixa de lista de checagem
  • Caixa de botões de rádio

As versões mais recentes para o GNU/Linux, porém, já oferecem mais de 15 diálogos diferentes e dezenas de opções. Uma lista completa dos diálogos, bem como suas respectivas sintaxes, pode ser obtida com o comando:

:~$ dialog --help | grep -- '  --'

Ou, para a sintaxe de um elemento ou opção específica:

:~$ dialog --help | grep -- '--ELEMENTO'

Exemplo:

:~$ dialog --help | grep -- '--calendar'
  --calendar     <text> <height> <width> <day> <month> <year>

Obviamente, para que os comandos acima funcionem, o dialog precisa estar instalado, mas isso não é feito por padrão na maioria das distribuições GNU/Linux. No Debian, nós instalamos assim:

:~$ sudo apt install dialog

Importante! Como o dialog não está sempre instalado, ele deve ser tratado como uma dependência do seu projeto.

Utilização

Digamos que, em determinado ponto, o seu script precise solicitar uma data ao usuário: este é o tipo de situação onde a padronização da entrada dos dados é crítica e a implementação da validação e tratamento de erros pode tomar muito tempo. Com o dialog, boa parte desse trabalho pode ser reduzido com o uso da caixa de calendário.

Nós já vimos a sintaxe do calendário:

--calendar <text> <height> <width> <day> <month> <year>

Onde:

  • <text> - O texto explicativo do uso do diálogo.
  • <height> - Altura da caixa de diálogo (0 = automática).
  • <width> - Largura da caixa de diálogo (0 = automática).
  • <day> <month> <year> - Data inicial no formato numérico DIA MÊS e ANO.

Palavras são palavras!

Repare que os argumentos descritos são palavras na linha do comando: e palavras são separadas por espaços e interpretadas pelo shell de acordo com as regras de citação.

Logo, os dois exemplos abaixo terão resultados muito diferentes:

# ERRADO!

dialog --calendar Escolha uma data: 0 0 21 03 2022
                    ↑      ↑   ↑    ↑ ↑ ↑  ↑   ↑
        Argumentos  1      2   3    4 5 6  7   8

# CORRETO!

dialog --calendar 'Escolha uma data:' 0 0 21 03 2022
                           ↑          ↑ ↑ ↑  ↑   ↑
        Argumentos         1          2 3 4  5   6

De fato, veja o que acontece quando executamos a forma errada do comando:

:~$ dialog --calendar Escolha uma data: 0 0 21 03 2022

Expected no more than 20 tokens for --cale, have 8.
Use --help to list options.

Traduzindo: não eram esperados mais que 20 tokens, tem 8.

Apesar da mensagem de erro pessimamente redigida (que diabos de "20 tokens" são esses?), o que ela nos informa é que a quantidade de argumentos está incorreta: nós passamos 8 argumentos em vez dos 6 esperados.

Para não esquecer: quem determina o que é uma palavra na linha do comando é o shell, não o dialog!

Voltando à forma correta de uso, este seria o resultado:

:~$ dialog --calendar 'Escolha uma data:' 0 0 21 03 2022

Ao aceitarmos uma data selecionada (no caso, a própria data definida como inicial), o dialog exibiria a string 21/03/2022 na posição do cursor:

Isso acontece porque o dialog não restaura o terminal antes de imprimir sua saída, ou seja: nós teremos que lidar com isso no script.

Adicionando um título

Como vimos, o argumento <text> refere-se ao texto informativo do diálogo, não a um título. Ocorre, porém, que o título é um atributo da "janela" onde o diálogo será exibido e, portanto, deve ser informado antes dos argumentos relativos ao nosso calendário:

dialog --title 'Agendamento' --calendar 'Escolha uma data:' 0 0 21 03 2022
       |                   | |                                           |
       +---------+---------+ +-------------------+-----------------------+
                 |                               |
        Definição da caixa             Definição do diálogo

Este seria o resultado:

Limpando o terminal

Com as opções --clear e --erase-on-exit, nós podemos limpar o terminal, mas existem diferenças:

  • --clear - Remove o diálogo e mantém a cor de fundo (útil para diálogos encadeados).
  • --erase-on-exit - Restaura o terminal.

Em ambos os casos, o cursor vai para a última linha do terminal e não é possível ver a resposta do utilizador.

Para onde vão as saídas dos diálogos

Por padrão, como a saída padrão (STDOUT, descritor de arquivos 1) está ocupada com a exibição do diálogo em si, todos os textos retornados pelo dialog são mandados para a saída padrão de erros (STDERR, descritor de arquivos 2).

:~$ dialog --calendar 'data' 0 0 21 03 2022 2> respostas
:~$ cat respostas
21/03/2022

Por outro lado, quando redirecionamos a saída padrão (STDOUT) para um arquivo, a exibição do diálogo não acontece e o comando parecerá estar travado -- na verdade, ele estará apenas esperando a resposta do utilizador para um diálogo que não está sendo exibido.

Observe no teste abaixo (feito em dois terminais):

# TERMINAL 1 - Saída padrão redirecionada

:~$ dialog --calendar 'data' 0 0 21 03 2022 > respostas
▊ ←--- Parece travado!

# TERMINAL 2

:~$ ls -l /proc/$(pidof dialog)/fd
total 0
lrwx------ 1 blau blau 64 mar 22 08:26 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 mar 22 08:26 1 -> /home/blau/respostas
lrwx------ 1 blau blau 64 mar 22 08:26 2 -> /dev/pts/1

Quando o dialog é executado em uma substituição de comandos (um subshell), a saída padrão (STDOUT) é redirecionada para o pipe criado pelo shell para capturar as saídas dos comandos do subshell e também parecerá estar "travado", o que pode ser comprovado desta forma:

# TERMINAL 1

# Obtendo o PID do shell
:~$ echo $$
286647

# Dialog executado na substituição de comandos...
:~$ data=$(dialog --calendar 'data' 0 0 21 03 2022)
▊ ←--- Parece travado!

# TERMINAL 2

# Descritores de arquivos do processo do 'dialog'...
:~$ ls -l /proc/$(pidof dialog)/fd
total 0
lrwx------ 1 blau blau 64 mar 22 08:02 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 mar 22 08:02 1 -> 'pipe:[2168309]' ← Pipe!
lrwx------ 1 blau blau 64 mar 22 08:02 2 -> /dev/pts/1

# Descritores de arquivos do processo do subshell...
~/doc/cis $ ls -l /proc/286647/fd
total 0
lrwx------ 1 blau blau 64 mar 22 08:02 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 mar 22 08:02 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 mar 22 08:02 2 -> /dev/pts/1
lrwx------ 1 blau blau 64 mar 22 08:02 255 -> /dev/pts/1
lr-x------ 1 blau blau 64 mar 22 08:02 3 -> 'pipe:[2168309]' ← Pipe!

Neste segundo caso, observe que o shell criou um descritor de arquivos auxiliar 3 para capturar as saídas do subshell.

Técnica da troca de papéis

Para resolver essas duas situações (subshells e redirecionamento da saída padrão), uma opção é fazer um pequeno malabarismo com os descritores de arquivos através de alguns redirecionamentos, por exemplo:

:~$ data=$(dialog --calendar 'data' 0 0 21 03 2022 3>&2 2>&1 1>&3)

Aqui, na ordem em que os redirecionamentos aparecem, o descritor de arquivos 3 é criado para receber uma cópia do descritor de arquivos 2 (STDERR), o descritor de arquivos 2 recebe uma cópia do descritor de arquivos 1 (STDOUT) e o descritor de arquivos 1 recebe uma cópia do descritor de arquivos 3, que é uma cópia da função original do descritor de arquivos 2.

Simplificando: o descritor de arquivos 3 foi cirado para auxiliar na "troca de papeis" entre os descritores 1 e 2.

Observe o resultado:

# TERMINAL 1 - Dialog executado no subshell

:~$ data=$(dialog --calendar 'data' 0 0 21 03 2022 3>&2 2>&1 1>&3)
(exibiu o diálogo normalmente)

# TERMINAL 2 - Descritores de arquivos do 'dialog'

:~$ ls -l /proc/$(pidof dialog)/fd
total 0
lrwx------ 1 blau blau 64 mar 22 08:54 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 mar 22 08:54 1 -> /dev/pts/1       ←--- Trocou!
l-wx------ 1 blau blau 64 mar 22 08:54 2 -> 'pipe:[2280557]' ←--- Trocou!
lrwx------ 1 blau blau 64 mar 22 08:54 3 -> /dev/pts/1

Trabalhando com a saída padrão

Contudo, toda essa complicação pode ser evitada com uma opção do dialog: a opção --stdout. Com ela, o próprio dialog trata de criar o descritor de arquivos auxiliar 3 para permitir a exibição do diálogo enquanto a saída padrão é utilizada para escrever a resposta do utilizador:

# TERMINAL 1 - Dialog executado no subshell

:~$ data=$(dialog --stdout --calendar 'data' 0 0 21 03 2022 3>&2 2>&1 1>&3)
(exibiu o diálogo normalmente)

# TERMINAL 2 - Descritores de arquivos do 'dialog'

:~$ ls -l /proc/$(pidof dialog)/fd
total 0
lrwx------ 1 blau blau 64 mar 22 09:10 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 mar 22 09:10 1 -> 'pipe:[2280161]'
lrwx------ 1 blau blau 64 mar 22 09:09 2 -> /dev/pts/1
l-wx------ 1 blau blau 64 mar 22 09:10 3 -> /dev/pts/1

Como capturar as respostas do usuário

Visto como a coisa funciona, em princípio, nós só podemos capturar as respostas do utilizador redirecionando a saída de erros (STDERR) para um arquivo:

:~$ dialog --calendar 'data' 0 0 21 03 2022 2> respostas
:~$ cat respostas
21/03/2022

Mas, se precisarmos mandar as respostas para variáveis, isso implicará no uso de substituições de comandos, o que nos obrigará a utilizar a técnica da troca de descritores de arquivos ou a opção --stdout:

:~$ data=$(dialog --stdout --calendar 'data' 0 0 21 03 2022)
(diálogo exibido)
~ $ echo $data
21/03/2022

:~$ data=$(dialog --calendar 'data' 0 0 21 03 2022 3>&2 2>$1 1>$3)
(diálogo exibido)
~ $ echo $data
21/03/2022

Também em princípio, não há por que redirecionar a saída padrão (STDOUT) para um arquivo, mas também pode ser feito como nos exemplos acima.

Dialog e dados em arquivos

Antes de utilizar o dialog, reflita se ele é mesmo a solução que você busca para o seu projeto. Para isso, observe alguns detalhes importantes especificamente relacionados ao uso de arquivos como fontes de dados/argumentos.

Limite de argumentos na linha de comando

Cada diálogo do dialog é um comando e o kernel estabelece um limite máximo de argumentos que podem ser passados nas linhas de comandos. O limite é enorme, mas será facilmente atingido se você tentar expandir arquivos com centenas de milhares de linhas para montar dinamicamente um comando do dialog.

No geral, é preferível utilizar o dialog com argumentos que podem ser escritos no próprio código do script, mas dados em arquivos podem ser recebidos através da opção --file. Por exemplo:

:~$ cat arquivo.txt
1 "Teste 1"
2 "Teste 2"
3 "Teste 3"

:~$ dialog --menu teste 0 0 0 --file arquivo.txt

Obtendo...

Inferno das aspas

Alguns diálogos recebem argumentos no formato:

<tag> <texto>

Onde, tanto <tag> quanto <texto> são palavras.

Para o shell, palavras são sequências de caracteres delimitadas por operadores: e espaços, tabulações e quebras de linha são operadores para o shell. Isso nos obriga a recorrer às aspas, o que não é problema algum se esses argumentos forem escritos diretamente nos scripts ou em arquivos (como no exemplo anterior). Mas, novamente, se quisermos obtê-los a partir da leitura de arquivos, nem sempre haverá uma forma simples de escapar textos contendo espaços.

Observe este arquivo:

~ $ cat arquivo.txt
20.03.22-20:42 20.03.22-20:42 Teste 1
20.03.22-20:42 20.03.22-20:42 Teste 2
20.03.22-20:42 20.03.22-20:42 Teste 3

Como no último exemplo, queremos passar seu conteúdo como itens do diálogo de menu, tendo o número de cada linha como <tag> e o conteúdo da linha como <texto>. Uma das formas de realizar esse processamento, poderia ser:

~ $ grep -n '' arquivo.txt | sed -E 's/([0-9]+):(.*)/\1 "\2"/'
1 "20.03.22-20:42 20.03.22-20:42 Teste 1"
2 "20.03.22-20:42 20.03.22-20:42 Teste 2"
3 "20.03.22-20:42 20.03.22-20:42 Teste 3"

Daqui, teríamos duas opções: criar um novo arquivo com a saída acima ou, se o shell for o Bash, executar o processamento em uma substituição de processos, que é o que faremos para efeito de demonstração:

~ $ dialog --menu teste 0 0 0 --file <(grep -n '' arquivo.txt | sed -E 's/([0-9]+):(.*)/\1 "\2"/')

Resultando em:

Conclusão

Ainda há muito o que descobrir sobre o dialog (e nós iremos bem mais fundo no nosso curso de interfaces para scripts em shell), mas eu acredito que os tópicos deste pequeno artigo deverão remover os principais obstáculos nos seus estudos e experimentos.

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

1 Response

Deixe um comentário

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

Post comment