bash/02/README.org

774 lines
22 KiB
Org Mode

#+title: Shell Script na Prática
#+author: Blau Araujo
#+email: blau@debxp.org
* Desafio 2: Sua graça
** Objetivos
- Receber dados interativamente
- Avaliar expressões
- Tomar decisões lógicas
- Dividir linhas de texto em campos
- Contar caracteres
- Executar comandos repetidamente
- Alterar caixas de texto (maiúsculas e minúsculas)
- Comparar valores numéricos
** Enunciado
Solicitar a digitação do nome da pessoa utilizadora e imprimir uma mensagem
de saudação.
Exemplo:
#+begin_example
:~$ ./salve.sh
Olá, qual é a sua graça?
> Blau
Salve, Blau!
#+end_example
** Evolução 1
Se nada for digitado, imprimir ~Salve, simpatia!~.
** Evolução 2
Se várias palavras forem digitadas, apenas a última deve ser utilizada
na saudação.
** Evolução 3
Para cada palavra digitada, imprimir a mensagem abaixo com o primeiro
caractere de ~<palavra>~ em caixa alta:
#+begin_example
<palavra> tem <n> caracteres
#+end_example
** Evolução 4
Alterar o estágio anterior de modo a imprimir ~caractere~ ou ~caracteres~ de
acordo com a quantidade de caracteres de ~<palavra>~.
* Anotações da aula 2
** Receber dados interativamente
Existem 5 formas de passar dados para um script:
- Argumentos de linha de comando;
- Exportação de variáveis;
- Leitura de arquivos (por redirecionamento);
- Leitura da saída de outros comandos (/pipe/);
- Leitura interativa (dados digitados pelo usuário).
*** Manipulação de argumentos
Como visto na [[../01/README.org#headline-16][aula 1]], nós utilizamos os /parâmetros posicionais/ e as
expansões de parâmetros para manipular argumentos:
#+begin_src bash
#!/bin/bash
# Expande o primeiro argumento ou "simpatia", se não houver nenhum...
echo "Salve, ${1:-simpatia}!"
#+end_src
Para testar:
#+begin_example
:~$ ./salve Fulano
Salve, Fulano!
:~$ ./salve
Salve, simpatia!
#+end_example
*** Manipulação de variáveis exportadas
Se, antes da palavra utilizada como /invocação/ do comando, houver uma ou
mais atribuições de variáveis (palavras no formato ~NMOME=VALOR~), essas
variáveis serão exportadas para o processo do shell que será iniciado
para executar o script e, portanto, poderão ser utilizadas como qualquer
outra variável definida localmente.
Exemplo (script ~salve.sh~):
#+begin_src bash
#!/bin/bash
# Expande a variável 'nome' ou "simpatia", se 'nome' não existir...
echo "Salve, ${nome:-simpatia}!"
#+end_src
Para testar:
#+begin_example
:~$ nome=Fulano ./salve.sh
Salve, Fulano!
:~$ ./salve
Salve, simpatia!
#+end_example
*** Leitura de arquivos com o comando 'read'
No Bash, nós podemos ler o conteúdo de arquivos com o comando interno ~read~:
#+begin_example
read [OPÇÕES] [VAR] [< ARQUIVO]
#+end_example
O ~read~*uma linha* de texto recebida, em princípio, pela /entrada padrão/
(o teclado do terminal). Portanto, para que a leitura da linha de um arquivo
seja possível, nós precisamos fazer um /redirecionamento de leitura/, que
associa a entrada padrão (dispositivo /stdin/) para um arquivo aberto para
leitura.
Exemplo:
#+begin_example
:~$ read linha < /etc/os-release
:~$ echo $linha
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
#+end_example
Deste modo, apenas a primeira linha de ~ARQUIVO~ será lida e seu conteúdo
será associado à variável ~VAR~. Se ~VAR~ não for informada, será utilizada
a variável interna ~REPLY~:
#+begin_example
:~$ read < /etc/os-release
:~$ echo $REPLY
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
#+end_example
Além do redirecionamento de leitura, existe uma forma muito utilizada para
*** Leitura da saída de outros comandos
Um dos mecanismos mais característicos de sistemas /Unix-like/, como o GNU/Linux,
é o chamado /pipe/ (de /canalização/). Através desse mecanismo, a /saída padrão/
de um comando é redirecionada para um arquivo especial (do tipo /pipe/) ao
mesmo tempo que a /entrada padrão/ do comando seguinte é redirecionada para
esse mesmo arquivo:
#+begin_example
┌───────────┐ stdout ┌──────────────┐ stdin ┌───────────┐
│ COMANDO 1 │-------->│ ARQUIVO PIPE │-------->│ COMANDO 2 │
└───────────┘ └──────────────┘ └───────────┘
#+end_example
No shell, o encadeamento de comandos por /pipe/ é feito com o /operador de pipe/
(~|~) e, nos nossos scripts, a leitura da saída de outro comando também é feita
com o comando interno ~read~, mas com um detalhe muito importante: no /pipe/,
todos os comandos são executados em novos processos do shell (chamados de
/subshells/) e, por isso, a variável associada à linha lida pelo ~read~ não
estará disponível na sessão do shell em que o comando foi executado!
Por exemplo:
#+begin_example
:~$ echo banana | read linha
:~$ echo $linha
<--- nada foi expandido!
:~$
#+end_example
Contudo, se o comando ~read~ for utilizado em uma sessão não interativa do
shell (como as que executam scripts), os comandos serão executados no
mesmo processo que participa do /pipe/ e, portanto, a variável associada
à leitura do ~read~ estará disponível localmente.
Exemplo:
#+begin_example
:~$ echo banana | bash -c 'read linha; echo $linha'
banana
#+end_example
*** Leitura interativa (dados digitados pelo usuário)
Sem o redirecionamento de leitura, o comando ~read~ adotará seu comportamento
padrão e lerá o dispositivo na /entrada padrão/ (o teclado) para receber uma
linha digitada pelo usuário:
#+begin_example
:~$ read
Uma linha digitada...
:~$ echo $REPLY
Uma linha digitada...
#+end_example
O Bash ainda oferece a opção ~-p~ no comando ~read~ para que seja definida uma
mensagem de /prompt/:
#+begin_example
:~$ read -p 'Digite algo: '
Digite algo: algo
:~$ echo $REPLY
algo
#+end_example
Mas, se estivermos utilizando uma implementação estritamente POSIX do shell
(shell ~sh~, por exemplo), a opção ~-p~ não existe e o prompt pode ser impresso
antes com o comando ~printf~:
#+begin_example
:~$ printf 'Digite sua graça: '; read nome
Digite sua graça: Blau
:~$ echo $nome
Blau
#+end_example
O shell ~dash~, muito utilizado como shell POSIX, implementa a opção ~-n~
no comando ~echo~ (opção também presente no ~echo~ do Bash) para que o texto
impresso não seja terminado com uma quebra de linha. Então, outra opção
compatível seria:
#+begin_example
:~$ echo -n 'Digite sua graça: '; read nome
Digite sua graça: Blau
:~$ echo $nome
Blau
#+end_example
Em implementações POSIX, a única opção do ~read~ é ~-r~, que impede que barras
invertidas (~\~) sejam interpretadas como caracteres de /escape/:
#+begin_example
:~$ read -p 'Digite algo: '
Digite algo: 123\n456
:~$ echo "$REPLY"
123n456
A barra invertida sumiu porque "escapou" o caractere 'n'...
:~$ read -r -p 'Digite algo: '
Digite algo: 123\n456
:~$ echo "$REPLY"
123\n456
A barra invertida foi mantida...
#+end_example
A opção ~-r~ é muito útil para manter a integridade do arquivo lido, mas a sua
omissão pode ser muito útil no uso interativo do ~read~, pois possibilita
o uso da barra invertida para quebrar a digitação de linhas muito longas
em linhas menores:
#+begin_example
:~$ read -p 'Digite algo: '
Digite algo: uma linha muito \
> comprida.
:~$ echo $REPLY
uma linha muito comprida.
#+end_example
O caractere ~>~, exibido no início da linha quebrada, é o /prompt de continuação/
(definido na variável ~PS2~, do Bash), e sempre aparece no terminal quando a
digitação de um comando precisa ser quebrada em várias linhas.
Para mais informações sobre o ~read~ do Bash, consulte:
#+begin_example
help read
#+end_example
O uso do ~read~ para receber dados digitados pelo usuário é o que chamamos
de /entrada interativa/.
** Avaliação de expressões
Na /Evolução 1/, o problema pede que seja exibida a mensagem /"Salve, simpatia!"/
se nada for digitado, o que poderia ser resolvido facilmente com uma expansão
condicional:
#+begin_example
:~$ read -r -p 'Digite sua graça: '
Digite sua graça:
:~$ echo "Salve, ${REPLY:-simpatia}!"
Salve, simpatia!
:~$ read -r -p 'Digite sua graça: '
Digite sua graça: Blau
:~$ echo "Salve, ${REPLY:-simpatia}!"
Salve, Blau!
#+end_example
Mas, o usuário poderia digitar espaços, o que resultaria em:
#+begin_example
:~$ read -r -p 'Digite sua graça: ' nome
Digite sua graça: <-- Foram digitados 4 espaços!
:~$ echo "Salve, ${nome:-simpatia}!"
Salve, !
#+end_example
Embora seja possível tratar a linha associada a ~nome~ para remover eventuais
espaços, uma solução mais direta e semântica pode ser alcançada com uma
avaliação do resultado da expansão da variável utilizando uma das formas do
comando ~test~:
#+begin_example
test ARGUMENTOS
[ ARGUMENTOS ]
[[ EXPRESSÃO ]] (no Bash)
#+end_example
- Com os comandos ~test~ e ~[~ as expressões avaliadas são passadas como ~ARGUMENTOS~;
- Com o comando ~[[~, tudo entre os colchetes é interpretado como ~EXPRESSÃO~.
Apesar das diferenças, o objetivo desses comandos é avaliar se ~EXPRESSÃO~
(passada ou não como ~ARGUMENTOS~) é verdadeira. Se for, os comandos terminarão
com sucesso (estado de término ~0~); caso contrário, terminarão com erro
(estado de término ~1~).
Para saber como as expressões podem ser escritas, consulte:
#+begin_example
help test (opções em comum)
help [[ (opções específicas do "test do Bash")
#+end_example
O comando ~[[~ é chamado de /"test do Bash"/ e se encaixa na categoria de /comandos
compostos/, ou seja, comandos delimitados por /palavras reservadas/ do shell -- logo,
~[[~ e]] ~]]~ são palavras reservadas e, para serem interpretados como palavras, precisam
estar separados de outras palavras na linha do comando.
Um dos operadores que podemos utilizar em todas as versões do ~test~ é o ~=~, que
compara duas /strings/ e avalia verdadeiro se ambas forem iguais. Sendo assim,
nós poderíamos tentar avaliar...
#+begin_example
:~$ read nome
<--- foram digitados 4 espaços
:~$ test $nome = ''
bash: test: =: esperava operador unário
#+end_example
Isso aconteceu porque ~$nome~ expandiu quatro espaços, o que, internamente, seria
o mesmo que escrever:
#+begin_example
+--- espaços são separadores de palavras!
:~$ test = ''
bash: test: =: esperava operador unário
#+end_example
Com o /test do Bash/, porém, tudo entre os colchetes duplos é avaliado como uma
expressão e, se a variável não expandir nada antes do operador ~=~, a comparação
ainda será válida, pois é o resultado da sua expansão que está em avaliação:
#+begin_example
:~$ read nome
<--- foram digitados 4 espaços
:~$ [[ $nome = '' ]] <--- nenhum erro foi reportado!
#+end_example
Para visualizar o estado de término do último comando executado, nós podemos
expandir o parâmetro especial ~?~:
#+begin_example
:~$ read nome
<--- foram digitados 4 espaços
:~$ [[ $nome = '' ]]
:~$ echo $?
0
#+end_example
O estado de término ~0~ representa /sucesso/ e, caso do /test do Bash/, significa
que a avaliação resultou em /verdadeira/. Mas, se o usuário digitasse alguma
coisa, a expressão avaliaria como /falsa/ e o /test do Bash/ terminaria com estado
~1~, representando um término com erro:
#+begin_example
:~$ read nome
Fulano
:~$ [[ $nome = '' ]]
:~$ echo $?
1
#+end_example
*** Testando expansões vazias de forma compatível
A forma compatível de verificar se a expansão de uma variável resulta em
algo que signifique "vazio" é com o operador ~-z~ (de /zero/):
#+begin_example
:~$ read nome
:~$ test -z $nome; echo $?
0
:~$ [ -z $nome ]; echo $?
0
:~$ [[ -z $nome ]]; echo $?
0
:~$ read nome
Beltrano
:~$ test -z $nome; echo $?
1
:~$ [ -z $nome ]; echo $?
1
:~$ [[ -z $nome ]]; echo $?
1
#+end_example
** Decisões lógicas
No shell, exceto em avaliações de expressões, todas as decisões lógicas são
feitas com base em estados de término (/sucesso/ ou /erro/). É isso que se
utiliza em estruturas condicionais (como ~if~) e loops condicionais (como
~while~ e ~until~) para decidir o que será executado em seguida. Porém, estas
não são as únicas formas de condicionar a execução de comandos.
*** Operadores de encadeamento condicional
Comandos simples podem ser encadeados de várias formas com os /operadores
de controle/ do shell:
#+begin_example
==================================================================================
| Operador | Encadeamento |
==================================================================================
| ; | Encadeamento incondicional síncrono (um depois do outro). |
|----------|---------------------------------------------------------------------|
| & | Encadeamento incondicional assíncrono (um ao mesmo tempo do outro). |
|----------|---------------------------------------------------------------------|
| && | Encadeamento condicional "se sucesso". |
|----------|---------------------------------------------------------------------|
| || | Encadeamento condicional "se erro". |
|----------|---------------------------------------------------------------------|
| | | Encadeamento por pipe. |
==================================================================================
#+end_example
Com os operadores de encadeamento condicional, a execução do comando seguinte
fica condicionada ao término com sucesso ou erro do *último comando executado*.
Portanto, no caso da /Evolução 1/ do desafio, nós poderíamos fazer:
#+begin_src bash
#!/bin/bash
read -p 'Digite sua graça: ' nome
[[ -z $nome ]] && nome=simpatia
echo Salve, $nome!
#+end_src
** Dividir linhas de texto em campos
Outra finalidade do comando ~read~ é separar a linha lida em campos. Por padrão,
o caractere espaço é utilizado como delimitador e cada palavra da linha pode
ser atribuída a uma variável diferente.
Exemplo:
#+begin_example
:~$ read d m y
25 09 2025
:~$ echo $d/$m/$y
25/09/2025
#+end_example
Se houver *menos* campos do que a quantidade de variáveis, as últimas não terão
um valor associado:
#+begin_example
:~$ read d m y
25 09
:~$ echo $d/$m/$y
25/09/
#+end_example
Se houver *mais* campos do que a quantidade de variáveis, a última receberá todo
o restante da linha:
#+begin_example
:~$ read d m y
25 09 2025 século XXI
:~$ echo $d/$m/$y
25/09/2025 século XXI
#+end_example
Isso funciona muito bem, mas não se aplica muito ao caso do desafio, pois não
há como saber quantas palavras o usuário irá digitar. Portanto, nos restam
duas opções: aprender a trabalhar com /vetores/ no Bash (variáveis associadas
a uma lista de valores) ou utilizar comandos externos (utilitários).
*** Solução com o interpretador da linguagem AWK
Embora seja muito utilizado como um utilitário qualquer, o ~awk~ é um
interpretador de uma das linguagens de programação mais poderosas criadas
para sistemas /Unix-like/ e pode resolver facilmente o nosso problema:
#+begin_example
:~$ read data
25 09 2025
:~$ echo $data | awk '{print $NF}'
2025
#+end_example
Onde:
- ~print~: instrução para imprimir strings (equivalente ao ~echo~)
- ~$NF~: expansão do campo de número igual ao número de campos
No ~awk~, por padrão, os campos das linhas são delimitados por espaços e
tabulações e são numerados como se fossem os parâmetros posicionais do
shell, onde o campo ~$0~ representa a linha inteira. Portanto, se há 3 campos,
~NF~ será ~3~ e o campo ~$3~ será impresso.
*** Solução com 'cut' e 'rev'
O utilitário ~cut~ separa a linha em campos e imprime apenas os que forem
selecionados na linha do comando:
#+begin_example
:~$ read data
25 09 2025
:~$ echo $data | cut -d' ' -f3
2025
#+end_example
Onde:
- ~-d~: caractere utilizado como delimitador de campos
- ~-f~: lista dos campos a serem impressos
Mas, como não sabemos exatamente a quantidade de campos, nós podemos inverter
os caracteres da linha na variável ~data~ com o utilitário ~rev~:
#+begin_example
:~$ echo $data | rev
5202 90 52
#+end_example
Assim, o ~cut~ pode selecionar apenas o primeiro campo:
#+begin_example
:~$ echo $data | rev | cut -d' ' -f1
5202
#+end_example
Por fim, basta "desinverter" o campo com o ~rev~ novamente:
#+begin_example
:~$ echo $data | rev | cut -d' ' -f1 | rev
2025
#+end_example
*** Solução com o utilitário 'grep' e expressões regulares
O utilitário ~grep~ é uma poderosa ferramenta de filtragem de linhas de texto
a partir de padrões descritos com /expressões regulares/ (REGEX). Com ele e
as especificações de expressões regulares da linguagem Perl (PCRE), é
possível fazer...
#+begin_example
:~$ echo $data | grep -oP '.* \K.*'
2025
#+end_example
Onde:
- ~-o~: imprimir apenas o que casar com a expressão regular
- ~-P~: utilizar especificações PCRE
Com as especificações PCRE, nós podemos utilizar o operador ~\K~, que diz
que todo o padrão escrito antes dele deve ser descartado do casamento. Então,
se a REGEX ~'.* .*'~ casa com qualquer quantidade de qualquer caractere seguida
de um espaço e qualquer outra quantidade de caracteres quaisquer, com o operador
~\K~ somente a parte que vier depois do último espaço será casada com o padrão e,
por isso, impressa pelo ~grep -o~.
#+begin_quote
As expressões regulares serão estudadas mais adiante e são mais simples do
que parece: esta é só uma demonstração do poder dos utilitários disponíveis
para uso via shell no GNU/Linux.
#+end_quote
*** Substituição de comandos (uma nova expansão)
O problema dessas soluções com comandos externos é que todos eles imprimem
suas saídas no terminal -- e nós precisamos desses dados em uma variável
para expandir na mensagem ~"Salve, $nome!"~.
Entretanto, nunca faltam soluções no shell, e nós podemos recorrer a outro
tipo de expansão chamada de /substituição de comandos/:
#+begin_example
$(COMANDOS)
#+end_example
Com ela, os comandos podem ser executados em um /subshell/ e suas saídas são
expandidas, podendo ser utilizadas diretamente numa concatenação de strings
ou atribuídas a variáveis.
Por exemplo:
#+begin_example
:~$ read nome
Blau Araujo
:~$ echo "Salve, $(echo $nome | awk '{print $NF}')!"
Salve, Araujo!
#+end_example
Ou ainda:
#+begin_example
:~$ read nome
Blau Araujo
:~$ last=$(echo $nome | grep -oP '.* \K.*')
:~$ echo "Salve, $last!"
Salve, Araujo!
#+end_example
*** Solução com vetores
A implementação do ~read~ no Bash oferece a opção ~-a~, que atribui cada palavra
da linha, sequencialmente, a um vetor. Vetores são um tipo de variável que tem
seu nome associado não a um valor, mas a uma lista de valores. Cada valor da
lista é chamado de /elemento/ e é identificado por um índice numérico que inicia
com ~0~. Para expandir um elemento qualquer, nós utilizamos o nome do vetor e
um /subscrito/ com o índice desse elemento:
#+begin_example
:~$ read -a data
25 09 2025
:~$ echo ${data[0]}/${data[1]}/${data[2]}
25/09/2025
#+end_example
Se quisermos o último elemento, basta utilizar ~-1~ no subscrito:
#+begin_example
:~$ echo ${data[-1]}
2025
#+end_example
O número ~-1~ significa /"o primeiro de trás para frente"/, e o mesmo princípio
poderia ser utilizado para expandir o penúltimo elemento:
#+begin_example
:~$ echo ${data[-2]}
09
#+end_example
** Contar caracteres
A contagem de caracteres do valor associado a uma variável pode ser feita
com o modificador ~#~ antes do identificador em uma expansão de parâmetros:
#+begin_example
:~$ var=banana
:~$ echo ${#var}
6
#+end_example
Se quisermos a quantidade de caracteres do elemento de um vetor, a ideia
é exatamente a mesma:
#+begin_example
:~$ echo ${data[-1]}
2025
:~$ echo ${#data[-1]}
4
#+end_example
** Executar comandos repetidamente
A execução de comandos repetidamente requer o uso de uma estrutura de
repetição, e o shell tem três delas...
- Estrutura ~for~: controlada por uma lista de palavras;
- Estruturas ~while~ e ~until~: controladas pelo estado de término de um comando.
Para o que é solicitado no desafio, nós podemos utilizar a estrutura ~for~,
também chamado de /loop/ ~for~:
#+begin_example
:~$ read nome
João da Silva
:~$ for n in $nome; do echo $n tem ${#n} caracteres; done
João tem 4 caracteres
da tem 2 caracteres
Silva tem 5 caracteres
#+end_example
Nos scripts, o ~for~ pode ser melhor estruturado:
#+begin_src bash
for n in $nome; do
echo $n tem ${#n} caracteres
done
#+end_src
** Alterar caixas de texto (maiúsculas e minúsculas)
O Bash também é capaz de modificar expansões de modo a alterar a caixa dos
caracteres expandidos:
#+begin_example
${VAR^} - Trona maiúsculo o primero caractere expandido.
${VAR^^} - Trona maiúsculos todos os caracteres expandidos.
${VAR,} - Trona minúsculo o primero caractere expandido.
${VAR,,} - Trona minúsculos todos os caracteres expandidos.
#+end_example
Por exemplo:
#+begin_example
:~$ read nome
João da Silva
:~$ for n in $nome; do echo ${n^^} tem ${#n} caracteres; done
JOÃO tem 4 caracteres
DA tem 2 caracteres
SILVA tem 5 caracteres
#+end_example
** Comparar valores numéricos
Novamente, se temos que comparar valores, isso terá que ser feito pela avaliação
de uma expressão. Utilizando as três implementações do ~test~, nós temos os
operadores de comparação numérica:
- ~-eq~: os números são iguais;
- ~-ne~: os números não são iguais;
- ~-lt~: o número da esquerda é menor do que o da direita;
- ~-le~: o número da esquerda é menor ou igual ao da direita;
- ~-gt~: o número da esquerda é maior do que o da direita;
- ~-ge~: o número da esquerda é maior ou igual ao da direita.
Por exemplo:
#+begin_example
:~$ a=banana b=pitanga
:~$ echo "$a (${#a}), $b (${#b})"
banana (6), pitanga (7)
:~$ test ${#a} -lt ${#b}; echo $?
0
:~$ test ${#a} -eq ${#b}; echo $?
1
:~$ test ${#a} -gt ${#b}; echo $?
1
#+end_example
#+begin_quote
O mesmo vale para os comandos ~[~ e ~[[~.
#+end_quote
As comparações também podem ser feitas com valores numéricos literais:
#+begin_example
:~$ a=banana
:~$ [[ ${#a} -lt 10 ]]; echo $?
0
:~$ [[ 10 -lt ${#a} ]]; echo $?
1
#+end_example