.. | ||
README.org |
Shell Script na Prática
- Desafio 2: Sua graça
- Anotações da aula 2
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 read
lê 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:
:~$ 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 comoARGUMENTOS
; - Com o comando
[[
, tudo entre os colchetes é interpretado comoEXPRESSÃ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 aoecho
)$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
euntil
: 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