Módulo 1: A linguagem

Aula 2: Me chame pelo meu nome

Conteúdo da aula

Problema 2

Escreva um script que receba um NOME pela linha do comando de sua invocação e imprima: “Salve, NOME!”.

Entendendo o problema

O ponto central desta aula está em descobrir como podemos passar dados para um script na sua invocação pela linha do comando. Posto de outra forma, visto que um script, na verdade, é executado por um processo do shell, nós teremos que responder:

  • O que é um processo?
  • O que é um processo do shell?
  • Como ele é iniciado?
  • Como ele pode receber dados?
  • Como esses dados podem ser usados em comandos?

Mais uma vez, porém, não há como responder essas questões sem darmos mais uma olhada em como o sistema operacional lida com a execução de programas e onde o shell se encaixa “no grande esquema das coisas”.

2.1 – Programas e processos

Quando falamos de processos, é comum ficarmos com a impressão de que eles são só programas em execução: afinal, sempre que executamos um programa, um novo processo, associado a ele, é iniciado – contudo, uma associação não é o mesmo que uma definição! É por isso que vamos respirar fundo e dar um passo atrás para, antes de qualquer coisa, entendermos o que é um programa.

O que são programas

Do ponto de vista do sistema, programas são os arquivos binários que contêm instruções, dados e diversas outras informações requeridas pelo sistema para que, primeiro, seus conteúdos sejam validados como programas executáveis e, segundo, todas as suas partes sejam transferidas corretamente para regiões bem definidas na memória do computador. Cada sistema operacional deve especificar o que será considerado um formato binário executável válido, ou seja, como o conteúdo binário do arquivo deverá ser disposto e a quais informações esses dados deverão corresponder.

Neste aspecto, o arquivo de um programa não difere muito do arquivo de uma imagem PNG ou de um áudio OGG, no sentido de que todo arquivo possui uma forma padronizada e predefinida para dizer ao sistema como os seus conteúdos deverão ser tratados.

Exibindo o formato binário de uma imagem:

:~$ file -b ~/Imagens/figura1.png | cut -d, -f1
PNG image data

Exibindo o formato binário de um arquivo de vídeo:

:~ $ file -b ~Vídeos/exemplo.mkv | cut -d, -f1
Matroska data

No caso dos arquivos binários executáveis de programas (que podemos chamar apenas de “binários”), os formatos mais comuns são:

  • GNU/Linux: ELF
  • macOS: Mach-O
  • Windows: PE

Algumas implementações do FreeBSD e do NetBSD também oferecem suporte ao formato Mach-O.

Exibindo o formato binário do programa Bash:

:~$ file -b /usr/bin/bash | cut -d, -f1
ELF 64-bit LSB pie executable

Carga do programa na memória

Quando um programa é executado, o kernel carrega cada parte do programa (as suas seções) na memória segundo uma estrutura de dados padronizada dividida em segmentos. Essa estrutura, também chamada de layout de memória, é definida como uma representação virtual e contínua da verdadeira localização dos dados de um programa na memória: a chamada memória virtual. Deste modo, todos os programas terão acesso limitado e compartilhado à memória principal do sistema, mas trabalharão com a “ilusão” de terem acesso a toda a memória apenas para si mesmos.

O diagrama abaixo, representa (muito superficialmente) a memória virtual que o kernel associa à execução de um programa. Para facilitar o entendimento, imagine a memória virtual como uma estante, onde cada segmento de dados é colocado em uma prateleira.

vm-processo-01.png
Figure 1: Memória virtual atribuída a um programa

As “prateleiras” mais baixas (os endereços mais baixos), receberão o código das instruções em linguagem de máquina (segmento .text) e os dados que forem definidos em variáveis no programa (segmentos .data e .bss). A região central da “estante” é reservada para a alocação temporária de dados trabalhados dinamicamente ao longo da execução do programa (heap) e para a carga de códigos oriundos de bibliotecas ligadas dinamicamente ao programa.

Bibliotecas são arquivos de programas criados com o propósito de serem utilizados por outros programas, o que pode acontecer de forma estática, quando a biblioteca é compilada junto com o arquivo binário do programa, ou dinâmica, quando a biblioteca é carregada na memória apenas quando o programa é executado.

