Módulo 1: A linguagem

Aula 3: Qual é a sua graça?

Conteúdo da aula

Problema 3

Este problema será dividido em duas partes…

Parte 1

Escreva um script que receba um NOME pela entrada padrão e imprima: “Falaê, NOME!”.

Parte 2

Escreva um script que, com a mensagem: “Salve, simpatia! Qual é a sua graça?”, solicite a digitação de um NOME que será utilizado para imprimir: “Falaê, NOME!”.

Entendendo os problemas

A essa altura, você já deve ter percebido alguns pontos dignos de nota:

  • Nós estamos dando bastante foco nas formas como os nossos scripts podem receber dados.
  • Todas as formas, vistas até aqui, têm alguma relação com o processo do shell iniciado para executar os nossos scripts.
  • Todas essas formas têm alguma relação com a filosofia UNIX e o conceito de interface CLI.
  • Todas essas formas estão representadas na sintaxe de linhas de comandos.

O mais interessante, é que não será diferente com a recepção de dados pela entrada padrão, porque os fluxos de dados…

  • Também têm relação com processos;
  • Estão previstos na filosofia UNIX e no conceito de interface CLI;
  • E têm representações na sintaxe de linhas de comandos!

Tudo que precisamos, então, é descobrir quais são os recursos e os mecanismos que o shell oferece para a leitura e utilização de fluxos de dados.

3.1 – O caminho até aqui

Na aula anterior, nós descobrimos que é possível passar dados para um script através da exportação de variáveis e da passagem de argumentos na linha de comandos, o que pode ser demonstrado o script nomes.sh, abaixo:

#!/bin/bash

echo "Script: $0"
echo "Salve, $1!"
echo "Salve, $nome!"

Com as devidas permissões, o script poderia ser executado assim:

:~$ nome=Maria ./nomes.sh Pedro
Script: ./nomes.sh
Salve, Pedro!
Salve, Maria!

Neste caso, estas são as relações entre os elementos da linha do comando, os dados e o processo do shell iniciado para executar o script:

cmds-procs01.jpg
Figure 1: Relações entre linhas de comandos e processos.

Repare que, apesar de não ser tão aparente, o processo do shell que executou o script também recebeu acesso à escrita no dispositivo de terminal, como visto na primeira aula, ou nada teria sido impresso na saída padrão. Nós podemos modificar o script nomes.sh para demonstrar essa relação entre o processo do shell iniciado para executá-lo e os fluxos de dados padrão:

#!/bin/bash

ls -l /proc/$$/fd

echo #Só imprime uma quebra de linha

echo "Script: $0"
echo "Salve, $1!"
echo "Salve, $nome!"

Executando…

:~$ nome=Maria ./nomes.sh Pedro
total 0
lrwx------ 1 blau blau 64 jul  7 09:43 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 09:43 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 09:43 2 -> /dev/pts/0
lr-x------ 1 blau blau 64 jul  7 09:43 255 -> /home/blau/nomes.sh

Script: ./nomes.sh
Salve, Pedro!
Salve, Maria!

Sabendo que os descritores de arquivo 0, 1 e 2 correspondem a stdin, stdout e stderr, respectivamente, e que todos eles são herdados de um processo pai para acesso ao terminal (/dev/pts/0, no exemplo), a relação entre os processos e os fluxos de dados fica evidente – até porque a abstração desses fluxos é feita na forma de arquivos no diretório /proc, que é onde os diretórios virtuais de todos os processos são montados.

Para que serve o descritor de arquivos 255

Nada impede que os programas requisitem acesso a outros arquivos além dos dispositivos de terminal. Sendo assim, o processo do Bash faz uma chamada de sistema da família open (man 2 open) a fim de obter uma ligação simbólica para leitura do arquivo do script, que é onde estão as linhas dos comandos que ele deverá interpretar e executar não interativamente:

lr-x------ 1 blau blau 64 jul  7 09:43 255 -> /home/blau/nomes.sh
 ↑                                                          ↑
Apenas leitura!                                Arquivo do script!

