diff --git a/02/README.org b/02/README.org index fbd0050..e30c447 100644 --- a/02/README.org +++ b/02/README.org @@ -24,7 +24,8 @@ de saudação. Exemplo: #+begin_example -:~$ Olá, qual é a sua graça? +:~$ ./salve.sh +Olá, qual é a sua graça? > Blau Salve, Blau! #+end_example @@ -53,3 +54,721 @@ 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 +