# Escrevendo um 'demake' do select do Korn Shell para o Bourne shell original
… e para POSIX também.
Semana retrasada, o Samuel (callsamu), um
grande amigo meu, me pediu para escrever um script simples para lidar com o
Timewarrior. A ideia era algo simples: um programa
com uma lista de opções que rode constantemente em outro terminal, como um
cronômetro à parte mesmo.
De primeira, ele sugeriu que eu usasse o Korn Shell 93 para fazer o script, até
porque existem funções nele que facilitam a vida de quem quer escrever um
programa interativo, como o próprio select (o tema deste artigo).
No entanto, por se tratar de algo simples, eu decidi fazer em Bourne shell
original (nem sequer no POSIX) e
implementar o que o Korn Shell teria de forma embutida em nome da diversão.
Esse artigo não se trata desse script em si, então vamos direto ao que me
levou a precisar usar o select nesse código.
Em um certo momento, eu queria que fosse possível listar os IDs de cada um
desses cronômetros numa lista a fim de selecioná-los para alguma ação em
específico — afinal, o timewarrior faz uso desses IDs para a esmagadora
maioria das operações.
Existem várias abordagens possíveis, como, por exemplo, pedir para o usuário
inserir o ID em específico manualmente após rodar o timew summary e, caso
não estivesse na lista de IDs, jogar uma mensagem de erro e repetir o pedido.
No entanto, algo como o select ainda seria uma melhor opção, tanto por
reduzir a possibilidade de erro quanto por ser mais rápido de usar, tendo apenas
de digitar o índice numérico da lista.
Cá vai um trecho do manual do Korn Shell 88, onde o select apareceu
primeiro — algo ligeiramente engraçado, pois o shell POSIX, que tem o o 88
como a maior, digamos, inspiração, não inclui essa função, o que talvez seja
compreensível a fim de simplicidade e de não incluir uma das killer features
(e que também não é algo absolutamente insubstituível) do 88 no padrão geral:
select identificador [ in palavras . . . ] ;do lista ;done
O comando
selectexibe na saída de erro padrão (file descriptor nº 2), o conjunto de palavras, cada uma numerada pelo seu índice. Se “in palavras …” vier a ser omitido, então os parâmetros posicionais ($@) serão utilizados em seu lugar.
O prompt PS3 é, então, exibido e uma linha é lida da entrada padrão.
Se essa linha consistir do número de índice de uma das palavras listadas, então o valor da variável identificador será definido para a palavra correspondente. Se a linha estiver vazia, a lista com o conjunto de palavras e seus respectivos índices é impressa novamente. Caso nenhuma dessas condições acima se aplique e a linha contenha um valor válido de índice e nem esteja vazia, a variável identificador é definida como nula, uma string vazia. O conteúdo da linha que fora lida é salvo na variávelREPLY. A lista é executada para cada seleção até que oselectseja quebrado (break), o que é equivalente a um laçowhile, ou até que um caractere de fim de arquivo —EOFou Ctrl + D/^D — seja encontrado.
Caso a variávelREPLYseja nula — ou seja, um índice inválido foi informado —, o conjunto de palavras com seus respectivos índices é impresso novamente, com o prompt PS3 a pedir uma nova resposta — o que é, na prática, o comportamento do caso em que a linha esteja vazia, mencionado anteriormente.
Traduzido e adaptado de KSH88(1), do saudoso site
research.att.com.
Bem, é simples. É uma função que associa elementos a um índice (algo que o
próprio shell já faz com elementos posicionais, $1, $2, etc) e lê o
número do índice para retornar seu elemento correspondente.
O que vai mudar de fato aqui é que não temos como criar uma nova palavra-chave
e copiar toda a funcionalidade um-para-um. Não temos macros aqui, shell não é C.
Por esse motivo, estaremos a fazer um demake da palavra-chave enquanto função,
não um port compatível de forma intercambiável.
Irei pela abordagem de ter apenas os elementos (palavras, como visto no trecho
do manual acima) enquanto argumentos da função e exportar o conteúdo da
linha/índice e o elemento selecionado nas variáveis REPLY e SELECTED,
respectivamente.
Antes disso, precisamos saber como acessaremos o elemento posicional tendo seu
número passado em outra variável.
No Korn Shell 93 — e no Bash, e outros shells que tenham suporte a arrays —,
nós podemos ir por uma via preguiçosa: duplicar o array dos elementos
posicionais em outro e acessar pelo índice.
Cá vai um código extremamente básico para demonstrar:
function indice_elemento {
elems=("$@")
print -v elems
printf >&2 'Digite um índice: '
read i
print -v elems[i]
}
S145% indice_elemento a b c d e
(
a
b
c
d
e
)
Digite um índice: 1
b
S145% : Lembrete: nós contamos do zero aqui pois não
S145% : me dei ao trabalho de adaptar a aritmética
S145% : para um mero exemplo.
No entanto, não temos como declarar arrays no Bourne shell e nem no POSIX, então
essa possibilidade preguiçosa já cai por terra. O que resta é que usemos algo
análogo a um ponteiro em C — ou, já que estamos numa linguagem interpretada,
uma analogia a variáveis
variáveis (sim,
o nome é esse mesmo)
do PHP e aos namerefs do Korn Shell
93
seria mais justa — para que consigamos acessar o valor da variável cujo
identificador está contido em outra variável.
Em 2024, implementei um código para isso no
herbiec:
function eval_per_identifier {
[[ $verbose ]] && set -x
identifier=$1
eval echo -n \$\{"$identifier"\}
unset identifier
}
Dentro do contexto do herbiec (e do conhecimento que eu tinha na época), essa
função acabava por ser “eficiente” e cumpria o trabalho para a maioria das
coisas. Na época eu também não tinha ideia da existência de namerefs nativos
do Korn Shell 93.
Todavia, para esse caso, não precisamos de uma função dedicada a isso, apenas
escrever uma declaração com o valor de \$ (o caractere “$” puro) ao lado do
número de índice/identificador, com tudo isso sendo precedido pelo eval, que
executa a linha de código em definitivo:
eval selecionado="\$$indice"
Sim, eu sei que o eval não é a coisa mais segura do mundo, mas não temos
outra alternativa na prática que seja portátil.
Escrevi uma função para demonstrar como isso funciona:
teste_namerefoide() {
read i
eval elem="\$$i"
echo Selecionado $elem
}
S145% teste_namerefoide Hoje é um belo dia
+ teste_namerefoide Hoje é um belo dia
+ read i
4
+ eval elem='$4'
+ elem=belo
+ echo Selecionado belo
Selecionado belo
Feito isso, agora é só correr para o abraço e implementar uma função que liste os elementos e leia o índice:
select_in() {
prompt3="${PS3:-#? }"
REPLY=0
SELECTED=""
index=0
for elem do
index=`expr $index + 1`
printf >&2 '%d) %s\n' \
"$index" "$elem"
done
printf >&2 '%s' "$prompt3"
read -r REPLY </dev/tty
if [ $? != 0 ]; then
return 1
fi
if [ `expr "x$REPLY" : 'x[0-9]*$'` -gt 0 ] &&
[ "$REPLY" -ge 1 ] && [ "$REPLY" -le $# ]; then
eval SELECTED=\"\$$REPLY\"
fi
export REPLY SELECTED
return 0
}
Resolvi aplicar algumas boas-práticas aqui: primeiramente, estamos lendo a
entrada do /dev/tty ao invés da entrada padrão a fim de evitar eventuais
problemas com redirecionamentos; além disso, também utilizei a opção -r
(de “raw”, “cru”) pois não esperamos ter de tratar nada na string de índice,
afinal é só um inteiro. Vale lembrar que precisei tratar EOFs manualmente,
verificando se o read retornou um código de erro e não apenas verificando se
a string de retorno está vazia.
Também fiz questão de usar aspas duplas no eval, só para garantir.
Ah, e claro, tratei a entrada duas vezes: primeiro, verificamos se a entrada é
puramente numérica — afinal, estamos a lidar com índices — usando o
expr com uma expressão que verifica se a entrada é totalmente numérica
(e, caso for, retorna seu comprimento) e, em seguida, verificamos se o índice
está dentro do intervalo válido que vai de 1 até o número total de elementos.
E bem, cá vai uma curiosidade (e um spoiler de um futuro JdP que aborde o
Heirloom NG): o Bourne shell não suporta
expressões aritméticas nativamente, logo se torna necessário usar uma
calculadora externa para tal, sendo o expr a mais comum por ser simples e
rápido. Sim, o expr é como um canivete suíço no Bourne shell original e
ainda encontra muito uso mesmo no POSIX.
Para usar essa função é simples, sendo apenas questão de inseri-la na condição
de um while sem um subshell (afinal precisamos das variáveis exportadas
REPLY e SELECTED):
while select_in insira os elementos cá; do
: [...]
done
E cá vai um código de exemplo, usando todas as funcionalidades que implementamos. No espírito do Gramado in Concert, achei que seria interessante usar a famigerada escala de Dó como exemplo:
PS3='Qual nota? '
while select_in Dó Ré Mi Fá Sol Lá Si; do
case "x$SELECTED" in
'x') : "SELECT" está vazio.
if [ -z "$REPLY" ]; then
echo 'Você não selecionou nada?'
else
echo "Índice inválido: $REPLY"
: Sai da seleção.
break
fi ;;
x'Dó'|x'Fá'|x'Sol') echo "Nota $SELECTED, ou você quis dizer clave?" ;;
*) echo Nota $SELECTED ;;
esac
done
E bem, talvez uma pergunta que possa ser-me feita é qual a licença para
reutilizar esse código. Tudo nesse blog está na CC-BY
4.0, que permite
redistribuição franca desde que haja atribuição de créditos.
Por mim, faça o que tu queres, só credita-me também.
Finalizo esse artigo já de volta à Serra, depois de ter feito uma parte no ônibus e, claro, a grande maioria no litoral. Agradeço por ter lido até aqui. Até à próxima.