Outros shells poderão requisitar acesso para leitura dos arquivos de scripts por outros descritores de arquivos, mas o Bash utiliza o descritor de arquivos mais alto disponível, retrocedendo a partir de 255. Contudo, o ponto que mais nos interessa dessa observação, é relembrar que toda leitura de dados se dá através de descritores de arquivos: sejam os descritores padrão 0, 1 e 2, herdados pelos processos, sejam descritores criados especificamente para o acesso de leitura e/ou escrita de arquivos.

Neste primeiro módulo, nós nos limitaremos ao uso dos descritores de arquivos padrão para leitura e escrita de fluxos de dados, mas não fará mal algum darmos uma espiada rápida, mesmo que sem muitos detalhes, na criação de descritores de arquivos no Bash.

Breve introdução à criação de descritores de arquivos

Como eu disse, o Bash faz uma chamada de sistema da família open para ter acesso a um arquivo. Em princípio, o número do descritor de arquivos seria calculado pelo sistema e retornado para o programa em função de um certo argumento da chamada de sistema execve. Também já vimos que as chamadas de sistema da família exec fazem a troca do conteúdo do processo clonado, na chamada de sistema fork, pelo conjunto de dados do programa que estiver sendo executado. Pois bem, o Bash tem um comando interno que se comporta de forma análoga – o builtin exec:

:~$ help -d exec
exec - Replace the shell with the given command.

Troca o shell por um dado comando.

Na prática, isso significa que o processo do shell corrente será trocado pelo processo daquilo que tiver sido invocado na linha do comando, resultando no término da execução do shell e no início de um novo programa: assim como a chamada de sistema exec faz com o processo clonado.

Sintaxe do comando interno exec
exec [OPÇÕES] [COMANDO [ARGUMENTOS]] [REDIRECIONAMENTOS]

Quando executado sem COMANDO, quaisquer eventuais REDIRECIONAMENTOS terão efeito sobre o processo do shell corrente: e é isso o que possibilita a criação de novos descritores de arquivos para a sessão corrente do shell:

:~$ ls -l /proc/$$/fd
total 0
lrwx------ 1 blau blau 64 jul  7 10:20 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 255 -> /dev/pts/0

Aqui, nós temos os descritores de arquivos atribuídos ao processo da sessão corrente do shell. Então, vamos executar o builtin exec apenas com a indicação de um redirecionamento:

:~$ exec 3> teste.txt

Isso cria o descritor de arquivos 3 para escrita (>) no arquivo teste.txt, como podemos comprovar listando os descritores de arquivos do shell corrente:

:~$ ls -l /proc/$$/fd
total 0
lrwx------ 1 blau blau 64 jul  7 10:20 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 255 -> /dev/pts/0
l-wx------ 1 blau blau 64 jul  7 12:03 3 -> /home/blau/teste.txt
  ↑                                    ↑     ↑
Apenas escrita!                       FD3   Arquivo

Deste modo, nós podemos utilizar o descritor de arquivos 3 para redirecionar a saída padrão, por exemplo, para escrever no arquivo teste.txt:

:~$ echo 'Uma linha de texto' >&3
:~$ cat teste.txt
Uma linha de texto

Este redirecionamento pode ser lido como: redirecione o fluxo de dados na saída padrão (descritor de arquivo 1, omitido) para escrita (>) em quem estiver ligado ao descritor 3 (&3). Nós também poderíamos criar um descritor de arquivos para leitura de dados:

:~$ exec 4< teste.txt
:~$ ls -l /proc/$$/fd
total 0
lrwx------ 1 blau blau 64 jul  7 10:20 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 255 -> /dev/pts/0
l-wx------ 1 blau blau 64 jul  7 12:03 3 -> /home/blau/teste.txt
lr-x------ 1 blau blau 64 jul  7 12:27 4 -> /home/blau/teste.txt
 ↑                                     ↑     ↑
Apenas leitura!                       FD4   Arquivo

O que possibilita a leitura de teste.txt pelo redirecionamento da entrada padrão (<) para quem estiver ligado ao descritor de arquivos 4 (&4):

:~$ $ cat <&4
Uma linha de texto

Para nos livrarmos dos novos descritores de arquivos, basta executar o exec novamente redirecionado-os para o sinal traço (-)…

:~$ exec 3>&- 4>&-

