Entries from February 2007 ↓

Listas de opções interativas em bash

Durante o feriado do carnaval, decidi brincar um pouco mais com o bash. Desta vez, descobri como detectar se as teclas direcionais do teclado foram pressionadas pelo usuário, o que, combinado às técnicas apresentadas no meu post anterior, me permitiu desenvolver um script que apresenta um menu com diversas opções sobre as quais o usuário pode navegar.

Seqüências de escape são “o ouro”, como dizem! Elas permitem realizar uma série de coisinhas legais, como mudar a posição do cursor (o clássico gotoxy) ou limpar a linha atual do terminal. Para quem não sabe, uma seqüência de escape é uma série de caracteres normalmente iniciada após pressionar a tecla ESC que permite que uma máquina ou aplicação execute um comando [1]. No escopo deste post, estamos falando das seqüências de escape ANSI X3.64 [2], usadas pela maioria dos terminais presentes nas distribuições Linux.

Para fazer um pequeno teste, experimente o seguinte: abra um terminal e pressione ENTER algumas vezes. Agora, digite o seguinte:

echo -e "\e[2;1He[2Khello, world"

O cursor moveu-se para o início da segunda linha do terminal, e foi impressa a mensagem “hello, world”. Explicando o comando:

echo – comando para imprimir mensagens no terminal. A opção -e permite que sejam usadas seqüências de escape.