Finalmente, nas “prateleiras” mais altas da memória virtual do programa, nós temos a pilha (ou stack, em inglês), onde, como o nome sugere, dados poderão ser “empilhados” e “desempilhados” pelo programa, quando necessário. É como numa pilha de pratos, só que invertida: o prato na base da pilha está no endereço mais alto da memória, enquanto que novos pratos serão adicionados em endereços cada vez mais baixos.

O que é um processo, afinal?

É exatamente essa estrutura de dados na memória, junto com outras estruturas de dados que o kernel utiliza para administrar a execução de programas, que nós chamamos de processo! Ou seja…

Processo é uma estrutura de dados (na memória) associada à execução de um programa.

É fundamental que se entenda desta forma, mesmo para nós, que não estamos visando desenvolver sistemas, porque há muito mais num processo do que a simples cópia do binário de um programa para a memória e a sua execução: o GNU/Linux é um sistema multitarefas e multiusuário, e tudo isso precisa ser cuidadosamente orquestrado pelo kernel. Como, por padrão, os processos serão iniciados (e administrados) a partir do shell, as implicações para a criação de scripts são inevitáveis!

No terceiro módulo (o sistema), nós exploraremos detalhes mais aprofundados sobre a administração de processos. No momento, porém, o que nos interessa é saber como os processos se relacionam com o shell e a criação e execução de scripts. Portanto, vamos relembrar como os processos são iniciados a partir do shell:

inicio-de-processos-01.png
Figure 2: Como os processos são iniciados

A novidade é que, quando o shell faz chamada de sistema execve, ele precisa passar algumas informações. Segundo as definições dessa chamada de sistema na biblioteca C padrão (man execve), o shell tem que passar três argumentos:

execve(CAMINHO, ARGV, ENV)

Onde CAMINHO é o caminho completo do binário do programa, ARGV é um vetor contendo todas as palavras utilizadas na invocação do programa, incluindo seus argumentos, e ENV (envp, na verdade, por se tratar de um ponteiro), que é o ponteiro para um vetor que contém todas as variáveis, no formato NOME=VALOR, definidas como “exportadas” no shell. Nós já vimos onde, no processo, esses dois vetores vão parar…

stack-processos-01.png
Figure 3: Memória virtual atribuída a um programa

Vetor de ambiente

Quando o shell é iniciado (e nós já veremos como isso acontece), diversas variáveis são definidas e incluídas num vetor interno do shell chamado de environment (ambiente, em inglês), onde cada elemento desse vetor é uma string no formato NOME=VALOR seguida de um caractere nulo (valor ASCII 0). Quando um processo é iniciado pela chamada de sistema execve, o endereço desse vetor na memória (ou seja, seu ponteiro) é copiado na pilha do novo processo.

Para que uma variável seja incluída no vetor de ambiente do shell, ela terá que ser exportada, o que pode ser feito:

  • Pelo comando interno export;
  • Pelo comando interno do Bash declare com a opção -x;
  • Pela atribuição de variáveis antes da invocação de um programa.

Exportando variáveis para qualquer novo processo iniciado pelo shell corrente:

:~$ fruta=banana bicho=zebra
:~$ export fruta bicho

Desta forma, as variáveis fruta e bicho serão passadas para a pilha de qualquer processo que venha a ser iniciado nesta sessão do shell. O mesmo poderia ser feito com o comando interno declare:

:~$ fruta=banana bicho=zebra
:~$ declare -x fruta bicho

De um modo ou de outro, o atributo de exportação das duas variáveis poderia ser conferido, no Bash, com a opção -p do comando declare:

:~$ declare -p fruta bicho
declare -x fruta="banana"
declare -x bicho="zebra"

Se, a partir de algum momento, nós quiséssemos remover variáveis do vetor ambiente, bastaria executar o comando export com a opção -n:

:~$ export -n fruta bicho

Conferindo…

:~ $ declare -p fruta bicho
declare -- fruta="pera"
declare -- bicho="tigre"

Exportando variáveis apenas para um novo processo específico:

:~$ fruta=banana bicho=zebra ./meu-script.sh

Como vimos, quem executa scripts é o shell, logo, as variáveis fruta e bicho só serão exportadas para o novo processo do shell que for iniciado para executar os comandos no script.

O comando declare -x e o utilitário env, ambos sem mais argumentos, listam todas as variáveis exportadas na sessão corrente do shell.

Antes de passarmos para o elemento seguinte na pilha de um novo processo, vale a pena fazer algumas observações…

  • Apesar do uso comum na linguagem coloquial, tecnicamente, não existem “variáveis de ambiente”: o que existem são variáveis exportadas, variáveis do/no [vetor] ambiente ou vetor [de] ambiente, se estivermos falando daquilo que está na pilha de um processo.
  • Embora todos os programas recebam o vetor ambiente nos seus processos, não é obrigatório que todos esses dados sejam utilizados, ou seja, não basta exportar variáveis para scripts e programas: se for o caso, eles terão que ser programados para fazer algo com esses dados.
  • Esta pode ser uma pista para uma possível solução do nosso problema!

Quantidade e Vetor de argumentos

Todas as palavras na linha do comando que corresponderem à invocação de um programa, incluindo seus argumentos, serão passadas para a chamada de sistema execve. Quando executada, a lista dos argumentos (também separada por um caractere nulo) será incluída na pilha do novo processo acima do vetor de ambiente, enquanto a quantidade total de palavras será registrada no topo de tudo (o topo da pilha está embaixo, não se esqueça).

Para o nosso uso em linhas de comandos e scripts, o shell possibilita expandir os dados dos argumentos do seu processo na forma de parâmetros posicionais e dos parâmetros especiais: *, @ e #.

Parâmetros do shell relacionados com argumentos

Parâmetro O que expande
0 A palavra utilizada para iniciar o processo do shell.
1 … N Cada argumento da invocação do shell, se houver.
# Quantidade de argumentos da invocação do shell (zero, se não houver).
* Todas as palavras utilizadas como argumento da invocação do shell.
@ Todas as palavras utilizadas como argumento da invocação do shell.

2.2 – Parâmetros do shell

Para o shell, todas as entidades que identificam valores são parâmetros, que podem ser um nome, um número ou um caractere gráfico – se o identificador for um nome, nós teremos uma variável, caso contrário, poderá ser um parâmetro posicional (identificado por um número) ou um parâmetro especial (identificado por um caractere gráfico).

Parâmetros do shell

Parâmetro Formação
Variável Caracteres alfabéticos da tabela ASCII, o caractere sublinhado e dígitos numéricos, desde que não sejam o primeiro caractere do nome.
Parâmetro posicional Qualquer combinação de números. Se houver mais de um dígito, o número deve ser escrito entre chaves: {N}.
Parâmetro especial Caracteres gráficos #, *, @, -, $, ? e !.

Portanto, podemos dizer que toda variável é um parâmetro, mas nem todo parâmetro é uma variável.

Parâmetros especiais

Parâmetro O que expande
# Quantidade de argumentos da invocação do shell (zero, se não houver).
* Todas as palavras utilizadas como argumentos da invocação do shell.
@ Todas as palavras utilizadas como argumentos da invocação do shell.
- As opções de início do shell na forma de flags.
$ O PID do processo corrente do shell.
? O valor retornado ao término do último comando executado.
! O PID do último processo executado em segundo plano (job).

O manual do Bash inclui o parâmetro 0 (zero) na lista dos parâmetros especiais, mas eu prefiro deixá-lo entre os parâmetros posicionais em nome da coerência conceitual: afinal, zero é um número!

Outra diferença importante entre as variáveis e os demais parâmetros é que, de modo geral, nós podemos criar variáveis, mas apenas o shell pode definir e gerenciar os parâmetros posicionais e especiais. A exceção, é quando o próprio shell define variáveis internamente como imutáveis, ou seja, de modo que elas só poderão ser expandidas (read only).

Ordem de processamento de comandos