Note que não importa a direção do acesso ao arquivo (leitura ou escrita), porque o destino traço (-) sempre significará o fechamento de um descritor de arquivo.

Para comprovar a remoção…

:~$ ls -l /proc/$$/fd
total 0
lrwx------ 1 blau blau 64 jul  7 10:20 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  7 10:20 255 -> /dev/pts/0

3.2 – Leitura da entrada padrão

Estabelecido que os fluxos de dados também estão relacionados com processos, está na hora de descobrirmos como os fluxos de entrada podem ser lidos a partir das linhas dos nossos comandos. Nós já mencionamos dois mecanismos do shell que dão acesso aos fluxos de dados: os redirecionamentos e os pipes, mas esta é apenas uma parte da solução – afinal, não basta dar acesso aos fluxos, os nossos comandos terão de capturar e processar os dados de alguma forma.

Em outras palavras, nós precisamos de comandos internos e programas que façam alguma coisa com os dados recebidos pela entrada padrão!

O comando interno 'read'

A maioria dos utilitários tradicionais de sistemas UNIX e GNU são capazes de ler dados na entrada padrão e processá-los de alguma forma: como vimos, esta é uma característica da interface de linha de comando (CLI). Contudo, o shell tem apenas um comando interno e, mesmo assim, ele só é capaz de armazenar os dados lidos em variáveis – é o bultin read.

:~$ help -d read
read - Read a line from the standard input and split it into fields.

Em tradução livre…

uma linha da entrada padrão e a divide em campos.

Destaque especial para o fato de que o read só lê uma linha da entrada padrão, que é delimitada pelo caractere de quebra de linha (\n, ASCII 10) ou pelo fim do arquivo. Já a parte da “divisão em campos” diz respeito a como os dados serão armazenados em variáveis.

Veja na sintaxe POSIX do read:

read [-r] VARIÁVEIS

O argumento opcional -r determina como o read vai lidar com a ocorrência de caracteres barra invertida (\) na linha lida – com ele, as barras serão mantidas nas variáveis. Se houver mais argumentos, todos serão interpretados como os nomes das variáveis que receberão cada palavra da linha. Se houver apenas um nome, toda a linha será atribuída a ele; havendo mais de um, as palavras serão distribuídas pelos primeiros nomes e, se ainda houver palavras, o restante da linha será atribuído ao último nome.

Por exemplo:

:~$ read var
Maria tinha um carneirinho.
:~$ echo $var
Maria tinha um carneirinho.

Quando eu dei <Enter> na linha read var, o read ficou esperando receber uma cadeia de caracteres pela entrada padrão (por padrão, o teclado) seguida de uma quebra de linha (no caso, outro <Enter>). Como o read só lê uma linha, o comando foi terminado e todos os caracteres lidos foram atribuídos à variável var – exceto a quebra de linha.

Mas, veja o que aconteceria se eu tivesse passado dois nomes de variáveis como argumentos:

:~$ read var1 var2
Maria tinha um carneirinho.
:~$ echo $var1
Maria
:~$ echo $var2
tinha um carneirinho.

Desta vez, apenas a primeira palavra lida foi para var1, enquanto var2 recebeu o restante da linha. A separação de palavras é feita a partir do primeiro caractere da variável IFS, que nós estudamos na aula anterior:

:~$ IFS=u read var1 var2
Maria tinha um carneirinho.
:~$ echo $var1
Maria tinha
:~$ echo $var2
m carneirinho.

Além de demonstrar como ficou a separação das palavras, o exemplo mostra que nós também podemos definir a variável IFS apenas para o comando read, o que é extremamente útil para a solução de vários problemas!

O comando 'read' no Bash

Diferente das especificações POSIX, o read do Bash pode receber mais opções e argumentos (veja em help read). Por exemplo, nós podemos definir um texto de prompt, evitando dúvidas quanto ao que fazer quando o comando estiver esperando uma linha interativamente:

:~$ read -p 'Digite alguma coisa e tecle ENTER: ' linha
Digite alguma coisa e tecle ENTER: Alguma coisa
:~ $ echo $linha
Alguma coisa

Aqui, a opção -p determinou que a cadeia de caracteres seguinte seria usada como mensagem de prompt. Note, também, que o nome da variável (linha) foi passado como último argumento do comando.

