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:
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…
Lê 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.
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…
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.
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:
:~$ :~$
- Resultado esperado: A impressão do valor da variável
- 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:
:~$ :~$
- Resultado esperado: A impressão do valor da variável
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