- Eriberto Blog - http://eriberto.pro.br/blog -

Programação shell: a influência do ambiente Bash nos comandos emitidos pelo usuário

Tweet [1]

Bem, este é mais um daqueles posts que estou há muito tempo para escrever. Trata-se de algo simples mas que muitos desconhecem (inclusive quem já programa em shell).

Muitas vezes queremos criar um script e, mesmo parecendo simples, algo dá errado. Quer ver? Faça um teste. Reinicie o seu computador (para limpar o conteúdo de /tmp) e digite:

# cd /tmp
# ls /etc | grep lo*

Depois, execute o mesmo comando dentro de /etc. Para isso, faça assim:

# cd /etc
# ls /etc | grep lo*

Bem, você deve ter notando uma grotesca diferença. Mas o que será que ocorreu? É a influência do Bash.

O Bash está entre a linha emitida pelo usuário e a execução pelo comando em si. Isso pode ser entendido com a figura a seguir.

[2]

Repare que todas as ordens do usuário passam pelo Bash. Com isso, há uma interpretação intermediária. Por exemplo: considere um arquivo, chamado lista.txt, com o seguinte conteúdo:

alho
cebola
(alface)
tomate

Digamos que você queira ver somente as linhas que contenham um parênteses. A solução imediata seria:

$ grep  (  lista.txt

No entanto, o seguinte erro surgiria:

bash: erro de sintaxe próximo do `token' não esperado `lista.txt'

Observe com atenção: o erro foi reportado pelo Bash e não pelo comando grep. Então, a ordem do usuário nem chegou no grep! Isso ocorreu porque, para o Bash, o parênteses tem um formato de uso especial e que faz sentido para ele. Uma solução é fazer o Bash tratar o parênteses como um caractere comum e não como um caractere especial de operação. Uma contrabarra resolve o problema. Veja:

$ grep  \(  lista.txt
(alface)

A contrabarra serve para determinar que o próximo caractere não tenha interpretação especial. Ou seja: trata-se de um caractere simples. Quando adicionamos a contrabarra, o Bash pensa: devo tratar o parênteses como um caractere qualquer, sem sentido especial para mim. Então, vem a mágica: ele retira a contrabarra e passa somente o parênteses para o grep. Veja:

[3]

 

Agora vamos a um caso mais interessante. Imagine que uma linha termine com um ponto. Veja:

alho
cebola
alface.
tomate

Digamos que queiramos ver somente a linha que contém um ponto. A primeira ideia:

$ grep . lista.txt

O resultado seria:

alho
cebola
alface.
tomate

Isso ocorreu porque para o grep, que usa expressões regulares, o ponto representa “qualquer caractere” e casa com todas as linhas, pois todas têm, pelo menos, um caractere. Assim, surge a ideia de proteger o ponto com uma contrabarra. Vamos testar:

$ grep \. lista.txt
alho
cebola
alface.
tomate

Novamente não deu certo. Mas porquê? Simples! Para o shell, contrabarra quer dizer: não interprete o próximo caractere como algo especial (que seria um metacaractere). Então, ele retira a contrabarra e repassa o restante para o grep. Veja:

[4]

A solução é adicionar outra contrabarra, que irá proteger a segunda. Veja:

 

[5]

Na nova situação, o shell viu a primeira contrabarra e pensou: “devo tratar o próximo caractere, que é uma contrabarra, como um caractere comum”. Como isso, a contrabarra, protegendo o caractere ponto, chega ao grep.

Para terminar, o que ocorreu no comando que abriu este post? Simples:

# cd /tmp
# ls /etc | grep lo*

Estando no /tmp, foi dado o comando ls com grep. Ao passar pelo shell, o mesmo interpreta “lo*” como sendo qualquer arquivo iniciado com lo. Como não há nenhum, ele repassa “lo*” para o grep e pronto. No entanto, em:

# cd /etc
# ls /etc | grep lo*

há uma série de arquivos iniciados com lo. Então, o shell substitui o “lo*” pelos nomes desses arquivos e envia para o grep e a confusão está feita. Isso pode ser visto com o comando bash -x, que nos fornece um ambiente interativo que mostra como o bash está raciocinado. Veja:

# bash -x
# cd /tmp
# ls /etc | grep lo*
+ ls /etc
+ grep 'lo*'
alternatives
bash_completion.d
bindresvport.blacklist
[...]

Agora, indo para o /etc:

# cd /etc
# ls /etc | grep lo*
+ ls /etc
+ grep locale.alias locale.gen localtime logcheck login.defs logrotate.conf logrotate.d

Entendeu?

O comando anterior daria certo da seguinte forma:

# ls /etc | grep lo\*
+ ls /etc
+ grep 'lo*'
alternatives
bash_completion.d
bindresvport.blacklist
blkid.tab

Quem programa em shell sabe que tudo que é colocado entre apóstrofos também fica protegido. Então, também valeria:

# ls /etc | grep lo’*’

ou

# ls /etc | grep 'lo*'

Apenas para terminar, para sair do ambiente interativo do shell, digite quit.

Enjoy!