Outra característica do read do Bash, é que nós podemos optar por omitir o nome da variável que receberá a linha lida. Neste caso, será utilizada uma variável interna de nome REPLY, que receberá tudo que for lido:

:~$ read -p 'Digite alguma coisa e tecle ENTER: '
Digite alguma coisa e tecle ENTER: Alguma outra coisa
:~ $ echo $REPLY
Alguma outra coisa

A opção -e também é muito interessante, pois facilita a edição do que estiver sendo digitado.

3.3 – Já temos uma das soluções

A segunda parte do nosso problema diz assim:

Escreva um script que, com a mensagem: “Salve, simpatia! Qual é a sua graça?”, solicite a digitação de um NOME que será utilizado para imprimir: “Falaê, NOME!”.

Nós já sabemos que o read pode nos ajudar aqui, mas vale uma observação: o uso proposto no enunciado implica um tipo de interface em que o utilizador interage com o script (ou um programa) via terminal. Portanto, nós deixamos o foco nos mecanismos da interface CLI para priorizar a implementação de uma interface de terminal, ou TUI (de terminal user interface).

A principal característica de uma interface TUI é a implementação de alguma forma de interatividade com a pessoa utilizadora.

Sendo assim, vamos criar o script seunome-tui.sh com o seguinte conteúdo:

#!/bin/bash

echo 'Salve, simpatia! Qual é a sua graça?'
read nome
echo "Falaê, $nome!"

Dadas as devidas permissões, vamos executá-lo:

:~$ ./seunome-tui.sh
Salve, simpatia! Qual é a sua graça?
Blau
Falaê, Blau!

Expansão condicional de valor padrão

Funcionou, mas o que acontece se não for digitado nenhum nome?

:~$ ./seunome-tui.sh
Salve, simpatia! Qual é a sua graça?
Falaê, !

Precisamos dar um jeito nisso…

Felizmente, o Bash nos oferece uma solução perfeita para este novo problema: as expansões condicionais de parâmetros.

Table 1: Expansões condicionais de parâmetros.
Expansão Descrição
${NOME:-STRING} Também chamada de “valor padrão”, expande STRING se NOME for nulo ou não definido.
${NOME:=STRING} Se NOME for nulo ou não definido, expande a STRING e a atribui a NOME.
${NOME:+STRING} Só expande STRING se a variável NOME estiver definida e não for nula.
${NOME:?STRING} Se NOME for nulo ou não definido, expande STRING e interrompe a execução do comando com erro. Se essa expansão for utilizada não interativamente (num script, por exemplo), causa o término do shell com erro.

Vejamos alguns exemplos…

Expansão de valor padrão
:~$ a=123 b= str='Variável nula ou não definida!'
:~$ echo ${a:-$str}
123
:~$ echo ${b:-$str}
Variável nula ou não definida!
Expansão e atribuição de valor padrão
:~$ a=123 b= default=456
:~$ echo ${a:=$default}
23
:~$ echo $b

:~$ echo ${b:=$default}
56
:~$ echo $b
56
Expansão de string alternativa
:~$ a=123 b= str='Variável definida e não nula!'
:~$ echo ${a:+$str}
Variável definida e não nula!
:~$ echo ${b:+$str}
    ← Nada foi expandido!
:~$

Então, já que existem esses recursos, vamos alterar o nosso script…

#!/bin/bash

echo 'Salve, simpatia! Qual é a sua graça?'
read nome
echo "Falaê, ${nome:-ser misterioso}!"

Testando…

:~$ ./seunome-tui.sh
Salve, simpatia! Qual é a sua graça?
Blau
Falaê, Blau!
:~$ ./seunome-tui.sh
Salve, simpatia! Qual é a sua graça?
Falaê, ser misterioso!

Sensacional demais!

Uma última observação: o read não é o único comando interno do Bash para a leitura da entrada padrão: nós também temos o comando mapfile, mas este será um assunto para as próximas aulas.

3.4 – Lendo 'stdin' não interativamente

