Entries Tagged 'Programação' ↓
February 24th, 2007 — Programação
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
February 11th, 2007 — Programação
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
February 11th, 2007 — Programação
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 🙂
February 10th, 2007 — Programação
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.