Uma coisa que geralmente passa despercebida, é que o shell realiza diversos processamentos nas nossas linhas de comandos antes delas serem, de fato, executadas. Esse processamento ocorre numa ordem bem específica de cinco etapas. Na primeira, os apelidos são expandidos e, respeitando as regras de citação, todas as palavras e metacaracteres são coletados. Em seguida, de acordo com as combinações de palavras e operadores encontradas, o shell monta os comandos e determina se eles são comandos simples, complexos (listas de comandos e pipes) ou compostos (se houver palavras reservadas).

As expansões só serão processadas na terceira etapa, também seguindo uma ordem específica de acordo com os tipos de expansões encontradas na linha do comando. Depois das expansões, já na quarta etapa, se houver algum, o shell providenciará os recursos necessários os redirecionamentos, como a criação de arquivos, por exemplo. Finalmente, na quinta e última etapa, o shell verificará se precisa monitorar o estado de término (sucesso ou erro) dos comandos encontrados. Só depois de processar essas cinco etapas, é que a linha do nosso comando será executada: e, neste ponto, ela poderá ser bem diferente daquilo que nós digitamos.

É fundamental termos sempre em mente a ordem em que cada uma das etapas de processamento acontece: é muito fácil escrevermos sintaxes aparentemente válidas, mas que nunca resultarão no que esperamos. Portanto, aqui está um esquema geral:

diagrama-5-etapas.png
Figure 4: As cinco etapas de processamento de comandos.

Expansão de parâmetros

O Bash é capaz de processar sete tipos de expansões: e nós veremos todas elas ao longo do curso. Por enquanto, porém, vamos nos concentrar naquele tipo que é chamado de expansão de parâmetros, terminologia que engloba tanto a expansão de variáveis quanto a dos demais parâmetros do shell. A sintaxe geral da expansão de parâmetros é:

${IDENTIFICADOR}

Caso IDENTIFICADOR seja um parâmetro ou o nome de uma variável escalar (um nome que identifica apenas um valor), as chaves são opcionais: desde que os caracteres seguintes não causem confusão quanto ao que realmente queremos expandir.

Vejamos alguns exemplos…

:~$ fruta=banana
:~$ echo ${fruta}
banana
:~$ echo $fruta
banana

Com ou sem chaves, o resultado foi o mesmo: o shell processou a expansão da variável fruta, que passou a ser o argumento do comando echo – isso, após as cinco etapas de processamento da linha do nosso comando.

Mas as chaves seriam necessárias neste outro caso, onde queremos concatenar as palavras expandidas para formar a frase “banana ou bananada?”:

:~$ fruta=banana
:~$ echo $fruta ou $frutada?
banana ou ?
:~$ echo $fruta ou ${fruta}da?
banana ou bananada?

No primeiro caso, a expansão de fruta foi seguida de caracteres que formam nomes válidos para variáveis (d e a), resultando na expansão do nome de uma variável inexistente: =frutada+. Aliás, por padrão, o shell não produzirá nenhum erro quando tentarmos expandir uma variável inexistente: o resultado será a expansão de uma string vazia (na prática, a expansão de nada).

As expansões de parâmetros também podem receber símbolos que modificarão os dados expandidos: nesses casos, o uso das chaves deixa de ser opcional! Por exemplo, nós poderíamos querer que a primeira letra da frase fosse expandida em caixa alta (maiúscula), o que poderia ser feito com o modificador ^ antes do nome da variável:

:~$ fruta=banana
:~$ echo ${^fruta} ou ${fruta}da?
Banana ou bananada?

O Bash oferece diversos modificadores para as expansões de parâmetros, todos muito úteis e poderosos como recursos de programação, mas é melhor irmos com calma, priorizando sempre a descoberta dos contextos em que eles seriam aplicáveis.

Afinal de contas, os problemas sempre vêm antes das soluções!

Como as citações afetam as expansões de parâmetros

Como já vimos de passagem na primeira aula, as regras de citação afetam como as palavras serão separadas. Agora, nós descobrimos que esta separação ocorre na primeira etapa de processamento da linha do nosso comando, mas isso não é tudo: todas as expansões podem ser afetadas pelas citações!

Observe este exemplo:

:~$ bicho=zebra
:~$ echo A expansão $bicho resulta em $bicho.
:~$ A expansão zebra resulta em zebra.

Aqui, todas as expansões da variável bicho foram processadas, mas estava claro que a minha intenção era que a primeira não fosse expandida. Neste caso, nós temos algumas opções, como:

echo A expansão \$bicho resulta em $bicho.
echo A expansão '$bicho' resulta em $bicho.
echo 'A expansão $bicho resulta em' $bicho.

Nessas três soluções, a cadeia de caracteres impressa pelo echo seria:

A expansão $bicho resulta em zebra.

Funcionou porque, na primeira solução, o $ foi citado pela barra invertida (\) e, nas outras duas, o cifrão estava entre os caracteres citados pelas aspas simples.

Mas, entre aspas duplas, a situação seria diferente, veja:

:~$ echo "A expansão $bicho resulta em $bicho."
:~$ A expansão zebra resulta em zebra.

Desta vez, o significado do cifrão não foi removido e todas as expansões foram processadas. Isso porque, com aspas duplas, nós temos quatro exceções para a remoção de significados especiais:

  • O caractere $, quando iniciar uma expansão;
  • O acento grave (`), na forma obsoleta de uma substituição de comandos;
  • A barra invertida (\), se vier antes de $, `, \ ou !.
  • E a exclamação (!), se a expansão do histórico estiver habilitada.

Mas as aspas duplas têm outro efeito sobre as expansões, observe o exemplo:

:~$ nome='Luis Carlos'
:~$ set -- $nome
:~$ echo $#
2

O comando interno set tem a seguinte descrição no help:

:~$ help -d set
set - Set or unset values of shell options and positional parameters.

Define ou remove valores das opções do shell e de parâmetros posicionais.

No exemplo, depois dos dois traços (--), eu utilizei a expansão da variável nome para definir as palavras que passariam a ser os parâmetros posicionais do shell corrente. Como eu não usei aspas, duas palavras foram expandidas: Luis e Carlos, resultando numa contagem de dois parâmetros posicionais, que foi o que eu expandi com a cerquilha (#). Para conferir, vejamos o que expandem os parâmetros posicionais 1, 2 e 3 (este último, em tese, inexistente):

:~$ echo $1, $2, $3
Luis, Carlos,

Mas, expandindo nome entre aspas (duplas, é claro)…

:~$ set -- "$nome"
:~$ echo $#
1
:~$ echo $1, $2, $3
Luis Carlos, ,

Os traços (--), na linha do comando interno set, separam o fim das definições de opções do shell e o início das definições dos parâmetros posicionais.

Resumindo o que foi demonstrado até aqui, podemos dizer que as aspas duplas, além de manterem o significado especial do cifrão, possibilitando as expansões que iniciem com ele, fazem com que os caracteres expandidos formem apenas uma palavra na linha do comando.

Muita atenção, as aspas duplas não possibilitam o processamento de qualquer expansão, mas apenas daquelas que são iniciadas com o cifrão!

Parâmetros especiais '@' e '*'

Se você não ficou confuso com as minhas descrições dos parâmetros especiais @ e *, deveria…

Parâmetro O que expande
* Todas as palavras utilizadas como argumentos da invocação do shell.
@ Todas as palavras utilizadas como argumentos da invocação do shell.

No mínimo, você deveria estar se perguntando: “será que eu posso expandir qualquer um dos dois para obter todas as palavras utilizadas como argumentos na invocação do shell?” – e eu diria que, em princípio, é isso mesmo… Mas não se a expansão for feita entre aspas duplas!

Veja, sem aspas, nós temos o mesmo resultado:

:~$ nome='Luis Carlos'
:~$ set -- $nome
:~$ echo $#
2
:~$ printf '%s\n' $@
Luis
Carlos
:~$ printf '%s\n' $*
Luis
Carlos

Aqui, eu utilizei o builtin printf com a string de formato '%s\n', que sempre será o primeiro argumento que o printf receber. A função da string de formato é definir como cada um dos demais argumentos, representados pelo especificador %s, serão exibidos. No caso, eu determinei que uma quebra de linha, escrita com a notação ANSI-C \n, deveria ser incluída depois de cada um dos outros argumentos. Como tanto o @ quanto o * expandiram duas palavras, que são os dois parâmetros na sessão do shell, elas foram impressas em linhas separadas.

A expansão do '@' entre aspas

Quando expandido entre aspas, porém, o @ mantém a separação original dos parâmetros posicionais, como se eles fossem expandidos assim…

"$@" → "$1" "$2" "$3" …

Isso pode ser melhor demonstrado com outro exemplo:

:~$ set – Maria José 'Luis Carlos'
:~$ echo $#
3
:~$ printf '%s\n' $@
Maria
José
Luis
Carlos
:~$ printf '%s\n' "$@"
Maria
José
Luis Carlos

A expansão do '*' entre aspas

Com o * expendido entre aspas, os parâmetros da sessão também terão a sua separação original mantida, mas eles serão concatenados com o primeiro caractere definido em IFS, que é uma variável do shell onde são definidos os caracteres que serão usados para separar palavras após as expansões iniciadas com o $ e da substituição de processos:

expansions-word-split.png
Figure 5: Quando ocorre a separação de palavras.

O resultado seria equivalente a este:

"$*" → "$1caractere$2caractere$3" …

Por padrão, a variável IFS é definida com os caracteres espaço, tabulação (\t) e quebra de linha (\n). Como o primeiro caractere é o espaço, nós teremos:

"$*" → "$1 $2 $3" …

Mas IFS pode ser mudada para outros caracteres, por exemplo:

:~$ IFS='|'

Assim, o primeiro (e único) caractere passaria a ser a barra vertical (|), e isso vai nos ajudar a visualizar o que acontece na expansão do * entre aspas:

:~$ set – Maria José 'Luis Carlos'
:~$ echo $#
3
:~$ printf '%s\n' $*
Maria
José
Luis
Carlos
:~$ printf '%s\n' "$*"
Maria José Luis Carlos
:~$ IFS='|'
:~$ printf '%s\n' "$*"
Maria|José|Luis Carlos

Repare que a barra (|) foi interpolada entre cada um dos parâmetros do shell, mas o espaço original em Luis Carlos foi mantido.

Para restaurar IFS, nós podemos expandir uma citação de caracteres ANSI-C:

:~$ IFS=$' \t\n'

2.3 – Possíveis soluções

Com o que vimos até aqui, nós temos pelo menos duas abordagens que podem nos levar à solução do problema: exportar uma variável para o script ou passar o nome como um argumento na invocação do script. Então, bora testar os dois conceitos!

Exportando uma variável para outra sessão do shell

Como a execução de um script sempre resulta no início de uma sessão do shell, vamos demonstrar o conceito invocando o Bash na linha de comandos:

:~$ nome='Luis Carlos'
:~$ bash
:~$ echo "Salve, $nome"
Salve,
:~$ exit

Como a variável nome foi definida, mas não foi exportada, a nova sessão do shell, iniciada com a invocação do binário bash, não a recebeu no ambiente de seu processo e nada foi expandido.

Se este é o problema, vamos tentar novamente, desta vez exportando nome:

:~$ export nome='Luis Carlos'
:~$ bash
:~$ echo "Salve, $nome"
Salve, Luis Carlos
:~$ exit

Sucesso! Então vamos fazer tudo novamente, só que numa linha de comando:

:~$ nome='Luis Carlos' bash
:~$ echo "Salve, $nome"
Salve, Luis Carlos
:~$ exit

Sensacional, nós já temos a primeira possível solução para o problema e já podemos criar o script, digamos, nome-export.sh:

:~$ cat << 'FIM' >> nome-export.sh
> #!/bin/bash
>
> echo "Salve, $nome!"
> FIM
:~$ chmod +x nome-export.sh
:~$ nome='Maria Antonieta' ./nome-export.sh
Salve, Maria Antonieta!

Funciona que é uma beleza!

Aproveitando a empolgação, vamos testar o segundo conceito…

Passagem de argumentos para outra sessão do shell

Novamente, vamos testar invocando o executável do Bash. O problema é que, por padrão, se eu passar um nome para ele, este argumento será interpretado como o nome de um arquivo.

Sendo assim, eu preciso passar o nome com a opção -s, como podemos ler no manual (man bash) em tradução livre:

Esta opção permite que parâmetros posicionais sejam definidos na invocação de um shell interativo ou na leitura de uma entrada via pipe.

Então, vamos testar…

:~$ bash -s 'Luis Carlos'
:~$ echo "Salve, $*"
Salve, Luis Carlos
:~$ exit

Massa demais!

Será que dá para passar vários argumentos e imprimir com o printf?

:~$ bash -s Maria José 'Luis Carlos'
:~ $ printf 'Salve, %s\n' "$@"
Salve, Maria
Salve, José
Salve, Luis Carlos
:~$ exit

E não é que deu!

Então, vamos colocar isso num script, desta vez, nome-args.sh:

:~$ cat << 'FIM' >> nome-args.sh
> #!/bin/bash
>
> printf 'Salve, %s!\n' "$@"
> FIM
:~$ chmod +x nome-args.sh
:~$ ./nome-args.sh Maria José 'Luis Carlos'
Salve, Maria!
Salve, José!
Salve, Luis Carlos!

Passando apenas um argumento…

:~$ ./nome-args.sh 'Maria Antonieta'
Salve, Maria Antonieta!

Aí está, segundo problema solucionado!

Exercícios

1. Expansões do shell

Pesquise, analise e explique por que as expansões abaixo não funcionam.

Expansão de chaves:

:~$ echo {1..5}
1 2 3 4 5  ← Resultado esperado…
:~$ var=5
:~$ echo {1..$var}
{1..5}     ← Não era isso que eu queria!

Expansão do til:

:~$ echo ~
/home/blau         ← Resultado esperado…
:~$ echo "Meu diretório é ~"
Meu diretório é ~  ← Não era isso que eu queria!

Expansão de parâmetros:

:~$ var=banana
:~$ echo $var-prata
banana-prata   ← Resultado esperado…
:~$ echo $var_prata
               ← Não era isso que eu queria!

2. Variáveis exportadas

Escreva um comando que permita afirmar, visualmente, se as variáveis abaixo são exportadas ou não:

  • HOME
  • IFS
  • USER
  • PATH
  • PPID
  • BASH_VERSION
:~$

3. Parâmetros posicionais

Dado o comando abaixo…

:~$ set -- banana laranja abacate

Escreva comandos que exibam:

  • O número total de argumentos;
  • O nome utilizado na invocação do shell;
  • O terceiro argumento passado para o shell;
  • Todos os argumentos passados para o shell (um por linha).
:~$

:~$

:~$

:~$

4. Utilitários do sistema

Pesquise e descreva o que fazem os seguintes utilitários:

  • man -k:
  • cut:
  • file:
  • stat:

5. Parâmetros especiais

Utilizando os parâmetros especiais, escreva comando que informem…

Se a sessão corrente do shell é interativa:

:~$

O PID do processo do shell corrente:

:~$

6. Comandos internos do Bash

Defina e elabore pelo menos dois exemplos de uso de cada um dos seguintes comandos internos do Bash:

  • export:
  • declare:
  • typeset:
  • unset:
  • shift:

7. Fulano gosta de frutas, mas…

Escreva um script que imprima a mensagem: “NOME gosta de FRUTA, mas preferiu um DOCE”.

Exemplo de uso do script:

:~$ ./script.sh NOME FRUTA DOCE

8. Expansões de parâmetros

Pesquise e descubra uma forma para que o script anterior imprima “pudim”, caso o terceiro argumento (DOCE) não seja passado na linha do comando.

9. Mais expansões de parâmetros

Com a mesma solução encontrada para o exercício anterior, faça com que seja impressa a mensagem “Você gosta de banana, mas preferiu um pudim.”, caso não seja passado nenhum argumento na invocação do seu script.

10. Exportação de variáveis para o script

Crie uma versão do seu script para receber dados por exportação de variáveis em vez de argumentos.

Importante: ele terá que se comportar do modo que ficou após o exercício 9!