A principal diferença de uma abordagem interativa (TUI) para uma solução não interativa (CLI) é que, em vez de contarmos com a passagem de dados via digitação, nós teremos que preparar o nosso script para trabalhar com pipes e redirecionamentos.

O próprio read pode dar conta de ler fluxos de dados não interativamente: basta ler o arquivo que estiver ligado à entrada padrão do processo do shell em execução. Por exemplo, nós podemos ler a primeira (e única) linha do arquivo /proc/uptime, que registra o tempo de atividade do sistema:

:~$ bash -c 'read linha; echo $linha' < /proc/uptime
698804.52 10866927.46

Aqui, nós fizemos um redirecionamento da entrada padrão do Bash (executado não interativamente com a opção -c) para leitura (<) do arquivo, mas também poderíamos fazer um encadeamento por pipe:

:~$ cat /proc/uptime | bash -c 'read linha; echo $linha'
699079.96 10871254.66

Você deve (ou deveria) estar se perguntando: “mas você não disse que o shell não interativo teria que ler os dados de um arquivo ligado à entrada padrão?” – Sim, mas os pipes também são implementados como arquivos!

Veja…

pipe-vs-redir.jpg
Figure 2: Diferença entre pipes e redirecionamentos.

Diferenças entre redirecionamentos e pipes Acontece que, no fundo, ambos os mecanismos envolvem redirecionamentos, já que desviam os fluxos de entrada e saída de processos para arquivos, mas existem algumas diferenças com implicações importantes.

Table 2: Comparações entre pipes e redirecionamentos
  Redirecionamento Pipe
Tipo de arquivo Quase todos os tipos. Apenas arquivos de pipe.
Contexto Entre um processo e um arquivo. Entre dois processos.
Operadores Operadores de redirecionamento. Operadores de controle.
Acesso Leitura e/ou escrita do arquivo. Escrita e leitura simultâneas.
Sessão do shell Corrente. Subshell.

Talvez você já tenha se esquecido, mas eu disse que existem sete tipos básicos de arquivos em sistemas UNIX like:

  • Arquivos comuns
  • Diretórios
  • Links
  • Dispositivos bloco
  • Dispositivos caractere
  • Sockets
  • Pipes (ou FIFO)

Com exceção dos diretórios, nós podemos acessar qualquer um desses tipos de arquivos com redirecionamentos, mas o mecanismo dos pipes envolve uma chamada de sistema da família pipe (man 2 pipe2) para que o sistema gere um canal unidirecional de comunicação entre dois processos – que é o arquivo do tipo pipe, ou FIFO (de first in first out), propriamente dito.

É isso que nós podemos ver no exemplo abaixo:

:~$ ls -l /proc/self/fd /proc/$(pidof cat)/fd | cat
/proc/8125/fd:  ← PID do cat
total 0
lr-x------ 1 blau blau 64 jul  9 09:10 0 -> pipe:[165217] ← Leitura
lrwx------ 1 blau blau 64 jul  9 09:10 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:10 2 -> /dev/pts/0

/proc/self/fd:
total 0
lr-x------ 1 blau blau 64 jul  9 09:10 3 -> /proc/8124/fd ← PID do ls
lrwx------ 1 blau blau 64 jul  9 09:10 0 -> /dev/pts/0
l-wx------ 1 blau blau 64 jul  9 09:10 1 -> pipe:[165217] ← Escrita
lrwx------ 1 blau blau 64 jul  9 09:10 2 -> /dev/pts/0

Aqui, nós podemos ver que o processo do cat (8125) tem o seu descritor de arquivo 0 ligado ao pipe 165217, que é o mesmo ligado ao descritor de arquivo 1 na listagem do processo do ls (8124).

Outro aspecto interessante a ser observado, é que os redirecionamentos são mecanismos acionados pelo shell na ocorrência de operadores que fazem parte da sintaxe de um comando simples (operadores de redirecionamento). Por sua vez, os pipes são escritos com operadores de controle (operadores de pipe), que separam comandos simples. Então, para que ocorra um fluxo de dados entre o comando que invoca o ls e o comando que invoca o cat, ambos os precisam ser executados ao mesmo tempo (execução assíncrona).

Pipelines, paralelismo e subshells

