#+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 ~~ em caixa alta: #+begin_example tem 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 ~~. * 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~ 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: #+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