e[2;1H – esta sequencia de escape (e representa o ESC) move o cursor para a primeira linha da segunda coluna.

e[2K – seqüência de escape que limpa a linha atual.

hello, world – a mensagem a ser impressa

As seqüências de escape também permitem alterar a cor da fonte de do fundo do terminal. O comando abaixo, por exemplo, imprime a mensagem “hello, world” com fundo azul e letra branca:

echo -e "\e[44;37mhello, world"

Além de brincar com as seqüências de escape, para escrever o script abaixo tive que descobrir como ler as tecla direcionais. Para isso, basta executar o comando read e pressionar alguma das teclas e ver o que é impresso. No caso da tecla direcional para cima, por exemplo, podemos ver que a string ^[[A é impressa no terminal. Seguindo esta dica, aprendi que, na verdade, ^[ também é o mesmo que a tecla ESC. Portanto, se quisermos “imprimir” uma tecla direcional para cima, podemos executar o comando

echo -e '\e[A'

Não há muita utilidade em “imprimir” uma tecla direcional (normalmente é impresso um retangulo), mas é possível usar o comando acima para armazenar o valor da tecla em uma variável, para posteriores comparações:

UP_KEY=$(echo -n -e '\e[A')

Abaixo está o script sobre o qual eu falei. Vale ressaltar que os echo‘s usam a opção -n para evitar que seja emitido um caractere de nova linha, o que o comando echo faz por padrão. As técnicas do post anterior foram utilizadas por apenas um motivo: o comando read é usado sem a opção -n. Fiz isso porque as teclas direcionais emitem 3 caracteres por vez, enquanto caracteres normais emitem apenas 1. Portanto, o comando read -n1 não funcionaria para detectar teclas direcionais, porém read -n3 obrigaria o usuário a teclar caso eu quisesse detectar outras teclas. Não é o caso deste script, mas pode ser que alguém queira permitir esse tipo de interação. Se a preocupação for apenas ler teclas direcionais, é possível usar o comando stty apenas com a opção -echo (para que não sejam impressas coisas estranhas na tela quanto o usuário tenta navegar além das opções apresentadas) e o comando read pode ser usado com a opção -n3.

Use as teclas direcionais para cima e para baixo para navegar entre as opções. Use a tecla direcional para a direita para escolher uma opção (apenas “Quit” faz alguma coisa neste script). Meu próximo passo será tentar descobrir como ler a barra de espaço e/ou a tecla ENTER (é preciso descobrir como determinar se uma dessas teclas foi pressionadas ou se nada foi pressionado).

Finalmente, eis o script:

#!/bin/bash

# armazena os valores das setas direcionais
UP_KEY=$(echo -n -e '\e[A')
DOWN_KEY=$(echo -n -e '\e[B')
RIGHT_KEY=$(echo -n -e '\e[C')

# configurações do menu
OPTION=( "Option 1" "Option 2" "Option 3" "Quit" )
NUM_OPTIONS=${#OPTION[*]}
CURRENT=0

# vai para o início da linha atual. como não há uma seqüência de escape única
# para isso, o cursor é movido para o início da linha anterior e a seguir
# para o início da linha seguinte, retornando à linha atual
function startline () {
    echo -n -e "\e[1F"
    echo -n -e "\e[1E"
}

# limpa a linha atual
function clearline () {
    echo -n -e "\e[2K"
    startline
}

# imprime uma opção do menu. se o segunto argumento for um valor verdadeiro
# (1, por exemplo), a opção é impressa como selecionada
function print_option () {
    if [ $2 ]; then
        echo -n -e "\e[7m"
    fi

    clearline

    echo -n -e ${OPTION[$1]}
    echo -n -e "\e[0m"
}

# move a seleção para cima. a sequencia de escape \e[1F move o cursor para
# o início da linha anterior
function move_up () {
    if [ $CURRENT -gt 0 ]; then
        print_option $CURRENT
        CURRENT=$((CURRENT - 1))
        echo -n -e "\e[1F"
        print_option $CURRENT 1
    fi
}

# move a seleção para baixo. a sequencia de escape \e[1E move o cursor para
# o início da linha seguinte
function move_down () {
    if [ $CURRENT -lt $((NUM_OPTIONS-1)) ]; then
        print_option $CURRENT
        CURRENT=$((CURRENT + 1))
        echo -n -e "\e[1E"
        print_option $CURRENT 1
    fi
}

# detecta se uma tecla foi pressionada
function keypressed () {
    read KEY
    [ $KEY ]
}

# armazena as configurações de linha do terminal
STTY_SETTINGS=$(stty -g)

# imprime as opções do menu
for i in $(seq 0 $((${#OPTION[*]}-1))); do
    echo ${OPTION[i]}
done

# vai para a primeira opção e a imprime como selecionada
echo -n -e "\e[${NUM_OPTIONS}F"
print_option $CURRENT 1

# altera as configurações de linha do terminal
stty -icanon min 0 time 0 -echo

# loop do menu
while [ 1 ]; do
    if keypressed; then
        if [ $KEY = $UP_KEY ]; then
            move_up
        elif [ $KEY = $DOWN_KEY ]; then
            move_down
        elif [ $KEY = $RIGHT_KEY ]; then
            if [ $CURRENT = 3 ]; then
                break
            fi
        fi
    fi
done

# restaura a cor padrão do terminal
echo -e '\e[0m'

# restaura as configurações de linha do terminal
stty $STTY_SETTINGS

Referências:

[1] http://en.wikipedia.org/wiki/Escape_sequence
[2] http://www.dicas-l.com.br/artigos/linux-modotexto/coluna11.html

Keypressed e Readkey em Bash

Quem já programou em Pascal provavelmente já usou a dupla de funções keypressed/readkey. A função keypressed serve para detectar se alguma tecla foi pressionada durante a execução do programa. Já a função readkey é usada para descobrir qual tecla foi pressionada.

Esse recurso poderia ser útil no Bash em algumas situações. No Bash, temos o comando read, que lê uma entrada do usuário. Apesar de ser possível utilizar o comando da seguinte maneira,

$ read -n1 KEY

para que o comando retorne assim que o usuário pressionar um tecla, não é possível fazer com que o comando aguarde pela entrada apenas por um período de tempo.

No entanto, é possível alterar as configurações do terminal para alcançarmos este objetivo. O comando stty serve para alterar as configurações de linha do terminal. Assim, podemos utilizá-lo da seguinte maneira para fazer com que o comando read não espere indefinidamente pela entrada do usuário:

stty -icanon min 0 time 0

Ou ainda:

stty -icanon min 0 time 0 -echo

caso não queiramos que a entrada digitada seja também impressa no terminal.

A opção -icanon permite que alguns parâmetros sejam configurados. Dois destes parâmetros são min e time. min determina qual deve ser o tamanho mínimo da entrada. Assim, configurando essa opção como 0, é possível fazer com que nenhum caractere seja lido da entrada. A opção time determina o timeout da leitura em décimos de segundo. Com o valor 0, este tempo será o mínimo possível.

Para ilustrar o uso desses comandos, escrevi o shell script abaixo. Vale ressaltar que a lógicado script é um pouco diferente daquela normalmente utilizada em Pascal. Em Pascal, chama-se keypressed, e, caso a função retorne true, chama-se readkey para descobrir qual tecla foi pressionada. No script abaixo, a ordem é contrária: chama-se readkey, e então keypressed. Se o usuário pressionou alguma tecla quando readkey foi chamada, keypressed retornará 1 (true). A tecla digitada fica armazenada na variável $KEY.

#!/bin/bash

# salva as configurações atuais do terminal
STTY=$(stty -g)

stty -icanon min 0 time 0 -echo

while [ 1 ] ; do
    read -n1 key

    if [ $key ]; then
        echo "Pressed $key."

        if [[ $key = "q" ]]; then
            break
        fi
    fi
done

# restaura as configurações do terminal
stty $STTY

É possível definir funções semelhantes aquelas que encontramos no Pascal:

#!/bin/bash

function readkey() {
    read -n1 KEY
}

function keypressed() {
    [ $KEY ]
}

# salva as configurações atuais do terminal
STTY=$(stty -g)

stty -icanon min 0 time 0 -echo

while [ 1 ]; do
    readkey

    if keypressed; then
        echo "Pressed $KEY."

        if [[ $KEY = "q" ]]; then
            break
        fi
    fi
done

# restaura as configurações do terminal
stty $STTY

Apesar de a lógica ser um pouco diferente, creio que isso não é um problema para a implementação em Bash das mesmas coisas que poderiam ser feitas em Pascal.

Edit: a mesma lógica utilizada em Pascal pode ser implementada em Bash como no script abaixo:

#!/bin/bash

function keypressed() {
    read -n1 KEY
    [ $KEY ]
}

# salva as configurações atuais do terminal
STTY=$(stty -g)

stty -icanon min 0 time 0 -echo

while [ 1 ]; do
    if keypressed; then
        echo "Pressed $KEY."

        if [[ $KEY = "q" ]]; then
            break
        fi
    fi
done

# restaura as configurações do terminal
stty $STTY

Closures

Pois é, meu último post foi sobre alguns desapontamentos com o Common Lisp, mas agora vou escrever sobre uma coisa que achei legal nele. Vai entender…

Uma coisa interessante de se fazer em Common Lisp são os chamados closures (em português acho que seria “fechamento”). Um closure se trata de um encapsulamento de uma variável léxica (em outra linguagens conhecidas como variáveis locais), tornando a sua referência acessível fora do escopo em que foi declarada. Para ilustrar, é melhor mostrar logo algum código.

Primeiro, um pouco de Common Lisp. Assim como em C declaramos variáveis locais, também é possível fazê-lo em CL, da seguinte maneira:

(defun func (arg1 arg2)
  (let ((a 0))
    ...

O Common Lisp também permite a declaração de funções anônimas (lambda). A expressão

(lambda (x) (+ x 1))

declara uma função que recebe um número e retorna o mesmo número somado a 1. Esse tipo de coisa é usado, por exemplo, em mapeamentos de listas, como no exemplo a seguir:

(map #'(lambda (x) (+ x 1)) '(1 2 3))

Esta expressão retorna a lista (2 3 4). Note que a facilidade oferecida pelo operador lambda é o fato de não ser necessário declarar uma nova função (quero dizer, com defun) para ser usada apenas naquele momento.

Agora, sabemos que, em C, algo parecido com isso

void *foo() {
    int a;
    return &a;
}

não funciona, pois a variável é perdida quando a pilha volta ao seu estado anterior. Porém, em Common Lisp, é perfeitamente legal fazer o seguinte:

(defun make-closure ()
  (let ((i 0))
    #'(lambda () (incf i))))

A princípio, pode parecer errado, pois estamos retornando uma função (o operador #’ faz com que seja retornado o objeto que representa a função, evitando que a expressão lambda seja avaliada) que incrementa uma variável local. No entanto, basta testarmos em algum ambiente Common Lisp para vermos que isso de fato funciona:

[1]> (defun make-closure () (let ((i 0)) #'(lambda () (incf i))))
MAKE-CLOSURE
[2]> (defparameter foo (make-closure))
FOO
[3]> (funcall foo)
1
[4]> (funcall foo)
2
[5]> (funcall foo)
3

Note que se chamarmos make-closure outra vez, receberemos um novo closure:

[6]> (defparameter bar (make-closure))
BAR
[7]> (funcall bar)
1
[8]> (funcall bar)
2
[9]> (funcall foo)
4

Uma idéia que tive para o uso de closures é justamente usá-los para definir contadores globais, sem a necessidade de declarar variáveis globais, aumentando o potencial de perda do controle das coisas.

(defun make-closure ()
  (let ((i 0))
    #'(lambda () (incf i))))

(defmacro defcounter (name)
  (let ((counter (make-closure)))
    `(defun ,name () (funcall ,counter))))

Note que defcounter cria ainda mais um closure! Testando:

[3]> (defcounter counter1)
COUNTER1
[4]> (counter1)
1
[5]> (counter1)
2
[6]> (counter1)
3
[7]> (counter1)
4
[8]> (defcounter counter2)
COUNTER2
[9]> (counter2)
1
[10]> (counter2)
2

Funciona 🙂

Desapontamentos com o Common Lisp

Quem convive comigo sabe que nos últimos dias eu andava empolgadíssimo por estar aprendendo alguma coisa sobre Common Lisp, através deste livro.

Bom, acho que hoje caí na real e percebi que o negócio não é tão legal como eu estava pensando. Acho que por causa de toda a propaganda que os Lispers (ou Lispniks, whatever) fazem em cima do Common Lisp, acabei achando que a linguagem ia revolucionar a minha vida de programador. Por isso,acabei achando extremamente fantástico tudo o que eu lia.

No fim das contas, não é bem assim. Somente hoje fui me dar conta de que as features que eu estava achando o máximo no Common Lisp também estão presentes no Python, e com uma sintaxe que eu considero muito mais elegante e mais sucinta.

Além disso, o Common Lisp tem alguns sérios problemas. Um deles é a falta de uma biblioteca padrão com coisas que tornem a vida do programador mais fácil. É verdade que existem diversas bibliotecas escritas por terceiros, e não nego que provavelmente funcionem muito bem, mas particularmente me sinto mais seguro para programar quando a linguagem oferece uma biblioteca, ou melhor, um conjunto de bibliotecas padrão que ofereça mecanismos para trabalhar com expressões regulares, interfaces gráficas ou realizar a comunicação com o sistema operacional.

Outra problema é que o Common Lisp é extremamente verboso. Por exemplo, enquanto em C, Java ou Python um elemento de um vetor pode ser acessado simplesmente através de uma expressão como v[i], em Common Lisp deve-se usar a expressão (aref v i). Pior ainda é atribuir um novo valor à posição desejada: (setf (aref v i) novo-valor).

Uma coisa que também me incomodou um tanto no Common Lisp é o tratamento de nomes de arquivos. É verdade que o mecanismo de pathnames do Common Lisp é bastante interessante e útil, mas às vezes nós queremos simplesmente que o interpretador/compilador reconheça automaticamente que “~/foo_dir” é um diretório e não somente um arquivo qualquer, sem que seja necessário escrever toda uma biblioteca que evite que as coisas não ocorram como devem ocorrer, como a que é discutida no livro mencionado anteriormente.

Apesar dessas críticas, pretendo continuar tentando dar umas chances ao Common Lisp. Os pontos positivos da linguagem são que existem diversas implementações boas, como o CMUCL, que possui um compilador capaz de gerar código tão rápido quanto um compilador C, e o fato de que tudo na linguagem, inclusive os próprios programas, são expressões. O sistema de macros do Common Lisp também é algo único, sendo com certeza uma das grandes marcas da linguagem. Não consigo pensar em alguns exemplos bons no momento, mas o fato de que praticamente tudo na linguagem pode ser tratado como uma expressão que retorna um valor dá diversas opções interessantes ao programador.

A questão é, como eu disse, continuar tentando. Porém, até agora não consegui pensar em nada que eu não resolveria de forma mais elegante em Python ou alguma outra linguagem.