Diferentes shells adotam abordagens diferentes para lidar com a necessidade de execução assíncrona em pipelines, mas o Bash faz isso executando cada um dos comandos a partir de processos clonados de seu próprio processo: o que nós chamamos de subshells.

Detalhando melhor a pipeline do exemplo, o que eu fiz foi listar os descritores de arquivos do processo do ls (/proc/self/fd) e os do processo que seria iniciado para executar o cat (/proc/$(pidof cat)/fd). Como eu não tinha como adivinhar o número do PID que o processo do cat receberia, eu utilizei uma substituição de comandos para obter, na linha de comando resultante, a expansão da saída do programa pidof.

Esta é a sintaxe de uma substituição de comandos:

$(COMANDOS): Expande os dados produzidos na saída dos COMANDOS.

O utilitário pidof busca (e exibe) o PID de um programa em execução. Como uma substituição de comandos também é executada em um subshell, no fim das contas, a linha do exemplo gerou a execução simultânea de três processos – ou seja, em paralelo – e assim, de quebra, nós acabamos descobrindo que o shell implementa paralelismo através de subshells!

É uma descoberta interessante, porque, se a execução assíncrona de comandos implica o início de processos em subshells, a substituição de comandos também deve utilizar um pipe para se comunicar com o processo do shell (seu processo pai).

Para confirmar essa hipótese, vamos investigar:

:~$ echo "$(ls -l /proc/self/fd)"
total 0
lr-x------ 1 blau blau 64 jul  9 10:14 3 -> /proc/11077/fd
lrwx------ 1 blau blau 64 jul  9 10:14 0 -> /dev/pts/0
l-wx------ 1 blau blau 64 jul  9 10:14 1 -> pipe:[165584]
lrwx------ 1 blau blau 64 jul  9 10:14 2 -> /dev/pts/0

Aqui, eu expandi a saída do ls usando uma substituição de comandos. Como resultado, o comando foi executado em um subshell e o processo do ls (11077) teve sua saída padrão (descritor 1) ligada a um pipe. Para saber quem está na outra ponta dessa comunicação, nós podemos incluir a exibição da listagem dos descritores de arquivos do processo pai (o processo do shell):

:~$ echo "$(ls -l /proc/self/fd /proc/$$/fd)"
/proc/6836/fd: → PID do shell
total 0
lrwx------ 1 blau blau 64 jul  9 09:00 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:00 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:00 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:00 255 -> /dev/pts/0
lr-x------ 1 blau blau 64 jul  9 09:59 3 -> pipe:[187639] ← Leitura

/proc/self/fd:
total 0
lr-x------ 1 blau blau 64 jul  9 10:25 3 -> /proc/11649/fd → PID do ls
lrwx------ 1 blau blau 64 jul  9 10:25 0 -> /dev/pts/0
l-wx------ 1 blau blau 64 jul  9 10:25 1 -> pipe:[187639] ← Escrita
lrwx------ 1 blau blau 64 jul  9 10:25 2 -> /dev/pts/0

Hipótese confirmada!

No caso da execução em segundo plano, iniciada com o operador de controle &, também acontece a criação de um subshell e de uma conexão via pipe com o processo pai. Contudo, devido a características que são próprias desse tipo de paralelismo, não temos como elaborar uma demonstração sem a introdução de complexidades desnecessárias neste curso. O problema é que os comandos em segundo plano, por definição, não têm acesso ao terminal enquanto estiverem sendo executados. Sendo assim, todo o gerenciamento dos fluxos de dados, e dos próprios processos, é feita internamente no shell.

Mas nós podemos confirmar que, em algum momento, houve uma ligação via pipe com o processo em segundo plano:

:~$  ls -l /proc/$$/fd &
[1] 18210
total 0
lrwx------ 1 blau blau 64 jul  9 09:00 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:00 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:00 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 jul  9 09:00 255 -> /dev/pts/0
lr-x------ 1 blau blau 64 jul  9 11:02 3 -> 'pipe:[243946]'
[1]+  Concluído              ls $LS_OPTIONS -l /proc/$$/fd

Aqui, o número entre colchetes ([1]) identifica o trabalho (job) executado, mas o ponto mais importante é a ligação para leitura de um arquivo de pipe no descritor de arquivo 3 do processo do shell corrente, mas a outra ponta desse pipe está no próprio mecanismo pelo qual o shell gerencia jobs.

