Módulo 1: A linguagem

Aula 1: Salve, Simpatia!

Conteúdo da aula

Problema 1

Escreva um script em shell que imprima a mensagem “Salve, simpatia!” no terminal.

Entendendo o problema…

O problema traz algumas questões fundamentais que serão estudadas nesta aula:

  • O que é um shell?
  • O que é um script?
  • Como criá-los?
  • O que significa “imprimir”?
  • O que é um terminal?
  • Como “imprimir” uma mensagem nele?

Contudo, não há como compreender o porquê da importância do shell, ou dos scripts, sem contextualizar suas origens e funções em sistemas operacionais semelhantes ao UNIX, como o nosso GNU/Linux – ou ainda, antes disso, o que é um sistema operacional.

O UNIX é uma especificação de sistemas operacionais com as características do sistema originalmente criado por Ken Thompson, em 1969, e mantido pela empresa estadunidense AT&T.

1.1 - O que é um sistema operacional

Um sistema operacional (SO) é um conjunto de programas que gerencia os recursos de hardware e fornece serviços essenciais para a criação e execução de aplicativos em um computador. No caso específico do GNU/Linux, o sistema operacional é formado pela combinação do kernel Linux, lançado em 1991, por Linus Torvalds, e desenvolvido por uma grande comunidade de colaboradores, com as ferramentas e utilitários do Projeto GNU (de GNU's Not Unix), criado e liderado, desde 1983, pelo fundador do movimento do Software Livre, Richard Stallman. Resumidamente, estes são os componentes básicos de um sistema operacional da linhagem do UNIX:

  • Kernel (núcleo do sistema);
  • Biblioteca C padrão (libc);
  • Shell (interpretador de comandos e interface com o usuário);
  • Utilitários diversos da base do sistema.

O sistema operacional UNIX surgiu com inovações que influenciam, até hoje, a computação como um todo, tendo introduzindo conceitos como portabilidade, simplicidade e modularidade, além de poderosos utilitários para a linha de comandos e, desde 1972, uma linguagem de programação que praticamente se universalizou como a principal linguagem para desenvolvimento de sistemas: a linguagem C, criada por Dennis Ritchie. Por essas e outras, o UNIX tornou-se a plataforma dominante nas empresas, universidades e nos centros de pesquisa do mundo todo.

O sistema operacional GNU

O UNIX original só tinha um problema: era um software proprietário – quer dizer: um software cujo uso, distribuição e acesso ao código-fonte é controlado e restrito pelo detentor dos direitos de cópia. Isso limita, de forma injusta e desproporcional, os direitos do usuário final.

Para contrapor-se aos controles abusivamente restritivos do licenciamento proprietário, em 1983, Richard Stallman anunciou sua intenção de criar um sistema operacional que garantisse ao usuário final as liberdades de executá-lo, copiá-lo, distribuí-lo, estudá-lo e modificá-lo sem qualquer restrição, exceto pela restrição de alterar o seu modelo de licenciamento para algo proprietário.

Aqui está um trecho do anúncio original do Projeto GNU, postado por Richard Stallman em 27 de Setembro de 1983:

[…] vou escrever um software completo compatível com Unix chamado GNU (sigla para Gnu Não é Unix), e o compartilharei livremente com qualquer um que possa usá-lo. […] O GNU rodará programas UNIX, mas não será idêntico ao UNIX. Nós faremos todas as melhorias que são necessárias, baseados em nossa experiência com outros sistemas operacionais.

A adoção do kernel Linux

Do anúncio, até 1991, o sistema GNU já contava com todos os utilitários da base do sistema, um shell, um compilador e uma biblioteca C padrão e já poderia ser considerado um sistema operacional completo, exceto pelo seu kernel. Na década anterior, enquanto o projeto trabalhava em seu próprio núcleo: o kernel Hurd, o desenvolvimento do restante do sistema foi feito utilizando kernels BSD (o principal kernel da família UNIX) e Mach (desenvolvido pela universidade Carnegie Mellon). Em 1991, porém, Linus Torvalds anunciou, numa lista da Usenet (um agregador de vários fóruns da época), seu trabalho em um kernel – que, mais tarde, viria a ser conhecido como Linux.

Eu estou fazendo um sistema operacional (livre) (apenas como passatempo, não vai ser grande e profissional como o GNU) para clones AT 386(486). Ele tem amadurecido desde Abril e está começando a ficar pronto.

O Linux cativou muitos desenvolvedores e usuários que o adaptaram como kernel para diversos sistemas operacionais: em especial, para o Projeto GNU, resultando no que hoje conhecemos como GNU/Linux.

Componentes do sistema operacional GNU/Linux

Na aparência e nos recursos gerais, o GNU/Linux pouco difere de um sistema UNIX, mas, como pretendido por Stallman, traz também algumas diferenças, principalmente quanto à flexibilidade da implementação das especificações de um UNIX (POSIX e SUS) e ao licenciamento.

É por isso que nós dizemos que o GNU/Linux é um sistema UNIX like (como o UNIX) e não da família UNIX.

Sobre os componentes, eles são basicamente os mesmos de um sistema UNIX:

Kernel Linux
Núcleo do sistema operacional, responsável por gerenciar os recursos de hardware, como CPU, memória, dispositivos de entrada e saída e sistema de arquivos. O kernel fornece uma interface entre o hardware e os aplicativos, permitindo que os programas acessem os recursos do sistema de forma organizada e padronizada.
Bibliotecas compartilhadas e módulos do kernel
O GNU/Linux inclui a biblioteca C padrão (glibc), desenvolvida pelo projeto GNU como a interface de acesso aos recursos do kernel (chamadas de sistema), bibliotecas compartilhadas, que fornecem funcionalidades comuns para os aplicativos, e módulos do kernel que podem ser carregados dinamicamente para adicionar suporte a dispositivos e funcionalidades específicas.
Utilitários e ferramentas da base GNU
O Projeto GNU também desenvolveu uma ampla variedade de ferramentas e utilitários essenciais para sistemas UNIX like, como o GNU Core Utilities (ls, cp, mv, etc), o GNU Compiler Collection (GCC) entre tantos outros. Essas ferramentas fornecem todos os serviços básicos necessários para interagir com o sistema operacional e desenvolver aplicativos.
Shell
É o interpretador de comandos e a interface padrão para a operação do sistema. O shell do Projeto GNU é o Bash (Bourne Again Shell).

1.2 - O que é o shell

De forma sucinta, o shell de sistemas UNIX like pode ser entendido como um interpretador de comandos programável e uma interface com o sistema, mas, acima de tudo, o shell é a interface padrão para a operação do sistema.

É através dessa interface que nós escrevemos comandos em texto para expressar o que queremos que o sistema execute.

Além disso, o shell acumula outras atribuições e funcionalidades importantes no GNU/Linux, como:

Manipulação de processos
O shell possibilita o início, a interrupção, a pausa e o gerenciamento dos processos em execução no sistema. Isso inclui o controle de processos em primeiro e segundo planos, o redirecionamento de fluxos de entrada e saída de dados, a passagem de parâmetros, a exportação de variáveis e a manipulação de sinais.
Automação de tarefas
É possível automatizar tarefas repetitivas por meio de scripts: sequências de comandos salvas em um arquivo de texto. Os scripts podem conter uma variedade de comandos do shell, incluindo comandos de manipulação de arquivos, estruturas de controle de fluxo (como loops e estruturas condicionais), invocações de programas externos, definições de variáveis e outras construções do shell.
Manipulação de arquivos e diretórios
O shell possibilita a navegação pelo sistema de arquivos, além da criação, remoção, copia, e alteração de nomes e permissões de arquivos e diretórios, operações que também podem ser realizadas em massa, graças à sua linguagem de programação nativa aliada a expressões regulares e caracteres curinga.
Personalização e configuração
Os usuários podem customizar o comportamento e a operação do sistema, via linhas de comandos, definindo e exportando variáveis, criando apelidos para linhas de comandos e funções personalizadas. Isso permite adaptar o ambiente de linha de comandos às necessidades individuais de cada pessoa utilizadora.

O shell também é um programa

É importante ter em mente que, embora seja um componente tão essencial do sistema, o shell é, no fim das contas, um programa como todos os outros que utilizamos no dia a dia – tanto que podemos ter diversos programas diferentes para funcionar como shell instalados nos nossos sistemas.

Os primeiros shells foram criados nos anos 1960 como interfaces para sistemas operacionais como o MULTICS (1965) e, posteriormente, para o UNIX (1969).

Ao longo da história do UNIX e do GNU, vários programas foram criados para assumir o papel do shell, entre eles:

Shell Thompson
(sh) O shell originalmente criado por Ken Thompson, para o UNIX.
Bourne Shell
(sh) Escrito por Stephen Bourne, foi adotado como sucessor do Shell Thompson na versão 7 do UNIX.
Bourne Again Shell
(bash) Escrito originalmente por Brian Fox e atualmente mantido por Chet Ramey, o Bash foi lançado em 1989 como o shell padrão do sistema operacional GNU: papel que ocupa até hoje no GNU/Linux.
Almquist Shell
(ash) Escrito por Kenneth Almquist com o propósito de ser um shell leve, clone do Bourne Shell utilizado na versão do UNIX conhecida como System V.4. Foi adotado como o shell padrão do BSD no início dos anos 1990.
Debian Almquist Shell
(dash) Em 1997, Herbert Xu portou o ash para a distribuição Debian GNU/Linux, mas o porte só foi nomeado como dash em 2002.

Entre parêntesis, acima, nós temos os nomes dos arquivos binários executáveis de cada um desses shells.

Os shells padrão

Por história e convenção, todos os programas que executam comandos do shell sem a atuação direta do usuário invocam um arquivo binário executável de nome sh no diretório /bin (caminho /bin/sh). Em sistemas da família UNIX, /bin/sh costuma ser, de fato, um arquivo (geralmente, uma implementação do ash ou do dash nomeada como sh). Nas distribuições GNU/Linux, porém, o mais comum é que /bin/sh seja uma ligação simbólica para o executável do shell que efetivamente assumirá esse papel.

Ligações simbólicas, links simbólicos, ou ainda symlinks, são um tipo de arquivo que aponta para outros arquivos ou diretórios no sistema – o que, na prática, funciona como um segundo nome para esses arquivos.

No Debian, por exemplo, essa ligação é feita com o shell dash, mas não é incomum encontrarmos o Bash ligado ao /bin/sh, o que faz com que ele se comporte de forma mais estritamente compatível com as especificações de um shell UNIX, tal como definido nas normas POSIX.

Descobrindo quem é o binário executável do shell sh no Debian:

:~$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 jun 21  2023 /bin/sh -> dash

Quando utilizado dessa forma, ou seja, invocado por outros programas, não há como o usuário interagir com o shell: é por isso que o shell identificado por /bin/sh também é chamado de shell não interativo padrão ou shell não interativo POSIX, numa referência a um dos dois modos que todo shell pode receber os comandos:

Modo interativo
Nós digitamos um comando no terminal, teclamos <Enter>, aguardamos o fim de seu processamento e decidimos o que fazer em seguida.
Modo não interativo
Os comandos são previamente escritos em um arquivo ou são passados para o executável do shell como argumentos ou fluxos de texto.

Um shell sempre executará os comandos escritos nos nossos scripts no modo não interativo, mas ele não será, necessariamente, o shell padrão em /bin/sh!

A escolha do shell que será utilizado por padrão no modo interativo pode ser mais pessoal: cada um escolhe o que lhe parecer mais confortável. Porém, em sistemas GNU/Linux padrão, o shell inicialmente definido para uso interativo é o Bash.

Se não for o Bash, o que pode acontecer, então não será um sistema GNU/Linux padrão.

Descobrindo o shell interativo definido para a minha conta de usuário:

:~$ awk -F: '/^blau/{print $7}' /etc/passwd
/bin/bash

Conhecer os shells padrão, interativos ou não, é importante para que possamos antecipar com quais shells poderemos contar em instalações do GNU/Linux nas máquinas de outras pessoas ou em servidores remotos.

Listando os shells disponíveis no sistema:

:~$ cat /etc/shells

1.3 - O que são scripts

Superficialmente, scripts são arquivos de texto que contêm uma ou mais linhas de comandos que antecipam o que shell terá que interpretar. Só a capacidade de executar sequências de comandos previamente escritos em um arquivo, já seria suficiente para abrir um mundo de possibilidades! Mas o shell possibilita soluções ainda mais completas e eficientes, pois conta, nativamente, com todos os elementos presentes na maioria das linguagem de programação modernas de alto nível!

A linguagem do shell pode ser classificada como:

Interpretada
O código é executado por um interpretador ainda na sua forma de texto, em vez de ser transformado em um arquivo binário e transferido para a memória quando invocado.
Imperativa
Nós expressamos o que queremos que o sistema faça através de comandos (shell, faça isso, faça aquilo, etc…).
Procedural
O código do programa expressa um ou mais procedimentos.
Estruturada
O fluxo de execução do código é controlado por sub-rotinas e estruturas de decisão e repetição.
De alto nível
O código é escrito com elementos e símbolos semelhantes aos de uma linguagem humana.

O shell e a filosofia UNIX

Não há limites para o que pode ser feito com scripts em shell, mas isso não significa que tudo será possível contando apenas com suas funcionalidades internas. Embora disponha de ferramentas para solucionar muitos problemas com seus comandos internos (também chamados de builtins), os recursos de programação do shell foram pensados para que nós pudéssemos programar e controlar a execução de comandos – e os nossos comandos, com frequência, envolverão a execução de outros programas.

A linguagem do shell existe para que possamos determinar como, quando, sob quais condições e com que dados os comandos deverão ser executados.

Além disso, existe outro aspecto fundamental da operação do shell: o fato dele poder receber comandos, tanto como argumentos, quanto pela leitura de fluxos de texto. Neste aspecto, o shell funciona como qualquer outro programa que implemente a chamada “interface de linha de comando” (CLI, de command line interface).

Observe, nos comandos abaixo…

O Bash recebendo comandos como argumentos:

:~$ bash -c 'echo Olá, mundo!'
Olá, mundo!

O Bash recebendo comandos como fluxos de texto (redirecionamento “here string”):

:~ $ bash <<< 'echo Olá, mundo!'
Olá, mundo!

Essas possibilidades têm suas origens na própria filosofia desenvolvimento de utilitários para o sistema UNIX e estão fortemente ligadas ao enunciado mais conhecido da chamada filosofia UNIX – aquele atribuído a Doug McIlroy:

Escreva programas que façam apenas uma coisa, mas que a façam bem-feita; escreva programas que trabalhem juntos; escreva programas que manipulem fluxos de texto, pois esta é uma interface universal.

O shell como plataforma

Vale a pena destacar dois aspectos muito importantes dessa forma de enunciar a filosofia UNIX: primeiro, veja que os dois primeiros preceitos sugerem uma abordagem modular para a solução de problemas – ou seja, um programa não precisa, necessariamente, ser capaz de resolver tudo sozinho, pois parte do processamento que levará à solução pode ser (e sugere-se que seja) delegado a outros programas mais especializados. O outro aspecto, está no fato do terceiro preceito estabelecer claramente a forma pela qual esses programas deverão trabalhar juntos: trocando dados entre si na forma de fluxos de texto, ou seja, cadeias de caracteres.

Todo o ecossistema UNIX (e GNU, portanto), foi desenvolvido para funcionar segundo esses princípios, o que atribui ao shell mais uma definição: além de um interpretador de comandos, uma interface com o usuário, uma interface com o sistema e uma linguagem de programação, o shell também é uma plataforma!

Em computação, uma plataforma é qualquer ambiente projetado para funcionar e ser ampliado de forma consistente com um conjunto de especificações características de suas partes. Num ambiente mediado pelo shell, são a modularidade e os fluxos de texto da interface CLI que determinam a consistência da plataforma.

1.4 - O que significa “imprimir”

Embora a operação no modo interativo nos induza a pensar que estamos atuando diretamente sobre um shell em execução, a verdade é que nós só interagimos com o shell através de terminais – e nele que nós digitamos os comandos e as mensagens que recebemos como respostas são exibidas. Essa confusão acontece porque, nos dias de hoje, terminais são programas exibidos em monitores de vídeo e que em pouca coisa diferem de, por exemplo, editores de texto, mas nem sempre foi assim!

Os primeiros terminais eram máquinas eletromecânicas compostas por um teclado e uma impressora: as chamadas teleimpressoras (ou teletipos). Utilizadas, originalmente, no serviço de telegrafia, essas máquinas foram modificadas, no fim dos anos 1950, para uso experimental como interfaces de computadores. Foi somente no começo dos anos 1960 que a empresa Teletype Corporation produziu as primeiras teleimpressoras dedicadas ao uso na computação, entre elas, a emblemática Teletype Model 33 ASR.

tty-01.png
Figure 1: Teletype Model 33 ASR

Fluxos de texto como interface universal

Esse pequeno vislumbre da história é importante para entendermos a origem de alguns termos e conceitos fundamentais para o domínio do shell: afinal, tudo no sistema UNIX foi desenvolvido numa realidade onde os terminais eram equipamentos fisicamente separados dos computadores e com dados fluindo entre eles através de cabos!

O que transitava pelos cabos eram sinais digitais – pulsos elétricos com apenas dois níveis de voltagem representando zeros e uns. A cada certa quantidade desses zeros e uns (também chamados de bits): geralmente, uma sequência de 8 deles (ou bytes), tanto o terminal quanto o computador eram projetados para interpretarem que haviam enviado, ou recebido, um caractere.

Os caracteres, por sua vez, poderiam ser utilizados para compor mensagens de texto, comandos (que também são textos), disparar alguma função do terminal ou sinalizar algum evento para o software executado no computador. Olhando deste modo, fica mais fácil entender o que Doug McIlroy quis dizer com o seu terceiro preceito da filosofia UNIX:

Escreva programas que manipulem fluxos de texto, pois esta é uma interface universal.

Ainda em 1963, a American National Standards Institute (ANSI) lançou uma norma padronizando a troca de dados entre computadores e dispositivos numa tabela que especificava 128 caracteres (o máximo de inteiros positivos que podemos escrever com 7 bits) e seus códigos: a tabela ASCII, utilizada até hoje.

Obtendo informações sobre a tabela ASCII:

:~$ man ascii

Saber disso torna evidente que, diferente dos nossos dias, terminais e fluxos de texto não tinham nada de abstrato na computação entre o fim dos anos 1960, quando UNIX nasceu, até meados dos anos 1980. Também revela que não havia nada de metafórico em certas expressões corriqueiras da época, como “digitar comandos no terminal”, pois o teclado era parte do terminal e as pessoas tinham que ir até ele para digitar comandos; ou ainda “imprimir no terminal”, já que todos os textos produzidos na execução de comandos eram literalmente impressos nas folhas de formulário contínuo dos terminais. Do mesmo modo, não era possível confundir o terminal com o shell: o terminal era, claramente, o equipamento onde comandos eram digitados e suas saídas impressas, e o shell era o programa em execução no computador que mantinha uma troca constante de fluxos de dados com ele.

Os fluxos de dados padrão

No contexto de como o sistema operacional disponibiliza o acesso aos fluxos de dados para o shell, é preciso entender que uma das funções do kernel é abstrair o hardware. Isso significa que o terminal, bem como as vias pelas quais os dados fluem entre ele e o computador, sendo tudo hardware, também serão abstraídos de algum modo como software.

No UNIX, todas as abstrações de hardware são implementadas na forma de arquivos, daí dizermos que: no UNIX, tudo é arquivo.

Em sistemas UNIX like, existem sete tipos básicos de arquivos:

  • Arquivos comuns;
  • Diretórios (sim, diretórios também são arquivos);
  • Ligações simbólicas (links);
  • Dispositivos caractere;
  • Dispositivos bloco;
  • Pipes (arquivos FIFO, de “first in, first out”);
  • Sockets.

No Linux, tanto os terminais quanto os fluxos de dados são abstraídos como arquivos do tipo dispositivo caractere, que são arquivos especiais usados para comunicação de baixo nível com dispositivos de entrada e saída, como discos, impressoras, teclado e mouse, por exemplo. Deste modo, os programas podem acessar esses dispositivos como se eles fossem arquivos comuns, permitindo a leitura e a escrita de dados de forma direta.

Os dispositivos caractere, emulam uma forma de comunicação onde os dados são transferidos caractere a caractere: ou seja, byte a byte, sem nenhuma acumulação intermediária.

Os arquivos de dispositivos são encontrados no sistema de arquivos do Linux como entradas no diretório /dev (de device). Deste modo, o arquivo /dev/sda representa um disco rígido, /dev/usb/lp0 representa uma impressora USB e, já direcionando o assunto ao que nos interessa, /dev/tty representa o nosso bom e velho terminal!

Listando o dispositivo de terminal (TTY):

:~$ ls -l /dev/tty
crw-rw-rw- 1 root tty 5, 0 mai 16 09:10 /dev/tty

O nome TTY, utilizado até hoje como sinônimo de console de terminal, veio da abreviação da marca TeleTYpe.

No exemplo acima, observe que, no início da primeira coluna da saída da listagem, nós encontramos a letra c, de caractere, indicando, justamente, que se trata de um arquivo dispositivo caractere.

De forma semelhante, as vias de comunicação com o terminal, os fluxos de dados padrão, também precisam ser disponibilizadas para os programas na forma de arquivos. Mas isso traria um problema, visto que, como o sistema é multitarefas, nós sempre teremos vários programas em execução ao mesmo tempo. Então, como o sistema organiza e isola o acesso ao terminal, de modo a evitar que os fluxos de dados de um programa interfiram com os dos demais?

Parte da resposta está no fato de que, apesar de vários programas estarem em execução ao mesmo tempo, um mesmo terminal será utilizado por apenas um deles por vez: enquanto um programa usa o terminal, os outros estarão em um estado de espera ou serão executados em segundo plano, o que chamamos de execução assíncrona ou jobs (trabalhos, em português).

O shell é um bom exemplo de programa que entra em estado de espera enquanto o programa invocado por um comando é executado.

Para encontrarmos a outra parte da resposta, nós teremos que investigar como o sistema implementa os dispositivos relacionados com os três fluxos de dados padrão: stdin, stdout e stderr.

Seguindo o fluxo

No GNU/Linux, nós encontramos os fluxos de dados padrão como arquivos no diretório dos dispositivos – o diretório /dev:

:~$ ls -l /dev/std*
lrwxrwxrwx 1 root root 15 mai 16 09:10 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 mai 16 09:10 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 mai 16 09:10 /dev/stdout -> /proc/self/fd/1

Aqui, nós temos várias informações importantes, mas vamos por partes…

No exemplo, eu usei o programa ls, com a opção -l, para listar os todos os arquivos no diretório /dev com nomes iniciados com std (de standard). Essa minha escolha, deveu-se a algo que eu já sabia: os nomes dos dispositivos dos fluxos de dados padrão…

Entrada padrão (/dev/stdin)
Liga o teclado do terminal aos programas.
Saída padrão (/dev/stdout)
Liga os programas à impressora do terminal.
Saída padrão de erros (/dev/stderr)
Liga os programas à impressora do terminal para a impressão de mensagens de erro.

Contudo, na listagem exibida, repare que, no começo da primeira coluna, nós temos uma letra l (um éle minúsculo), não o c, de dispositivo caractere – e isso tem a ver com parte da solução para o problema de isolar o acesso ao terminal, já que a letra l (éle) indica que os três arquivos listados são ligações simbólicas, ou seja, arquivos que apontam para outros arquivos e diretórios, a saber:

/dev/stderr -> /proc/self/fd/2
/dev/stdin -> /proc/self/fd/0
/dev/stdout -> /proc/self/fd/1

As setas (->) são a forma utilizada pelo utilitário ls para representar o arquivo destino de uma ligação simbólica.

Como podemos ver, os três fluxos de dados padrão apontam para arquivos diferentes em um mesmo diretório: o diretório /proc/self/fd – e aqui está a segunda parte da solução do problema do acesso aos fluxos de dados!

O processinho vai chegar

Como parte da função de gerenciar o acesso aos recursos do hardware, o kernel também gerencia a execução dos programas por meio de estruturas de dados na memória: os processos. Na segunda aula, nós entraremos mais no conceito de processos. Por enquanto, porém, isso é tudo que precisamos saber sobre eles:

  • Processos são identificados por números (PID);
  • Os recursos atribuídos aos processos, bem como informações sobre seus estados de execução, são representados como arquivos em diretórios virtuais que recebem nomes iguais aos de seus números de PID;
  • Todos os diretórios dos processos são montados no diretório chamado /proc.

Portanto, é possível inferir que as ligações simbólicas dos três fluxos de dados, na listagem, apontam para o diretório de um processo – mas de qual processo?

A dúvida procede porque, onde deveria haver um número de PID, a listagem nos mostra o nome self, mas o mistério não é tão misterioso assim…

O nome self é um link mágico, um recurso implementado pelo kernel para que, quando não for possível determinar o PID de um processo, o acesso ao diretório /proc/self gere o caminho do próprio processo que o estiver acessando.

Para simplificar, imagine que um programa queira ler os dados que estiverem na entrada padrão acessando /dev/stdin. Sendo uma ligação simbólica para /proc/self/fd/0, self será substituído pelo PID desse processo. Nós faremos isso no próximo exemplo, pois precisamos saber onde vão parar as ligações simbólicas com os três fluxos de dados padrão acessados pelo ls. Antes, porém, temos que entender o que é o diretório fd e o que são esses arquivos 0, 1 e 2, exibidos na listagem.

Descritores de arquivos

:~$ ls -l /proc/self/fd
total 0
lr-x------ 1 blau blau 64 mai 16 11:42 3 -> /proc/10089/fd
lrwx------ 1 blau blau 64 mai 16 11:42 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 16 11:42 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 16 11:42 2 -> /dev/pts/0

O nome do diretório, fd, vem de file descriptor (“descritor de arquivos”, em português). Nele, nós encontramos várias ligações simbólicas: os descritores de arquivos propriamente ditos, cada uma delas apontando para um arquivo, ao qual o processo terá acesso concedido para leitura e/ou escrita.

Então, é assim que o kernel resolve o problema do controle e isolamento do acesso aos fluxos de dados do terminal: designando descritores de arquivos para cada processo! Além disso, para garantir que todos os processos tenham acesso a um terminal, eles sempre serão iniciados com os descritores de arquivos 0 (entrada padrão), 1 (saída padrão) e 2 (saída padrão de erros) nos seus respectivos diretórios /proc/PID/fd.

Neste segundo exemplo, veja que os descritores de arquivos 0, 1 e 2 apontam para um mesmo arquivo: o dispositivo em /dev/pts/0. Antes de descobrirmos quem é esse dispositivo, porém, observe que a terceira e a quarta colunas da listagem mudaram: na primeira listagem, elas indicavam o nome de usuário root (coluna 3) e o grupo root (coluna 4); na segunda, elas mostram o meu nome de usuário e o meu grupo.

Deixando o assunto sobre grupos para outra oportunidade (ou para a sua pesquisa), vendo que eles têm os mesmos nomes dos usuários, é justo deduzir que as duas colunas indicam algo relativo à propriedade dos arquivos, ou seja, a quem eles pertencem. Isso tem relevância porque, sabendo que root é o nome do usuário administrativo, o todo-poderoso do sistema, e blau é o nome de um usuário comum, nós acabamos de constatar que o acesso aos descritores de arquivos, além de ser concedido ao processo, é dado, também, ao usuário que iniciou o processo (quando executou o programa).

Agora, para entendermos o que é o dispositivo /dev/pts/0, nós teremos que revisitar a história dos terminais para investigarmos, especialmente, onde as teleimpressoras foram parar, já que elas, obviamente, não são mais utilizadas.

1.5 - O que é um terminal hoje em dia

Os sistemas operacionais sempre tiveram módulos do kernel especializados em controlar e disponibilizar os terminais para usuários e programas. Contudo, foi somente com o advento de hardwares mais compactos, como os computadores pessoais do início dos anos 1980, que trabalham com monitores de vídeo e teclados de uso genérico, que os terminais “físicos” começaram a ser trocados por emuladores de terminal em software.

terminais-04-pb.png
Figure 2: Terminal virtual (/dev/tty) designado para um usuário.

No Linux, o dispositivo de terminal é o arquivo /dev/tty, mas nós não temos acesso direto a ele. Sem o início automático de um ambiente gráfico, quando o sistema termina de ser carregado, um prompt será exibido para a digitação do nome de usuário. Em seguida, um programa de login será executado para solicitar uma senha. Se as credenciais informadas forem válidas, o sistema disponibilizará o acesso ao primeiro TTY virtual, /dev/tty1, e um processo do shell interativo será iniciado como um shell de login. Daí para frente, novos TTY virtuais só serão disponibilizados mediante novos logins de usuário, o que pode ser feito com as combinações de teclas entre Ctrl+Alt+F1 e Ctrl+Alt+F6 (o limite de terminais virtuais pode variar com as configurações do sistema).

Por outro lado, se houver um ambiente gráfico instalado e configurado para ser iniciado automaticamente, o sistema, ao terminar sua carga, apresentará a tela de um gerenciador gráfico de display (ou DM, de display manager), responsável por autenticar as credenciais do usuário e iniciar um ambiente de desktop.

Estando no ambiente gráfico, nós também podemos iniciar terminais virtuais com os atalhos entre Ctrl+Alt+F1 e Ctrl+Alt+F6 e retornar ao desktop com o atalho Ctrl+Alt+F7, mas isso pode variar de acordo com o DM instalado.

Pseudoterminais

Como vimos, o acesso aos terminais TTY virtuais é estritamente dependente de um login, ou seja, os programas estão limitados a utilizar o TTY virtual designado para o usuário que fez o login no sistema – o que poderia ser um problema, especialmente no caso de acesso a sistemas remotos. Por isso, além dos terminais emulados (TTY), o kernel também implementa métodos para que os programas criem novos terminais: os chamados pseudo TTY, PTY ou simplesmente “pseudoterminais”.

pseudoterminais.png
Figure 3: Diagrama simplificado de um pseudoterminal (PTY)

No Linux, quando um processo requer o uso de um pseudoterminal, ele acessa o dispositivo /dev/ptmx (multiplexador de pseudoterminais), recebendo um descritor de arquivo ligado a um dispositivo pseudoterminal principal (PTM) e um novo dispositivo pseudoterminal secundário (PTS) é criado e conectado a um novo processo do shell através dos três descritores de arquivos padrão.

Exibindo o dispositivo pseudoterminal ligado à entrada padrão do shell:

:~$ tty
/dev/pts/0

Por conta de mecanismos que veremos mais adiante, o processo do terminal gráfico é pai do processo do shell iniciado a partir dele. Portanto, é possível obter seu PID expandindo a variável PPID (de “parent PID”) e descobrir qual é o descritor de arquivo que ele está usando para ligar-se ao pseudoterminal principal (PTM) – que foi obtido através do acesso ao dispositivo /dev/ptmx.

Expansão é um mecanismo do shell que “troca” certas sintaxes na linha do comando por outras palavras antes do comando ser executado.

Listando a ligação do processo de um terminal gráfico com o PTM:

:~$ ls -l /proc/$PPID/fd | grep ptmx
lrwx------ 1 blau blau 64 mai 17 09:14 11 -> /dev/ptmx

O utilitário grep filtra linhas de um fluxo de texto, exibindo apenas aquelas que casem com a ocorrência de um padrão descrito na forma de uma expressão regular (ou REGEX), assunto do Módulo 2.

Também podemos listar as ligações do processo do shell, iniciado pelo terminal gráfico, com o dispositivo PTS. Para obtermos o número do processo deste shell, nós podemos expandir o parâmetro especial $ (cifrão).

Listando a ligação do processo de um shell com o PTS (/dev/pts/?):

:~$ ls -l /proc/$$/fd | grep pts
lrwx------ 1 blau blau 64 mai 17 09:55 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 17 09:55 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 17 09:55 2 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 17 09:55 255 -> /dev/pts/0

O nome do parâmetro especial é $, o outro cifrão, que aparece antes dele, é o símbolo utilizado pelo shell para indicar que uma expansão deve ser processada (como vimos na expansão da variável PPID).

Não por coincidência, nós encontramos os mesmos dispositivos de terminal vistos quando seguimos as ligações dos fluxos de dados padrão: /dev/pts/0:

:~$ ls -l /proc/self/fd
total 0
lr-x------ 1 blau blau 64 mai 16 11:42 3 -> /proc/10089/fd
lrwx------ 1 blau blau 64 mai 16 11:42 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 16 11:42 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 mai 16 11:42 2 -> /dev/pts/0

Isso aconteceu porque, desde antes, estávamos utilizando o mesmo processo do shell e o mesmo terminal do meu ambiente gráfico. Além disso, todos os processos iniciados pelo shell herdarão suas ligações com o terminal. Portanto, enquanto estava sendo executado, o ls recebeu acesso ao terminal /dev/pts/0 e o shell entrou em espera aguardando seu término.

A relação entre o shell e o terminal

Agora, nós já temos todos os elementos necessários para compreender o que eu disse alguns tópicos atrás:

Embora a operação no modo interativo nos induza a pensar que estamos atuando diretamente sobre um shell em execução, a verdade é que nós só interagimos com o shell através de terminais…

Veja como isso acontece:

  • O processo do shell é iniciado e ligado a um terminal através dos descritores de arquivos padrão.
  • O shell monitora a entrada padrão à espera de um comando.
  • Quando o usuário entra com um comando, o terminal envia os caracteres para a entrada padrão do shell.
  • O shell interpreta a cadeia de caracteres e inicia a execução do comando.
  • Se o comando envolver a execução de um programa, um novo processo será iniciado com acesso ao mesmo terminal a que está ligado o shell.
  • Havendo mensagens a serem exibidas, elas serão enviadas, via saída padrão ou saída padrão de erros, para o terminal.
  • Quando a execução do comando termina, o shell volta a monitorar a entrada padrão esperando novos comandos.

A não ser que sejam passados como um argumento (opção -c do Bash, por exemplo), os comandos sempre serão lidos pela entrada padrão do processo do shell, inclusive os dos nossos scripts.

1.6 - A anatomia da linha de um comando

Um comando é a expressão daquilo que nós queremos que o sistema faça, mas o shell foi projetado para receber e interpretar cadeias de caracteres segundo uma série de convenções de sintaxe. As formas mais simples de comandos são compostas pela combinação de um ou mais dos elementos abaixo:

[EXPORTAÇÕES] [INVOCAÇÃO [ARGUMENTOS]] [REDIRECIONAMENTOS]

Onde:

Exportações
são definições de variáveis que estarão disponíveis apenas para o processo daquilo que estiver sendo invocado.
Invocação
pode ser uma função (um conjunto de comandos definidos na memória e identificados por um nome), um comando interno do shell ou qualquer programa disponível no sistema (também chamado, popularmente, de “comando externo”).
Argumentos
lista de palavras que representam os dados adicionais e opções que fazem parte da sintaxe daquilo que estiver sendo invocado.
Redirecionamentos
mecanismo do shell que estabelece ligações entre os fluxos de dados atribuídos ao processo do que vier a ser executado e um arquivo.

O shell classifica as linhas de comandos formadas por estes elementos como comandos simples. Eventualmente, podemos encadear dois ou mais comando simples para formar listas de comandos e pipes.

Metacaracteres do shell

Para delimitar as palavras da linha de um comando, o shell precisa designar um significado especial para alguns caracteres: os chamados metacaracteres do shell, que são o espaço, a tabulação, a quebra de linha e os caracteres ;, &, |, <, >, ( e ).

Todos os metacaracteres separam palavras, mas os caracteres ;, &, |, <, >, ( e ) também serão usados para formar operadores do shell.

Citações

Não raro, porém, nós teremos que escrever comandos com palavras contendo metacaracteres e outros símbolos com significado especial (tokens).

Por exemplo:

:~$ grep Rio de Janeiro cadastro.csv
grep: de: Arquivo ou diretório inexistente
grep: Janeiro: Arquivo ou diretório inexistente

Nesta linha de comando, Rio de Janeiro deveria ter sido passado como um único argumento, mas, devido aos espaços, foram passados três, fazendo com que o grep interpretasse de e Janeiro como nomes de arquivos. Então, para remover o significado especial dos espaços, eles precisarão ser citados.

O termo original em inglês é quote (citar), mas algumas traduções nos levam a imaginar, equivocadamente, que se trata apenas de “escrever entre aspas”.

As citações podem ser feitas de várias formas, sendo as três principais:

Barra invertida (\)
Cita apenas o caractere seguinte.
Aspas simples ('...')
Cita, sem exceção, toda a cadeia de caracteres entre elas.
Aspas duplas ("...")
cita toda a cadeia de caracteres entre elas, exceto o cifrão ($), o acento grave (`) e, sob certas condições, a contra-barra (\) e a exclamação (!).

Portanto, a linha de comando do exemplo poderia ser escrita de qualquer uma das três formas:

grep Rio\ de\ Janeiro cadastro.csv  # Citando apenas os espaços
grep 'Rio de Janeiro' cadastro.csv  # Citando com aspas simples
grep "Rio de Janeiro" cadastro.csv  # Citando com aspas duplas

Além dessas citações, o Bash também processa um tipo especial de expansão que resulta numa citação com aspas simples: a citação de caracteres ANSI-C.

Expandindo a citação de caracteres ANSI-C:

:~$ echo $'banana\nlaranja\nabacate'
banana
laranja
abacate

Aqui, a notação ANSI-C do caractere de quebra de linha (\n) foi trocada pelo seu valor ASCII real (caractere de valor decimal 10) e todo o conjunto resultante foi citado como se estivesse entre aspas simples, causando a sua impressão com quebras de linhas.

Outros caracteres especiais

Embora não sejam metacaracteres, dependendo do contexto, alguns caracteres também poderão ter significados especiais para o shell. Por exemplo, sozinho, o cifrão ($) é um caractere comum, mas, se for o primeiro caractere de certas palavras, ele indicará para o shell que alguns tipos de expansão deverão ser processados. O mesmo pode ser dito do caractere igual (=): não tem significado especial, mas também pode aparecer compondo atribuições de variáveis.

Palavras reservadas

Algumas palavras podem ser usadas para definir blocos de comandos ou iniciar estruturas de controle e de avaliação de expressões, formando os chamados comandos compostos: são as palavras reservadas do shell.

No Bash, as palavras reservadas são:

if       then    elif    else    fi      case      esac
select   for     in      until   while   do        done
{        }       [[      ]]      !       function  coproc  time

Apesar de serem metacaracteres, os parêntesis, quando utilizados para agrupar comandos ou definir expressões, também formarão comandos compostos do Bash.

Atribuições de variáveis

No shell, uma atribuição é uma palavra que contém um nome seguido do sinal de igual (=) e uma cadeia de caracteres quaisquer, inclusive nenhum caractere, para expressar um valor:

nome=[valor]

Onde NOME é uma cadeia de caracteres composta apenas por combinações de letras maiúsculas e minúsculas da tabela ASCII, o caractere sublinhado (_) e números, desde que não sejam o primeiro caractere. O VALOR, por sua vez, é qualquer sequência de zero ou mais caracteres literais quaisquer: ou seja, se a cadeia contiver metacaracteres, eles terão de ser citados.

Além disso, essa construção só será interpretada como uma atribuição se todas as palavras da linha do comando forem atribuições ou se todas as atribuições antecederem uma invocação. A diferença é que, no primeiro caso, as variáveis serão definidas e estarão disponíveis para uso ao longo da sessão corrente do shell, enquanto que, no segundo, elas serão definidas e exportadas para uso exclusivo no processo daquilo que tiver sido invocado.

Definindo variáveis para uso na sessão corrente do shell:

:~$ fruta=banana nome='Luis Carlos'

Exportando variáveis na invocação de um script:

:~$ nome='Helena Lopes' idade=22 exemplo.sh

Na segunda aula, nós estudaremos melhor o que significa a exportação de variáveis.

Expansões

Com o uso das expansões do shell, é possível compor linhas de comandos que só terão uma forma final após serem interpretadas pelo shell, por exemplo:

:~$ comando='ls -la'
:~$ $comando Documentos

Aqui, a variável comando recebeu uma cadeia de caracteres que corresponde à invocação do utilitário ls com o argumento -la. Ao receber a segunda linha, onde nós escrevemos um cifrão ($) antes do nome da variável, o shell saberá que deve trocar a construção $comando pelo valor da variável antes de executar a linha de comando resultante.

Esse tipo de expansão é chamado de expansão de parâmetros, mas o shell conta com várias outras expansões, como veremos ao longo do curso.

Apelidos

Apelidos se parecem muito variáveis, exceto por alguns detalhes:

  • Seus nomes podem receber qualquer caractere imprimível;
  • A atribuição precisa ser feita com o comando interno alias;
  • Apelidos não podem ser exportados;
  • Seus nomes não podem ser expandidos com o cifrão ($);
  • Um apelido só pode ser expandido se for a primeira palavra da linha.

Sempre que o shell recebe a linha de um comando, a primeira coisa que ele faz é verificar se a primeira palavra da linha é um apelido. Se for, o apelido será expandido e o processamento da linha será reiniciado.

Definindo e utilizando um apelido para o comando ls -la:

:~$ alias l='ls --group-directories-first --color=auto -la'
:~$ l Documentos

Como o shell expande o apelido para interpretar essa linha:

:~$ ls --group-directories-first --color=auto -la Documentos

Listando todos os apelidos:

:~$ alias

Listando apenas o apelido l:

:~$ alias l
:~$ alias l='ls --group-directories-first --color=auto -la'

Removendo o apelido l:

:~$ unalias l

Operadores de controle

Como vimos, comandos simples podem ser encadeados através de operadores de controle…

Operador de encadeamento incondicional síncrono (;):

COMANDO1; COMANDO2

COMANDO2 é executado incondicionalmente após a execução de COMANDO1.

Operador de encadeamento incondicional assíncrono (&):

COMANDO1 & COMANDO2

COMANDO1 é executado em segundo plano (sem acesso à saída padrão) e COMANDO2 é imediatamente executado em primeiro plano.

Operador de encadeamento condicional se sucesso (&&):

COMANDO1 && COMANDO2

COMANDO2 só é executado se COMANDO1 terminar com sucesso.

Operador de encadeamento condicional se erro (||):

COMANDO1 || COMANDO2

COMANDO2 só é executado se COMANDO1 terminar com erro.

Operador de encadeamento por pipe (|):

COMANDO1 | COMANDO2

COMANDO1 e COMANDO2 são executados em paralelo e o fluxo de dados na saída de COMANDO1 é passada para a entrada padrão de COMANDO2.

No Bash, também é possível passar os dados na saída padrão de erros para o comando seguinte com o operador |&.

Além desses, ainda temos os três operadores de controle utilizados no comando composto case: ;;, ;& e ;;&, que serão vistos nas próximas aulas.

Operadores de redirecionamento

O redirecionamento é um mecanismo do sistema que substitui o dispositivo de terminal, como origem e destino dos fluxos de dados de um processo, por um arquivo, de modo que os dados de entrada passam a ser lidos de um arquivo e os dados de saída são escritos em um arquivo. Existem muitas operações de redirecionamento possíveis, mas, para que possamos absorver o conceito, é melhor nos concentrarmos em apenas algumas das principais…

Redirecionamento de leitura (<):

INVOCAÇÃO [ARGUMENTOS] < ARQUIVO

As linhas de ARQUIVO são lidas pelo que tiver sido invocado no comando.

Redirecionamento de escrita (>):

INVOCAÇÃO [ARGUMENTOS] > ARQUIVO

O conteúdo de ARQUIVO é truncado (zerado) e os dados na saída padrão do que tiver sido invocado no comando é escrita nele.

Redirecionamento de append (>>):

INVOCAÇÃO [ARGUMENTOS] >> ARQUIVO

O conteúdo de ARQUIVO é mantido e os dados na saída padrão do que tiver sido invocado no comando é adicionada ao seu final.

A sintaxe de todos os redirecionamentos acima é [N]OPERADOR, onde N é o número de um descritor de arquivos. Portanto…

Redirecionamento de escrita da saída de erros (2>):

INVOCAÇÃO [ARGUMENTOS] 2> ARQUIVO

O conteúdo de ARQUIVO é truncado (zerado) e os dados na saída padrão de erros do que tiver sido invocado no comando é escrita nele.

Redirecionamento de append da saída de erros (2>>):

INVOCAÇÃO [ARGUMENTOS] 2>> ARQUIVO

O conteúdo de ARQUIVO é mantido e os dados na saída padrão de erros do que tiver sido invocado no comando é adicionada ao seu final.

O Bash também tem operadores que possibilitam redirecionar ambas as saídas, stdout e stderr, para um arquivo…

Redirecionamento de escrita de stdout e stderr (&>):

INVOCAÇÃO [ARGUMENTOS] &> ARQUIVO

Redirecionamento de append da stdout e stderr (&>>):

INVOCAÇÃO [ARGUMENTOS] &>> ARQUIVO

Isto aqui é um arquivo ('here doc' e 'here string')

#+endsrc

:CUSTOMID: isto-aqui-e-um-arquivo

Em programação, existe um conceito que envolve determinar que um trecho do código deve ser tratado como se fosse um arquivo separado: o chamado here document (ou apenas here doc). Tendo em vista este sentido, o shell implementa o mesmo recurso como um redirecionamento de leitura, apesar de não haver a indicação de nenhum arquivo de fato para ser lido – é como se nós disséssemos para o shell: “isto aqui é um arquivo”.

Lendo várias linhas de texto por “here doc”:

INVOCAÇÃO [ARGUMENTOS] << PALAVRA
Linha de texto 1
Linha de texto 2
PALAVRA

As linhas do texto são lidas pelo que estiver sendo invocado no comando até que PALAVRA seja encontrada sozinha e no início de uma linha.

Se PALAVRA estiver entre aspas (simples ou duplas), as expansões nas linhas do texto não serão processadas.

Para simplificar a leitura de um here doc contendo apenas uma linha de texto, o Bash implementa a chamada /here string/…

Lendo apenas uma linha de texto por “here doc”:

INVOCAÇÃO [ARGUMENTOS] << PALAVRA
Linha de texto
PALAVRA

Lendo apenas uma linha de texto por “here string”:

INVOCAÇÃO [ARGUMENTOS] <<< 'Linha de texto'

1.7 – Comandos internos do shell

Nunca é demais reforçar que um “comando” é toda a linha que escrevemos e enviamos para o shell interpretar e providenciar a execução. Agora, a não ser que a linha só contenha atribuições ou um redirecionamento (especificamente, de escrita, como veremos adiante), os comandos sempre terão a invocação de algo a ser executado.

Portanto, nós poderemos invocar:

  • A execução de um comando interno do shell;
  • A execução de uma função definida pelo usuário;
  • A execução de um programa (popularmente, “comando externo”).

O termo invocação, utilizado aqui e nos tópicos anteriores, não é um conceito, mas algo que nós podemos fazer na linha de um comando!

Neste tópico, nós nos limitaremos a conhecer algumas das funcionalidades internas do shell (em especial, do Bash), que são implementadas na forma de comandos internos (ou builtins).

Listando os comandos internos do Bash:

:~$ compgen -b

O builtin compgen gera listas de palavras que, no modo interativo, o Bash usa para autocompletar a digitação de comandos. Com a opção -b, as palavras geradas correspondem a todos os (atuais) 61 comandos internos do Bash.

Obtendo ajuda sobre comandos internos do Bash:

:~$ help [OPÇÕES] [TÓPICOS]

O manual do Bash (comando man bash) é muito completo, mas o builtin help pode ser mais prático quando já sabemos o tópico que queremos pesquisar.

Listando todos os tópicos de ajuda:

:~$ help

Sem argumentos, o help imprime uma lista de todos os tópicos de ajuda, o que inclui todos os comandos internos, comandos compostos (com exceção do agrupamento com parêntesis) e palavras reservadas do Bash.

Obtendo ajuda detalhada sobre o help:

:~$ help help

Sem OPÇÕES, é exibida a ajuda completa sobre os tópicos indicados (podem ser mais de um). Se o tópico não existir, o help terminará com erro, o que é uma das possíveis formas (a menos precisa de todas) de determinar se um comando é ou não um builtin.

*Obtendo descrição curta dos tópicos compgen e help:

:~$ help -d compgen help
compgen - Display possible completions depending on the options.
help - Display information about builtin commands.

Com a opção -d, é exibida uma descrição curta dos tópicos (infelizmente, ainda não traduzidos, ao menos no meu Debian GNU/Linux).

Obtendo a sintaxe do comando interno help:

:~$ help -s help
help: help [-dms] [PADRÃO ...]

Com a opção -s, o help exibe apenas a sintaxe (sinopse) dos tópicos.

Obtendo ajuda a partir dos primeiros caracteres dos tópicos:

:~$ help -d e
echo - Write arguments to the standard output.
enable - Enable and disable shell builtins.
eval - Execute arguments as a shell command.
exec - Replace the shell with the given command.
exit - Exit the shell.
export - Set export attribute for shell variables.

Os primeiros caracteres dos tópicos podem ser usados para filtrar as ajudas que serão exibidas – o que pode ser muito útil quando não nos lembramos do nome completo de um comando!

Obtendo ajuda a partir de padrões (curingas):

:~$ help -d '*f'
Comandos shell correspondendo à palavra-chave `*f'

if - Execute commands based on conditional.
printf - Formats and prints ARGUMENTS under control of the FORMAT.

Nós podemos descobrir tópicos a partir de padrões de texto descritos com os caracteres curinga: * (zero ou mais caracteres quaisquer), ? (um caractere qualquer) e a lista de caracteres válidos ([LISTA]). No exemplo, eu listei todos os tópicos com nomes terminados com o caractere f.

Listando a descrição de todos os tópicos de ajuda:

:~$ help -d \*

Pode ser útil exibir uma lista completa de tópicos e descrições para descobrir o que poderemos utilizar nos nossos problemas (pelo menos, se soubermos um pouco de inglês).

Observe que foi preciso citar as descrições dos padrões para evitar que o shell expandisse nomes de arquivos!

Comandos internos para imprimir no terminal

Com o que vimos até aqui, e tendo por base o último exemplo, o que estamos procurando para solucionar o nosso primeiro problema é um comando interno que imprima (ou escreva) uma cadeia de caracteres na saída padrão. Como as descrições estão em inglês, nós podemos escolher algumas palavras-chave fáceis de traduzir, como: print (imprimir), write (escrever) e output (saída).

Com um pouco de paciência, até aprendermos a fazer filtragens de linhas de texto no shell, eu encontrei esses dois comandos internos bem promissores:

:~$ help -d \*
...
echo - Write arguments to the standard output.
...
printf - Formats and prints ARGUMENTS under control of the FORMAT.
...

Então, vamos conferir suas ajudas (veja no seu terminal):

:~$ help echo
:~$ help printf

Não sei você, mas eu achei o echo mais simples para o nosso primeiro contato com a criação de scripts. Sendo assim, vamos relembrar o problema:

Escreva um script em shell que imprima a mensagem “Salve, simpatia!” no terminal.

Analisando a ajuda do echo, fica claro que “Salve, simpatia!” é o argumento que queremos passar para ele… Então, vamos testar:

:~$ echo 'Salve, simpatia!'
Salve, simpatia!

Sucesso!

Agora, só precisamos descobrir como criar um script para executar essa linha de comando!

1.8 – Como criar um script

Antes de falarmos do processo de criação de scripts, é preciso enfatizar que não há diferença alguma entre o que fazemos na linha de comandos e o que escrevemos em um script, ou seja: se um problema foi resolvido no modo interativo, a mesma solução poderá ser utilizada no arquivo de um script!

Pode parecer uma afirmação óbvia, mas é muito fácil nos esquecermos de que o shell que interpreta os scripts é o mesmo que interpreta os comandos digitados no terminal!

Seguindo em frente, o processo de criação de scripts geralmente envolve três etapas gerais, sendo duas delas opcionais:

  • Criar/editar o arquivo do script;
  • Escrever a linha do interpretador de comandos (opcional);
  • Tornar o arquivo do script executável (opcional).

Criação e edição do arquivo

A criação do arquivo pode ser feita de várias formas, sendo a mais comum aquela que envolve o uso de um editor de textos “brutos” (raw text, em inglês), como o GNU Nano ou o Vim, no terminal, ou qualquer editor de código que você utilize no ambiente gráfico.

Aqui, nós não nos preocuparemos com a forma escolhida para criar e editar os arquivos dos scripts – utilize o editor da sua preferência, mas os nossos exemplos serão com o editor Vim!

Criando um novo arquivo com o editor Vim:

:~$ vim meu-script.sh

A terminação .sh não significa nada para o shell e só é utilizada nos nossos exemplos para que nós saibamos que o arquivo é um script.

Criado desta forma, o editor Vim será aberto para iniciar a escrita do texto do script meu-script.sh. O mesmo aconteceria se usássemos o editor Nano, que está disponível por padrão na maioria das distribuições GNU/Linux:

:~$ nano outro-script.sh

Links para meus vídeos sobre como utilizar o Vim e o Nano:

A edição de textos no terminal não está no escopo do nosso curso, mas este é um dos conhecimentos mais importantes para qualquer usuário do GNU:

Criando novos arquivos sem editores

Nós podemos criar os arquivos do script e deixar a edição para outro momento do procedimento. Para isso, nós temos algumas possibilidades:

  • Com redirecionamento de escrita;
  • Com here document e redirecionamento de escrita;
  • Com here string e redirecionamento de escrita;

Com o utilitário touch.

Dessas opções, a primeira e a última podem ser utilizadas para criar arquivos vazios, enquanto apenas as três primeiras possibilitam a criação de arquivos com algum conteúdo (como uma hashbang, por exemplo).

Criando um novo arquivo vazio com redirecionamento de escrita:

:~$ > meu-script.sh

O operador de escrita (>) diz para o shell que a saída de um comando deve ser escrita no arquivo que vier depois dele. Se não houver um comando, ou se o comando não produzir saídas, como nos casos dos comandos nulos :, true e false, por exemplo, o arquivo será criado sem conteúdo.

Criando um novo arquivo vazio com um comando nulo:

:~$ : > meu-script.sh

Também poderíamos utilizar o operador de append (>>):

:~$ >> meu-script.sh

A diferença é que, com o operador de escrita (>), caso o arquivo já exista, seu conteúdo será truncado (zerado) antes da escrita dos novos dados. Já com o operador de append (>>), caso o arquivo já exista, seu conteúdo será mantido e as novas linhas serão adicionadas ao seu final.

Criando um novo arquivo vazio com o utilitário touch:

:~$ touch meu-script.sh

A verdadeira finalidade do utilitário touch é alterar as informações data e hora de último acesso de um arquivo (timestamp), mas ele tem o “efeito colateral” de criar os arquivos que forem passados como argumentos, caso eles não existam.

Descrição curta do manual do touch:

:~$ man -k '^touch'
touch (1) - altera os horários do arquivo

A criação de arquivos com o touch pode ser desabilitada com a opção -c, o que demonstra que não se trata de uma função primária.

Criando o novo arquivo com hashbang via redirecionamento de append:

:~$ echo '#!/bin/bash' >> meu-script.sh
:~$ cat meu-script.sh
#!/bin/bash

A finalidade da hashbang será explicada mais adiante, mas é algo que estará quase sempre presente nos nossos scripts. Por isso, caso não queiramos editar o novo arquivo imediatamente, pode ser interessante criá-lo com, pelo menos, esse texto inicial.

Criando o novo arquivo com hashbang via here string:

:~$ cat <<< '#!/bin/bash' >> meu-script.sh :~$ cat meu-script.sh #!/bin/bash :~$

O utilitário cat imprime na saída padrão as linhas de texto recebidos na sua entrada padrão ou dos arquivos cujos nomes forem passados para ele como argumentos. No exemplo, o texto da hashbang é passado para ele através de uma here string, enquanto sua saída é redirecionada para append no arquivo do novo script.

Esta é uma possibilidade viável, mas não faz muito sentido, a menos que um texto bem maior, atribuído a uma variável, por exemplo, seja expandido na here string.

Criando o novo arquivo com hashbang via here doc:

:~$ cat << 'FIM' >> meu-script.sh
> #!/bin/bash
> # Arquivo: meu-script.sh
> # Descrição: Apenas uma demonstração.
> # Autor: Blau Araujo <blau@debxp.org>
> FIM
:~$ cat meu-script.sh
#!/bin/bash
# Arquivo: meu-script.sh
# Descrição: Apenas uma demonstração.
# Autor: Blau Araujo <blau@debxp.org>
:~$

Com o here doc, nós podemos criar o arquivo do script com algumas linhas de texto iniciais ou, em alguns casos mais simples, criar o script todo de uma vez.

Criar o arquivo de um script sem editores pode ser interessante se quisermos alterar sua permissão de execução antes de editá-lo, pois facilita a realização de testes iniciais sem fechar o editor.

1.9 – A linha do interpretador de comandos

Sobre a linha do interpretador de comandos, também chamada de hashbang, termo que se refere aos caracteres cerquilha (#) e ponto de exclamação (!) em inglês, trata-se de um recurso do sistema operacional para determinar qual programa será executado para interpretar (ou apenas abrir) o conteúdo de um arquivo de texto que tenha permissão de execução. Nós já falaremos sobre permissões, mas os conceitos por detrás da hashbang exigem uma atenção um pouco mais imediata, pois dizem respeito àquilo que o shell faz depois de interpretar os nossos comandos: providenciar, efetivamente, a sua execução.

Vale mencionar que o shell só executa os seus comandos internos, ou seja, qualquer outra coisa invocada deverá ser executada pelo sistema operacional.

O que significa “executar”

Resumidamente, executar significa: transferir o conteúdo do arquivo binário de um programa para a memória e indicar para a CPU que ali estão as instruções que ela deverá processar. Este é um resumo superficial, mas é suficiente para tirarmos algumas conclusões importantes:

  • Somente os programas compilados em arquivos binários serão executados;
  • A CPU lê as instruções que estiverem na memória, e não diretamente do arquivo do programa;
  • Se CPU e memória são componentes de hardware, quem gerencia o seu uso é o kernel, e nós já sabemos que ele gerencia a execução de programas através de processos.

Portanto, se o shell tiver que providenciar a execução de um programa, ele terá que solicitar ao sistema a criação de um processo, o que envolve a chamada de duas funções da biblioteca C padrão: as chamadas de sistema da família fork e exec.

No GNU/Linux, serão as chamadas de sistema clone e execve.

Chamadas de sistema

Este tópico envolve conceitos que, embora digam mais respeito à programação de sistemas, são essenciais para a compreensão de como o shell providencia a execução dos nossos comandos.

Chamadas de sistema, é como nos referimos a funções especiais do kernel que são invocadas a partir de funções definidas na biblioteca C padrão (libc///glibc). A função fork (que resulta na chamada de sistema clone, no GNU/Linux) inicia um novo processo como uma cópia fiel do processo que fez essa chamada (daí o termo clone), diferindo apenas no seu número de identificação (seu PID).

Depois da clonagem do processo pai, o arquivo que deverá ser carregado na memória para execução é passado como argumento de uma função da família exec (geralmente, execve). Se o arquivo for encontrado e tiver permissão de execução, seu conteúdo substituirá parte dos dados no processo clonado.

O restante dos dados do novo processo dirão respeito, principalmente, aos argumentos na linha do comando que invocou o programa, às variáveis exportadas para ele e aos recursos de memória e de entrada e saída que forem disponibilizados pelo kernel – além, é claro, dos dados que o programa determinar como necessários à sua execução (como bibliotecas compartilhadas, por exemplo).

Apesar de envolver conceitos e mecanismos complexos, essa visão geral deve ser simples o suficiente para levantar uma dúvida perfeitamente legítima: se os scripts não são arquivos binários, como eles poderão ser executados?

A resposta é simples: é um novo processo do shell não interativo, e não o arquivo do script, que será executado!

Em outras palavras: invocar a execução de um script é o mesmo que invocar a execução do binário do shell passando o nome do arquivo do script como argumento!

Executando o shell no modo não interativo

Quando executado no modo não interativo, o shell poderá interpretar as linhas de comandos que receber como um argumento da opção -c, como um fluxo de texto na sua entrada padrão ou lendo o arquivo cujo nome for passado como argumento.

Iniciando o Bash para executar um comando recebido como argumento:

:~$ bash -c 'echo Salve, simpatia!'
Salve, simpatia!

A sintaxe geral é:

bash -c COMANDOS

Onde COMANDOS é uma única palavra contendo os comandos que deverão ser executados. Isso é muito utilizado por lançadores de programas em geral, mas invocando o shell definido no caminho /bin/sh (o shell não interativo padrão, como vimos).

Iniciando o Bash para executar comandos na forma de fluxos de dados:

:~$ echo 'echo banana; echo laranja' | bash
banana
laranja

Aqui, nós usamos o builtin echo para enviar uma cadeia de caracteres para a saída padrão que, por sua vez, estava encadeada por pipe com o processo do bash. O mesmo poderia ser feito, por exemplo, se os comandos estivessem em um arquivo. Por exemplo, se o conteúdo do arquivo teste.txt fosse:

:~$ cat teste.txt
echo banana
echo laranja

Nós poderíamos executar seu conteúdo com um redirecionamento de leitura:

:~$ bash < teste.txt
banana
laranja

Mas, se os comandos estão em arquivos, não precisamos de redirecionamentos, porque o shell pode receber o nome do arquivo como argumento.

Iniciando o Bash para interpretar o conteúdo de um arquivo:

:~$ bash teste.txt
banana
laranja

Repare que, como vimos, a terminação .txt, assim como qualquer outra, não significa absolutamente nada para o shell!

Contudo, existe uma quarta opção para fazer o shell executar o conteúdo de um arquivo-texto: invocando apenas seu nome, desde que ele tenha permissão de execução.

Permissão de execução

A rigor, a ação padrão do shell quando invocamos o nome de qualquer arquivo na linha de comandos é executar o seu conteúdo. Neste sentido, podemos dizer que todos os arquivos são, ao menos potencialmente, executáveis. Então. o que realmente vai determinar se o shell tentará ou não executar o conteúdo de um arquivo são as suas permissões.

Observe o que acontece quando tentamos executar o arquivo teste.txt, dos exemplos anteriores:

:~$ ./teste.txt
bash: ./teste.txt: Permissão negada

O ./, que aparece antes do nome do arquivo, é apenas a indicação de seu caminho, o que pode ser lido como “neste diretório”.

Veja que a mensagem de erro foi bastante clara quanto à causa do problema: “permissão negada” – resta saber: permissão de quem para fazer o quê?

Individualmente, todo arquivo tem três permissões configuráveis: leitura, escrita e execução, representadas, respectivamente, pelas letras r, w e x (ou suas ausências) numa listagem completa do utilitário ls:

:~$ ls -l teste.txt
-rw-rw-r-- 1 blau blau 25 abr 18 07:39 teste.txt

Repare que, logo no começo da linha, nós temos três repetições das três permissões: rw-, rw- e r--. Em todas as três, a posição que deveria ser ocupada pela permissão de execução (x) aparece com um traço (-). Isso quer dizer que, nem eu, nem qualquer outro usuário do sistema, tem permissão para executar o arquivo teste.txt.

Eu sei disso porque, em sistemas UNIX like, as permissões de acesso a arquivos são configuradas para o usuário que for dono do arquivo, para o grupo de usuários do dono do arquivo e para usuários dos demais grupos:

 +--------- Dono do arquivo.
 |  +------ Grupo do dono do arquivo.
 |  |  +--- Outros grupos de usuários.
 |  |  |
 ↓  ↓  ↓
-rw-rw-r-- 1 blau blau 25 abr 18 07:39 teste.txt
             ↑    ↑
             |    |
             |    +--- Grupo do dono do arquivo
             +-------- Dono do arquivo.

Dando permissão de execução

A configuração de permissões, embora seja um procedimento simples, envolve conceitos que nos desviariam muito do nosso foco. Por isso, sem descartar a importância de pesquisar mais sobre o assunto, vamos nos limitar a ver como podemos alterar a permissão de execução de um arquivo:

:~$ chmod +x teste.txt

Onde:

  • chmod: Utilitário para manipular permissões de arquivos.
  • -x: Argumento que dá permissão de execução a todos os usuários.
  • teste.txt (no exemplo): O arquivo que será afetado.

O resultado é esse:

:~$ ls -l teste.txt
-rwxrwxr-x 1 blau blau 25 abr 18 07:39 teste.txt

Nós só podemos alterar permissões de arquivos que nos pertencem! O único usuário que pode alterar permissões de qualquer arquivo é o usuário administrativo root.

Tendo permissão de execução, nós já podemos testar a afirmação de que o shell executará o conteúdo de qualquer arquivo invocado na linha de comandos:

:~$ ./teste.txt
banana
laranja

Sucesso! Mas, o que aconteceu aqui?

Mais um pouco sobre como o sistema funciona

Sendo a invocação de um arquivo, não de um comando interno, o shell fez uma chamada de sistema fork, iniciando um novo processo como um clone de si mesmo, e uma chamada exec, que, por sua vez, verificou se o arquivo era um binário de um formato executável válido (formato ELF, no Linux). Sendo um arquivo-texto, a chamada exec falhou e o shell verificou se os dois primeiros caracteres do arquivo eram #!. Como não eram (ou seja, o arquivo não tinha uma hashbang), o clone recém-criado do processo do shell foi utilizado para ler e executar o conteúdo do arquivo.

Isso pode ser comprovado observando uma flag que o utilitário ps exibe para informar como o início do processo aconteceu: se a flag for 0 ou 4, o processo foi inciado passando pelas chamadas fork e exec; se for 1 ou 5, o processo foi clonado mas a chamada exec não foi executada.

O utilitário ps exibe diversas informações sobre processos e será muito utilizada no terceiro módulo do nosso curso.

Para demonstrar, vamos recorrer a mais um recurso do shell: os parâmetros especiais, que são como variáveis, mas são identificados por símbolos gráficos e são controlados apenas pelo próprio shell. Um desses parâmetros especiais é o nosso velho conhecido cifrão ($), que recebe o número do processo do próprio shell. Para expandir seu valor, basta escrever um outro cifrão antes dele, resultando na construção $$.

Expandindo e exibindo o PID do processo corrente do shell:

:~$ echo $$
1843763

Este é o número de identificação do processo do shell corrente. Sabendo disso, nós podemos executar o utilitário ps da seguinte forma:

ps -o f PID

Onde…

  • ps: Utilitário para exibir uma tabela de informações sobre os processos em execução no sistema.
  • -o: Opção para definir quais colunas da tabela serão exibidas pelo ps.
  • f: Argumento para exibir apenas a coluna flags.
  • PID: Número de identificação do processo investigado.

Sendo assim, vejamos o que acontece quando executamos esse comando numa sessão interativa do shell:

:~$ ps -o f $$
F
0

Note que a coluna das flags (F) informou o valor 0: o processo do shell passou pelas chamadas de sistema fork e exec. Mas o que aconteceria se esse mesmo comando fosse escrito no nosso arquivo teste.txt? Para testar, vamos alterar o conteúdo do arquivo para que fique com apenas esta linha:

:~$ echo 'ps -o f $$' > teste.txt
:~$ cat teste.txt
ps -o f $$

Executando novamente, o resultado será…

:~$ ./teste.txt
F
1

Desta vez, a flag é 1, indicando que o processo do shell, iniciado para executar o conteúdo do arquivo, passou por uma chamada de sistema fork, mas não pela chamada de sistema exec.

O que muda com a hashbang?

Entretanto, se os dois primeiros caracteres do arquivo forem #!, o shell fará uma chamada exec utilizando o caminho e o nome do programa indicado na mesma linha e o nome do próprio arquivo-texto como argumento.

Ou seja, se a primeira linha do arquivo for…

#!/bin/bash

O efeito será exatamente o mesmo de…

:~$ /bin/bash ARQUIVO

Disso, podemos concluir que, efetivamente, o papel de uma hashbang é apenas possibilitar a execução ou a abertura de arquivos-texto executáveis apenas invocando seu nome. Sendo assim, vamos incluir a linha do interpretador de comandos no arquivo teste.txt:

#!/bin/bash
ps -o f $$

E vamos executá-lo:

:~$ ./teste.txt
F
0

Desta vez, nós encontramos o valor 0 na coluna da flag: portanto, o processo do shell foi clonado e passou pela chamada de sistema exec.

Como o primeiro caractere é uma cerquilha (#), que é interpretado como um comentário pelo shell, o nome de um arquivo-texto executável com hashbang também pode ser passado como argumento da invocação do shell:

:~$ bash teste.txt

Mitos e lendas sobre a hashbang

A hashbang é um dos recursos mais geniais do shell de sistemas UNIX like, mas, como muitos outros conceitos, não está imune àqueles que preferem propagar regras a conhecer e explicar como, de fato, as coisas funcionam. Então, vamos aproveitar para antecipar algumas dúvidas muito populares.

A hashbang é obrigatória?

Não, nós só estamos falando do que acontece quando a utilizamos (ou não) nos nossos scripts. A obrigatoriedade depende de como responderíamos algumas perguntas, por exemplo:

  • O shell utilizado para interpretar o conteúdo do arquivo é indiferente?
  • O seu sistema só tem um shell instalado?
  • O seu script só será executado pelo shell não interativo padrão (/bin/sh)?
  • Mesmo que tenha vários shells, só você executará o script?
  • Seu script só será executado a partir da carga (source) em outro script?

Se a resposta for “sim” para todas essas perguntas, tudo bem, a hashbang é opcional! Caso contrário, ou se nem se tratar de um script em shell (pode ser Perl, PHP, Python, AWK, etc), aí sim, é bom considerar o uso de uma hashbang.

O certo não seria usar o env na hashbang?

No shell, só uma coisa é certa: tudo depende! A (falsa) controvérsia sobre o uso do utilitário env na hashbang tem a ver com possíveis incertezas quanto à localização do arquivo binário que o script requer para ser executado, porque o env tem a funcionalidade de localizar outros programas no sistema a partir de uma variável exportada chamada PATH. Contudo, numa instalação padrão do GNU/Linux, por exemplo, o Bash sempre estará no diretório /usr/bin, que, por motivos de compatibilidade histórica, sempre tem uma ligação simbólica no diretório /bin:

:~S ls -l /bin
lrwxrwxrwx 1 root root 7 dez 17  2023 /bin -> usr/bin

Portanto, se estivermos numa instalação padrão do GNU/Linux, o executável do Bash sempre estará no caminho /bin/bash!

A localização do binário executável na hashbang é muito importante, porque o sistema, na chamada exec, não teria como adivinhar onde os arquivos estão – e é por isso que nós precisamos informar o caminho completo:

#!/bin/bash

O problema (que rarissimamente é um problema de fato) é que nem todos os sistemas operacionais UNIX like armazenam o binário do Bash nos mesmos diretórios do GNU/Linux.

Na prática, com raríssimas exceções, isso não faz a menor diferença, porque os scripts são executados nos próprios sistemas em que foram criados ou em sistemas iguais aos utilizados quando foram criados. Portanto, o mínimo que se espera de quem cria esses scripts é que a pessoa conheça o próprio sistema e saiba onde estão os binários necessários para executá-los!

Uma hashbang com o env ficaria assim:

#!/usr/bin/env bash

Quando passamos o nome de um executável para a invocação do env, o que acontece é que ele mesmo se encarregará de localizá-lo para executá-lo (como dissemos, utilizando a variável exportada PATH). Então, supondo que o binário do env sempre estará no diretório /usr/bin (o que realmente se espera que esteja em sistemas UNIX like), muitos defendem que esta seria a forma “certa” de escrever uma hashbang – quando, no máximo, seria apenas mais uma opção quase sempre inócua.

Mas o env não torna o script mais portável?

Outro argumento, é que o uso do env tornaria o script mais portável, mas isso também não faz muito sentido prático. Quando se quer portabilidade, utiliza-se o shell não interativo padrão no caminho /bin/sh, que é universal em sistemas UNIX like. Além disso, mirando apenas no shell que nos interessa, o Bash, ele só será instalado por padrão no GNU/Linux, o que faz dele uma dependência a ser testada em todos os outros sistemas: mas, existindo uma dependência, o script, por definição, não poderá mais ser chamado de portável!

Na verdade, os primeiros casos de uso do env na hashbang de que se tem notícia remontam a meados dos anos 1990, quando algumas linguagens interpretadas começaram a ser instaladas em diversas versões no mesmo sistema operacional. Com várias versões disponíveis, o env passou a ser utilizado para especificar a versão da linguagem que estivesse definida como padrão para o sistema: se a pessoa programadora quisesse utilizar outra versão, ela teria que escrever o caminho completo para a versão desejada diretamente na hashbang.

Isso se assemelha muito ao que acontece no macOS em relação ao Bash: até o momento, o Bash é instalado por padrão, mas na sua última versão distribuída com licenciamento compatível com o sistema (versão 3.2.x). Com um pouco de paciência, é possível instalar a última versão, resultando em dois binários do Bash no sistema: o antigo, em /usr/bin/bash, e a versão mais recente, instalada pelo usuário, em /usr/local/bin/bash. Sendo uma alteração pessoal do sistema, nem sempre o caminho do Bash mais recente será configurado para ser o primeiro encontrado na variável PATH – e o script que depender do env para ser portável, acabará sendo executado com o Bash 3.2.x.

É por isso que eu recomendo que se utilize o caminho direto para o Bash na sua hashbang (/bin/bash, no GNU). Se a portabilidade for realmente importante, não escreva seu script em Bash: escreva scripts estritamente compatíveis com o shell em /bin/sh e utilize este caminho na hashbang. Mas, se quiser distribuir scripts em Bash para qualquer outro sistema, escreva um instalador (que pode ser um script compatível com o shell em /bin/sh) e, através dele, ajuste tudo que for necessário para que o seu script em Bash seja executado corretamente.

Sendo assim, não custa nada reforçar…

Sempre questione tudo que for propagado como “o jeito certo” de se fazer qualquer coisa! Avalie os argumentos com senso crítico e nunca troque o aprendizado por regras tiradas da… Eh… “da cartola”!

1.10 – O script final

Depois de uma primeira aula repleta de novidades, nós estamos oficialmente prontos para escrever o nosso primeiro script desta etapa do aprendizado!

Criando o arquivo salve.sh:

:~$ vim salve.sh

Conteúdo do arquivo:

#!/bin/bash

echo 'Salve, simpatia!'

Para torná-lo executável:

:~$ chmod +x salve.sh

Para executá-lo:

:~$ ./salve.sh
Salve, simpatia!

Exercícios

1. Utilitários da base do sistema

Nesta aula, nós mencionamos alguns programas tradicionalmente encontrados em sistemas operacionais UNIX e GNU/Linux. Pesquise e faça uma descrição curta de cada um deles.

  • ls:
  • tty:
  • cat:
  • awk:
  • grep:
  • touch:
  • ps:

Dica: você pode consultar o manual dos programas instalados no sistema com o utilitário man:

:~$ man NOME_DO_PROGRAMA

2. Shells do sistema

No seu sistema GNU/Linux, macOS ou \*BSD, investigue o que for pedido.

  • Qual é o shell interativo padrão do seu usuário?
  • Qual é o shell não interativo padrão do seu sistema?
  • Que outros shells existem instalados no seu sistema?
  • O shell em /bin/sh é uma ligação simbólica?
  • Se for, para qual arquivo a ligação aponta?

3. Shell POSIX

O shell em /bin/sh também é chamado de “shell POSIX”. Pesquise e responda:

  • O que são as especificações POSIX?
  • Por que /bin/sh é chamado de “shell POSIX”?
  • O que torna um shell compatível com as especificações POSIX?
  • O Bash é um shell POSIX?
  • Os shells Ash e Dash são POSIX?
  • Como fazer para o Bash ser executado em modo POSIX?

4. Comandos internos do Bash

Com apenas uma linha de comando, obtenha as descrições curtas dos seguintes comandos internos: help, echo, printf, :, true e false. Em seguida, responda o que for pedido.

:~$
  • Qual deles usar para obter ajuda sobre comandos internos do Bash?
  • Quais deles usar para escrever um texto na saída padrão?
  • Quais deles são comandos nulos (comandos que não fazem nada)?
  • Quais deles sempre terminam com sucesso?
  • Quais deles sempre terminam com erro?

5. Fluxos de dados padrão

No terminal, exiba a lista dos descritores de arquivos associados ao processo do shell que estiver em execução e responda o que for pedido.

:~$
  • A quem estão ligados os descritores de arquivos 0, 1 e 2?
  • Quais desses descritores de arquivos referem-se à escrita de dados?
  • Quais desses descritores de arquivos referem-se à leitura de dados?
  • Quem está ligado a stdin, stdout e stderr, respectivamente?

6. Linhas de comandos

Se um comando simples pode ser formado pela combinação de: exportações de variáveis, uma invocação (e seus argumentos) e redirecionamentos, investigue e responda (o contexto é o shell Bash):

  • Se houver exportações, a invocação de algo é obrigatória?
  • O que acontece se a linha contiver apenas atribuições de variáveis?
  • O que acontece se a linha contiver apenas um redirecionamento de escrita?
  • O que separa as palavras na linha de um comando?
  • Como o shell sabe quem é o quê na linha de um comando simples?
  • Um comando pode ter mais de um redirecionamento?
  • A quem se destinam as exportações e os argumentos?
  • Quais partes desses elementos poderiam resultar de expansões do shell?

7. Operadores de controle

Na tabela abaixo, preencha as colunas vazias e responda.

===============================================================
| Operador | Função                                           |
===============================================================
|    ;     | Encadeamento síncrono incondicional.             |
|----------+--------------------------------------------------|
|          | O mesmo que ;.                                   |
|----------+--------------------------------------------------|
|          | Execução assíncrona (em segundo plano).          |
|----------+--------------------------------------------------|
|    |     |                                                  |
|----------+--------------------------------------------------|
|          | Pipe da saída padrão e da saída padrão de erros. |
|----------+--------------------------------------------------|
|    &&    |                                                  |
|----------+--------------------------------------------------|
|          | Encadeamento condicional "se erro".              |
===============================================================
  • O que significa “execução assíncrona”?
  • Por que os operadores de controle separam palavras na linha do comando?
  • Com o operador ;, quando o comando seguinte é executado?
  • Num encadeamento por pipe, quando os comandos são executados?
  • O que é uma “lista de comandos”?

8. Operadores de redirecionamento

Na tabela abaixo, descreva as funções dos operadores e responda.

===============================================================
| Operador       | Função                                     |
===============================================================
| <[FD] ARQUIVO  |                                            |
|----------------+--------------------------------------------|
| [FD]> ARQUIVO  |                                            |
|----------------+--------------------------------------------|
| [FD]>> ARQUIVO |                                            |
|----------------+--------------------------------------------|
| &> ARQUIVO     |                                            |
===============================================================
  • O número opcional FD (file descriptor) pode ser omitido em que situações?
  • Qual seria o número de FD para redirecionar stderr para um arquivo?
  • Os redirecionamentos sempre têm que ser feitos após uma invocação?
  • Como criar arquivos vazios de forma segura apenas com redirecionamentos?
:~$

9. Criação de scripts

Sem utilizar editores de texto, como você criaria o arquivo script.sh, abaixo?

#!/bin/bash

echo 'Salve, simpatia!'
echo 'Qual é a sua graça?'

Resposta:

:~$

Como tornar executável o arquivo script.sh que você acabou de criar?

:~$

Execute o script e anote o que foi impresso:

:~$

Envie a saída do script para o arquivo mensagens.txt:

:~$

Exiba no terminal o conteúdo do arquivo mensagens.txt:

:~$

Sem permissões, como seria possível executar o script?

:~$

10. Shell não interativo

O parâmetro especial - (traço) expande as opções de início do shell. Entre elas, a letra i é exibida quando o shell é executado no modo interativo. Com base nisso, demonstre…

Que o Bash ligado ao terminal está no modo interativo:

:~$

Que o Bash executado com a opção -c COMANDO é não interativo:

:~$

Que o Bash executa comandos recebidos em stdin não interativamente:

:~$

Que o Bash executa scripts não interativamente:

:~$