bash/02
2025-08-21 12:22:06 -03:00
..
README.org anotações da aula 2 2025-08-21 12:22:06 -03:00

Shell Script na Prática

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:

:~$ ./salve.sh
Olá, qual é a sua graça?
> Blau
Salve, Blau!

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:

<palavra> tem <n> caracteres

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 aula 1, nós utilizamos os parâmetros posicionais e as expansões de parâmetros para manipular argumentos:

#!/bin/bash

# Expande o primeiro argumento ou "simpatia", se não houver nenhum...
echo "Salve, ${1:-simpatia}!"

Para testar:

:~$ ./salve Fulano
Salve, Fulano!
:~$ ./salve
Salve, simpatia!

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):

#!/bin/bash

# Expande a variável 'nome' ou "simpatia", se 'nome' não existir...
echo "Salve, ${nome:-simpatia}!"

Para testar:

:~$ nome=Fulano ./salve.sh
Salve, Fulano!
:~$ ./salve
Salve, simpatia!

Leitura de arquivos com o comando 'read'

No Bash, nós podemos ler o conteúdo de arquivos com o comando interno read:

read [OPÇÕES] [VAR] [< ARQUIVO]

O readuma 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:

:~$ read linha < /etc/os-release
:~$ echo $linha
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"

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:

:~$ read < /etc/os-release
:~$ echo $REPLY
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"

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:

┌───────────┐ stdout  ┌──────────────┐   stdin ┌───────────┐
│ COMANDO 1 │-------->│ ARQUIVO PIPE │-------->│ COMANDO 2 │
└───────────┘         └──────────────┘         └───────────┘

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:

:~$ echo banana | read linha
:~$ echo $linha
                  <--- nada foi expandido!
:~$

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:

:~$ echo banana | bash -c 'read linha; echo $linha'
banana

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:

:~$ read
Uma linha digitada...
:~$ echo $REPLY
Uma linha digitada...

O Bash ainda oferece a opção -p no comando read para que seja definida uma mensagem de prompt:

:~$ read -p 'Digite algo: '
Digite algo: algo
:~$ echo $REPLY
algo

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:

:~$ printf 'Digite sua graça: '; read nome
Digite sua graça: Blau
:~$ echo $nome
Blau

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:

:~$ echo -n 'Digite sua graça: '; read nome
Digite sua graça: Blau
:~$ echo $nome
Blau

Em implementações POSIX, a única opção do read é -r, que impede que barras invertidas (\) sejam interpretadas como caracteres de escape:

:~$ 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...

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:

:~$ read -p 'Digite algo: '
Digite algo: uma linha muito \
> comprida.
:~$ echo $REPLY
uma linha muito comprida.

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:

help read

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:

:~$ 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!

Mas, o usuário poderia digitar espaços, o que resultaria em:

:~$ read -r -p 'Digite sua graça: ' nome
Digite sua graça:                <-- Foram digitados 4 espaços!
:~$ echo "Salve, ${nome:-simpatia}!"
Salve,     !

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:

test ARGUMENTOS
[ ARGUMENTOS ]
[[ EXPRESSÃO ]] (no Bash)
  • 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:

help test    (opções em comum)
help [[      (opções específicas do "test do Bash")

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…

:~$ read nome
                <--- foram digitados 4 espaços
:~$ test $nome = ''
bash: test: =: esperava operador unário

Isso aconteceu porque $nome expandiu quatro espaços, o que, internamente, seria o mesmo que escrever:

          +--- espaços são separadores de palavras!
          ↓
:~$ test      = ''
bash: test: =: esperava operador unário

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:

:~$ read nome
                      <--- foram digitados 4 espaços
:~$ [[ $nome = '' ]]  <--- nenhum erro foi reportado!

Para visualizar o estado de término do último comando executado, nós podemos expandir o parâmetro especial ?:

:~$ read nome
                      <--- foram digitados 4 espaços
:~$ [[ $nome = '' ]]
:~$ echo $?
0

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:

:~$ read nome
Fulano
:~$ [[ $nome = '' ]]
:~$ echo $?
1

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):

:~$ 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

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:

 ==================================================================================
 | 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.                                              |
 ==================================================================================

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:

#!/bin/bash

read -p 'Digite sua graça: ' nome

[[ -z $nome ]] && nome=simpatia

echo Salve, $nome!

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:

:~$ read d m y
25 09 2025
:~$ echo $d/$m/$y
25/09/2025

Se houver menos campos do que a quantidade de variáveis, as últimas não terão um valor associado:

:~$ read d m y
25 09
:~$ echo $d/$m/$y
25/09/

Se houver mais campos do que a quantidade de variáveis, a última receberá todo o restante da linha:

:~$ read d m y
25 09 2025 século XXI
:~$ echo $d/$m/$y
25/09/2025 século XXI

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:

:~$ read data
25 09 2025
:~$ echo $data | awk '{print $NF}'
2025

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:

:~$ read data
25 09 2025
:~$ echo $data | cut -d' ' -f3
2025

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:

:~$ echo $data | rev
5202 90 52

Assim, o cut pode selecionar apenas o primeiro campo:

:~$ echo $data | rev | cut -d' ' -f1
5202

Por fim, basta "desinverter" o campo com o rev novamente:

:~$ echo $data | rev | cut -d' ' -f1 | rev
2025

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…

:~$ echo $data | grep -oP '.* \K.*'
2025

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.

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.

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:

$(COMANDOS)

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:

:~$ read nome
Blau Araujo
:~$ echo "Salve, $(echo $nome | awk '{print $NF}')!"
Salve, Araujo!

Ou ainda:

:~$ read nome
Blau Araujo
:~$ last=$(echo $nome | grep -oP '.* \K.*')
:~$ echo "Salve, $last!"
Salve, Araujo!

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:

:~$ read -a data
25 09 2025
:~$ echo ${data[0]}/${data[1]}/${data[2]}
25/09/2025

Se quisermos o último elemento, basta utilizar -1 no subscrito:

:~$ echo ${data[-1]}
2025

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:

:~$ echo ${data[-2]}
09

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:

:~$ var=banana
:~$ echo ${#var}
6

Se quisermos a quantidade de caracteres do elemento de um vetor, a ideia é exatamente a mesma:

:~$ echo ${data[-1]}
2025
:~$ echo ${#data[-1]}
4

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:

:~$ 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

Nos scripts, o for pode ser melhor estruturado:

for n in $nome; do
    echo $n tem ${#n} caracteres
done

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:

${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.

Por exemplo:

:~$ 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

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:

:~$ 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

O mesmo vale para os comandos [ e [[.

As comparações também podem ser feitas com valores numéricos literais:

:~$ a=banana
:~$ [[ ${#a} -lt 10 ]]; echo $?
0
:~$ [[ 10 -lt ${#a} ]]; echo $?
1