Por que os subshells nos pipes importam?

Talvez eu tenha me aprofundado demais nas demonstrações, mas isso tem um propósito. Uma das características dos subshells é que…

O que acontece nos subshells, fica nos subshells!

Um subshell é um clone do processo do shell onde ele foi gerado. Isso significa que, exceto pelo número de identificação (o PID), ele herdará todos os dados e atributos do processo pai, como variáveis, apelidos, funções, descritores de arquivos, etc… Contudo, os dados iniciados em um subshell são apenas desse subshell, ou seja: eles nunca serão passados de volta para o processo pai.

Ignorar esse fato, pode levar a resultados desconcertantes, observe:

:~$ echo banana | read fruta
:~$ echo $fruta

:~$

Parece que o Bash “bugou” pois o read não atribuiu nada à variável fruta. Mas, como eu sempre digo:

O shell nunca erra! Se o resultado não foi o esperado, foi você que esperou o resultado errado.

O problema é que o read foi executado em um subshell e, nele, é que foi feita a atribuição de banana a fruta. Como as alterações nos dados em um subshell não se refletem no processo pai, a variável fruta simplesmente não existe no shell que está rodando no terminal – e, mesmo que existisse, ela não seria alterada.

Para expandir o valor em fruta, nós teríamos que executar outro comando ainda no mesmo subshell, o que é possível com o agrupamento de comandos com chaves:

:~$ echo banana | { read fruta; echo $fruta; }
banana
:~$ echo $fruta

:~$

O agrupamento com chaves, definido com as palavras reservadas { e } é um dos vários tipos de comandos compostos do shell. Sua função, como o nome sugere, é agrupar vários comandos encadeados em um mesmo bloco que, para todos os efeitos, terá o comportamento equivalente ao de um comando simples.

Importante: como as chaves são utilizadas como “palavras reservadas”, elas terão que ser separadas das demais palavras por metacaracteres e, no caso do último comando da lista, por um operador de controle!

Para evitar os efeitos colaterais dos pipes, nós podemos usar “here docs” para enviar saídas de comandos para o read:

:~$ :~ $ read nome << FIM
> $(whoami)
> FIM
:~ $ echo $nome
blau

Ou, já que o read lê apenas uma linha, nós podemos usar uma “here string”:

:~$ read data_hora <<< $(date)
:~$ echo $data_hora
ter 09 jul 2024 13:29:41 -03

3.5 – Mais uma solução

De tudo que nós vimos, acredito que já podemos ligar os pontos para elaborar uma solução para a primeira parte do problema…

Escreva um script que receba um NOME pela entrada padrão e imprima: “Falaê, NOME!”.

Sendo assim, aqui está a minha sugestão para o script seunome-stdin.sh:

#!/bin/bash

read -r nome
echo "Falaê, $nome!"
Testando em um pipe
:~$ echo Luis Carlos | ./seunome-stdin.sh
Falaê, Luis Carlos!
Testando com uma here string
:~$ ./seunome-stdin.sh <<< "Luis Carlos"
Falaê, Luis Carlos!
Testando com uma here doc
:~$ ./seunome-stdin.sh << FIM
> Luis Carlos
> FIM
Falaê, Luis Carlos!
Testando com um redirecionamento de leitura
:~$ cat teste.txt
Luis Carlos
:~$ ./seunome-stdin.sh < teste.txt
Falaê, Luis Carlos!

Esta solução só tem um problema: o read ficará aguardando a digitação de uma linha se o script não for invocado com um pipe ou um redirecionamento… Mas isso é assunto para a próxima aula.

Exercícios

1. Campos de data e hora

No Bash, o comando interno printf, com a string de formato '%(FORMATO)T', é capaz de imprimir datas e horas, como podemos ver abaixo:

:~$ printf '%(%A %F %T)T\n'
quinta 2024-07-11 07:35:06

Utilizando o comando interno read, e o mesmo comando com o printf acima, escreva um comando que separe a saída do printf em três campos e atribua cada um deles a uma variável:

  • Variável ds (dia da semana)
  • Variável dc (data completa)
  • Variável hc (hora completa)
