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 comodash
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.
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.
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”.
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 pelops
.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
e2
? - 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
estderr
, 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:
:~$