:~$

2. Campos de data

No exercício anterior, a variável dc recebeu a string da data atual no seguinte formato: ANO-MÊS-DIA. Com o read, escreva um segundo comando para separar o valor dessa variável em outros três campos atribuídos às variáveis:

  • Variável dia
  • Variável mes
  • Variável ano
:~$

3. Tempo de atividade do sistema

O arquivo /proc/uptime registra o tempo de atividade do sistema, mas apenas o primeiro valor antes do primeiro caractere ponto (.) expressa a quantidade de segundos desde o boot:

:~$ cat /proc/uptime
94107.40 1453714.59
     ↑
   94107 segundos

Utilizando apenas os comandos internos echo e read, escreva um comando que leia o arquivo /proc/uptime e imprima o tempo total de atividade do sistema em segundos.

:~$

4. Explique e corrija

Explique por que os comandos abaixo não produziram os resultados esperados e faça as devidas correções.

Comando 1
:~$ echo 'Maria tinha um carneirinho' | read -r linha
:~$ echo $linha
  • Resultado esperado: A impressão do valor da variável linha.
  • Resultado obtido: Nada foi impresso.
  • Motivo:

Correção:

:~$
:~$

Comando 2
:~$ read dia mes ano <<< '25/09/1978'
:~$ echo $mes

  • Resultado esperado: A impressão do valor da variável mes.
  • Resultado obtido: Nada foi impresso.
  • Motivo:

Correção:

:~$
:~$

5. Script: contatos

Utilizando apenas comandos internos do Bash, escreva um script que solicite a digitação dos dados abaixo e os registre no arquivo contatos.csv segundo o formato do exemplo:

NOME,APELIDO,E-MAIL,CELULAR

Nota: Cada execução do script deve incluir uma nova linha no arquivo.

Script contatos1.sh:

#!/bin/bash



6. Desafio extra!

Em vez de registrar as novas linhas no arquivo contatos.csv, elas deverão ser incluídas no final do próprio arquivo do script contatos.sh sem que isso afete seu funcionamento.

Script contatos2.sh:

#!/bin/bashV



7. Só a primeira linha

Crie um script que leia a primeira linha do arquivo contatos.csv (exercício 5) e produza no terminal a saída abaixo:

:~$ ./contatos3.sh
NOME   : O NOME (O APELIDO)
E-MAIL : O E-MAIL
CELULAR: O CELULAR

Script contatos3.sh:

#!/bin/bash



8. O utilitário 'xargs'

Entre outras coisas, com a sintaxe abaixo, o utilitário xargs (GNU Find Utils) é capaz de receber linhas de texto pela entrada padrão e passar cada uma delas como argumentos de um comando qualquer.

Sintaxe:

xargs -I{} COMANDO {}

Exemplo:

:~$ cat exemplo.txt
banana-da-terra
laranja-pera
fruta-de-conde
:~$ xargs -I{} echo {} < exemplo.txt
banana-da-terra
laranja-pera
fruta-de-conde

Sabendo disso, crie um script que receba como argumento cada uma das linhas do arquivo contatos.csv e imprima, para cada uma delas, uma saída segundo o mesmo formato do exercício 7.

Nota: o script será executado assim…

:~$ xargs -I{} ./contatos4.sh {} < contatos.csv

Script contatos4.sh:

#!/bin/bash



9. Vetores (arrays)

Apesar de não ser um recurso POSIX, o Bash implementa no read uma outra forma de separar a linha lida em campos: vetores. Um vetor é uma variável que recebe listas de palavras que podem ser expandidas individualmente pela referência aos seus índices.

Com base nas informações abaixo, refaça todos os exercícios anteriores, exceto o número 4, utilizando vetores.

Sintaxe para uso de vetores no read do Bash
read -a NOME
Sintaxe para expansão de elementos de vetores
${NOME[ÍNDICE]}
Exemplo
:~$ read -a linha <<< 'Maria tinha um carneirinho'
:~$ echo ${linha[0]}
Maria
:~$ echo ${linha[1]}
tinha
:~$ echo ${linha[2]}
um
:~$ echo ${linha[3]}
carneirinho