CI064, 2019-1 © Roberto André Hexsel, 2014-2019
Bash, ou como computeiros ganham tempo
Na primeira aula vimos alguns comandos que nos permitem utilizar
eficientemente algumas das abstrações providas pelo Unix. Nesta aula
iniciaremos o estudo mais aprofundado de Bash e de sua programação.
Antes de mais nada, crie um prompt vazio, copiando a linha abaixo
no seu terminal
alias prompt:=""
Três clicks no botão esquerdo para copiar, um click no meio para colar
Se ainda não o fez, por favor envie uma mensagem de e-mail para
rhexsel@gmail.com, com assunto "ci064", para que eu possa inscrevê-l{a,o}
na lista de discussão da disciplina.
Quarta abstração - Shell
Bash é um interpretador de comandos que esconde do usuário uma quantidade
enorme de detalhes incômodos e desagradáveis do funcionamento do sistema
operacional. Bash também é um ambiente de trabalho riquíssimo dotado de
funcionalidades que agilizam a vida de quem sabe tirar proveito delas.
Vejamos:
1) Bash mantém um histórico dos últimos comandos executados e permite sua
reutilização de forma indolor e eficiente, deveras eficiente;
2) Bash é programável, com suporte a variáveis, avaliação de expressões
aritméticas, comandos de iteração, funções e recursão;
3) Bash, em sendo programável, permite a escrita de programas
razoavelmente complexos numas poucas linhas de código;
4) Bash oferece vários mecanismos para acelerar a digitação de comandos;
5) muitas outras coisas de que você não quer saber agora, mas quererá ao
final desta e das próximas aulas.
Não tenho, nesta e nas próximas duas aulas, a menor intenção de oferecer
uma cobertura extensa e completa sobre todas as funcionalidades de Bash,
porque o tempo é escasso e Bash é grande e complexa. O manual de Bash
contém tudo o que você precisa saber para usar as funcionalidades de que
necessita.
Se você já domina o material destas aulas, vá ao manual e investigue o que
ficou de fora delas.
História
Antes de mergulharmos na história, é necessário diferenciar programas
executáveis de comandos da shell. Por enquanto, um programa
executável é o resultado da compilação de um programa escrito numa
linguagem como C e que é armazenado em /bin ou /usr/bin, por exemplo. Para
executáveis, o comando type retorna o caminho completo do programa:
prompt: type cp
cp is hashed (/bin/cp)
Um comando da shell é uma funcionalidade provida pela shell, e não
um programa autocontido como /bin/cp. Por exemplo, o comando cd é provido
por Bash e não é um programa executável porque a noção de diretório
corrente é uma ideia que faz (mais) sentido para usuários da shell.
prompt: type cd
cd is a shell builtin
Há uma página gigantesca de manual para Bash, porque este programa é
complexo e possui enorme funcionalidade. Para os comandos individuais,
geralmente "help x" é o suficiente para se obter a informação necessária
sobre o comando x:
prompt: help cd
cd: cd [-L|[-P [-e]]] [dir]
Change the shell working directory.
Change the current directory to DIR. The default DIR is the value of the
HOME shell variable.
...
Evidentemente,
prompt: type help
help is a shell builtin
E ainda,
prompt: help help
help: help [-dms] [pattern ...]
Display information about builtin commands.
...
Daqui para frente, um "programa" será chamado de programa ou de
executável, enquanto que um comando provido por Bash será chamado de
comando.
À História então.
O comando history mostra na tela a lista dos últimos comandos e programas
executados. A história é armazenada num arquivo escondido, tipicamente
~/.bashHistory o que permite relembrar o trabalho executado desde a última
sessão no sistema. Abaixo estão os últimos 10 itens da minha história
recente.
prompt: history
...
2056 which zile
2057 ls
2058 type /bin/ls
2059 type ls
2060 type cp
2061 which cd
2062 locate cd
2063 ls -l /bin/cd
2064 type cd
2065 history
Se eu disser "!!" (exclamação exclamação), o último comando é executado
novamente (history); se eu disser "!2059" o comando de número 2059 na
história é executado novamente (type ls).
Se eu disser CTRL-R (Reverse), Bash me permite "caminhar para trás no
tempo" e buscar um comando na história. Por exemplo, se eu disser CTRL-R e
w, Bash mostrará o primeiro comando na história com um caractere w, que no
caso é "which cd". Se ENTER for então digitado, o "which cd" é executado
novamente.
Considere a economia: CTRL-R w ENTER
ao invés de which cd
Estas notas pressupõem que Bash executa com um interpretador de teclado
que é Emacs-like; é possível configurá-la para um ambiente vi-like. Não
uso vi e não posso ajudar se seu ambiente é vi-like. Vire-se!
Um truque particularmente útil é a substituição rápida, obtida com um trio
de circunflexos ("^"). Suponha que o último comando executado tenha sido
prompt: which emacs
que identifica qual é o comando/programa chamado "emacs" disponível no
sistema. Se quisermos pesquisar pela versão leve do editor, poderíamos
digitar
^emacs^zile^
o primeiro '^' deve estar encostado na margem esquerda
isso causaria a execução do último comando, trocando-se emacs por zile,
cujo resultado seria equivalente a
prompt: which zile
Este exemplo não parece muito interessante, mas se o comando anterior
contiver 60 caracteres, trocar uma parte dele com a substituição rápida
pode ser bem mais eficiente do que editar a linha, usando as setas...
Setas? Sim, setas. A linha de comando pode ser editada e as setas
horizontais tem o comportamento óbvio. A seta upward retrocede na
história, e a seta downward avança na história.
A versão lenta, porém geral, da substituição é provida pelo comando fc.
Diga
prompt: help fc
e experimente com "fc -s pat=rep command", sendo "pat" um padrão (pattern)
--uma sequência de caracteres de um comando existente, "rep" (replacement)
a sequência que deve substituir "pat", e "command" o comando sobre o qual a
edição deve ser aplicada. Por exemplo, na minha história, qual seria o
efeito dos comandos abaixo? Responda antes de tentar na sua própria
história.
prompt: fc -s cd=mv 2063
prompt: fc -s cd=mv type
Dependendo de como o seu teclado estiver configurado, as setas podem ser
usadas para caminhar na história. A "seta para cima" permite voltar na
história, enquanto que as setas para os lados ajudam na edição de comandos
da história. Experimente --se seu teclado permitir-- voltar três comandos
para trás e mude algo com as setas mais DELETE, BACKSPACE e inserções.
Outra facilidade que é excepcionalmente útil é a "expansão de TABs". Se
você digitar um caminho incompleto e digitar TAB, Bash mostrará o conteúdo
do diretório, ou perguntará, caso as opções sejam muitas:
prompt: ls /bin/TAB
Display all 141 possibilities? (y or n)
Outra alternativa, é o caminho completo, com uma ou mais letras do nome do
arquivo ou diretório:
prompt: ls /bin/gTAB # g seguido de TAB
nada acontece porque não existe um arquivo chamado "g" em /bin mas
prompt: ls /bin/gTABTAB # g seguido de dois TABs
getfacl grep gunzip gzexe gzip
com dois TABs a lista de todos os arquivos que iniciam com "g" é mostrada;
se você acrescentar uma letra, "z", por exemplo:
prompt: ls /bin/gzTABTAB # gz seguido de dois TABs
gzexe gzip
a lista diminui para apenas os arquivos que iniciam com "gz". Expansão de
TABs é um acelerador poderoso quando se está buscando um arquivo ou
diretório e não há certeza de sua localização ou nome completo.
Meta-caracteres, o que são e como se reproduzem
Mudemos o modus operandi temporariamente. Execute o comando abaixo
e tente entender seu efeito:
prompt: ls -d /home/bcc/a*8
Qual o efeito do '*' (asterisco)?
Repita:
prompt: ls -d /home/bcc/ah*8
Qual a diferença?
Execute os comandos abaixo, com os '?' (interrogação) e analise seus efeitos.
prompt: ls /bin/??
prompt: ls /bin/???
prompt: ls /bin/????
{ espaço em branco intencional }
Voltemos ao modo normal que é "responda antes de executar".
O '*' (asterisco) é um "meta-caractere" que equivale a "qualquer sequência
de caracteres", inclusive a uma sequência vazia --este meta-caractere
substitui, ou vale, desde nada até tudo.
O '?' (interrogação) é um meta-caractere que equivale a exatamente um
caractere.
Estes meta-caracteres são interpretados por Bash, que os expande de uma
maneira que é dependente de contexto. Nos exemplos acima, os
meta-caracteres foram expandidos na busca por caminhos ("pathname
expansion"). Outras formas de expandir meta-caracteres serão vistas
adiante.
Vou repetir porque é importante: a interpretação de meta-caracteres, bem
como as expansões que veremos em breve, são efetuadas pela shell e não
pelos programas/aplicativos. No exemplo do ls com meta-caracteres, Bash
entrega para ls uma lista de argumentos que resulta da expansão dos
meta-caracteres; ls somente processa os argumentos recebidos da shell.
Dois exemplos da utilidade do asterisco são mostrados abaixo. O que eles
fazem?
prompt: ls -d /home/*/a*a
prompt: ls -l /usr/*/doc/*/README
Os meta-caracteres '*' e '?' são usados por Bash na expansão de caminhos
para "casar" padrões. Nestes últimos dois exemplos, o asterisco "casa" com
vários nomes de diretórios, e o meta-caractere é expandido para todos os
padrões que casam, que são todos os diretórios --ou arquivos-- num
determinado nível da árvore.
No exemplo acima ls -d /home/*/a*a, Bash efetua a expansão dos
metacaracteres e /bin/ls recebe uma lista de diretórios. Após a
expansão, o comando por executar seria algo como
ls -d /home/est/aurea /home/inf/anamaria /home/inf/aurora /home/mat/ana
Outro casador de padrões poderoso é o "conjunto de caracteres". Por
exemplo, '[abc]' representa um caractere que casa com qualquer dentre
'a', 'b' ou 'c'. Uma faixa de caracteres é representada por um hífen, como
'[A-Pq-z]'. Este conjunto representa um caractere que pode tomar o valor
de uma maiúsculas de 'A' até 'P', ou de uma minúscula de 'q' a 'z'.
Qual é o conjunto de diretórios listado pelo comando abaixo?
prompt: ls -d /home/bcc/[ampz]??[01][7-9]
Se o caractere '!' ou '^' é o primeiro elemento do conjunto, então o
conjunto representado é o complemento do que segue. '[^abc]' representa um
caractere que é qualquer das minúsculas exceto 'a', 'b' ou 'c'.
If the extglob shell option is enabled using the shopt
builtin, several extended pattern matching operators are recognized.
In the following description, a pattern-list is a list of one or
more patterns separated by a |. Composite patterns may be formed
using one or more of the following sub-patterns:
?(pattern-list)
Matches zero or one occurrence of the given patterns
*(pattern-list)
Matches zero or more occurrences of the given patterns
+(pattern-list)
Matches one or more occurrences of the given patterns
@(pattern-list)
Matches one of the given patterns
!(pattern-list)
Matches anything except one of the given patterns
Meta-caracteres podem ser "escapados" com a contra-barra '\', e quando
escapados, perdem seus superpoderes e tornam-se caracteres vulgares e
comuns. Por exemplo, se você criar um arquivo cujo nome é um único
asterisco --que os deuses tenham piedade do pateta que fizer isso-- existem
duas maneiras de trocar o nome para algo que não seja tão pavorosamente
perigoso, ou de remover o arquivo indesejável.
ACHTUNG: não executei estes comandos e recomendo enfaticamente que isso
não seja tentado, nem aqui, nem em sua casa.
A primeira maneira é com um escape para remover o poder universalmente
casador do asterisco:
prompt: rm \*
A segunda, não, não há segunda. Dane-se. Preste atenção na próxima vez.
Em casos menos drásticos, por exemplo quando magicamente aparece um hífen
no início de um nome de arquivo, algo como '-WWWW', tanto o mv quanto o rm
reclamarão amargamente sobre uma opção '-W' inexistente. Se você tiver
azar, muito azar, seu arquivo teria um nome como '-rf' e você tentaria
remover o arquivo de nome ofensivo seguido de mais um outro. Qual o
potencial resultado? Note que o primeiro argumento é um arquivo com nome
ofensivo e que é interpretado, equivocadamente, como uma opção.
prompt: rm -rf a*
Qual a saída? Aqui é simples, basta apontar o argumento como sendo um
caminho, e não uma opção. O './' indica ao rm que o primeiro argumento é
um arquivo e não as opções '-r' e '-f'.
prompt: rm ./-rf a*
Outro nome de arquivo "do mal" é um que inicia com '#', que é interpretado
como um comentário:
prompt: rm #_sou_um_bocoh
rm: missing operand
Try 'rm --help' for more information.
mas
prompt: rm ./#_nao_sou_tao_bocoh
rm: impossível remover './#_nao_sou_tao_bocoh': No such file or directory
tem o efeito desejado.
De novo, não custa insistir: se você quiser experimentar com nomes de
arquivos entre o esquisito e o ilegal ('*' ou '-xyz'), crie um diretório
para testes e execute seus testes neste ambiente confinado, como se fosse
uma caixa de areia no quintal da sua casa. Tome especial cuidado com
testes que envolvem a remoção de arquivos porque rm é implacável, impiedoso
e irreversível. Caveat emptor, ou "você foi avisado".
Substituições
A exposição sobre meta-caracteres é a introdução a um dos vários
mecanismos de substituição providos por Bash. As substituições são
brevemente descritas abaixo, na ordem em que são efetuadas. A exposição
pode parecer um tanto estéril, mas a utilidade das substituições ficará
evidente na próxima aula, quando serão utilizadas na programação e em
scripts.
Expansão de Chaves "Brace Expansion" é um mecanismo similar à
expansão de caminhos, mas as palavras geradas podem ser nomes arbitrários e
inexistentes. Um exemplo pode facilitar a compreensão:
prompt: echo prefixo-{a,c,b}-sufixo
prefixo-a-sufixo prefixo-c-sufixo prefixo-b-sufixo
O padrão prefixo-LETRA-sufixo é expandido com as opções dentro das chaves,
na ordem em que as letras aparecem. O prefixo e o sufixo não são
alterados. Tanto prefixo- quanto -sufixo são opcionais.
Esta expansão é particularmente útil quando se deseja gerar um conjunto de
nomes que seja mais restrito do que aquele provido por '*'. A saber:
prompt: ls /home/html/inf/roberto/ci{210,212,064}
mostra o conteúdo dos diretórios com material disponível na Internet de
três das minhas disciplinas. Note que a expansão das chaves ocorre ANTES
da expansão dos nomes dos diretórios; Bash entrega para ls três caminhos
completos e não um único caminho com meta-caracteres:
/home/home/html/inf/roberto/ci210
/home/home/html/inf/roberto/ci212
/home/home/html/inf/roberto/ci064
enquanto que
prompt: ls /home/html/inf/roberto/ci*
expande para uma lista com mais de 180 itens.
A outra forma da expansão pode ser usada para gerar sequências de números.
Dois exemplos ilustram as possibilidades:
prompt: echo {2..8}
2 3 4 5 6 7 8
prompt: echo {2..8..2}
2 4 6 8
No primeiro caso, a sequência é gerada e inclui os extremos da faixa. No
segundo, o terceiro parâmetro define o incremento entre os elementos da
sequência. Ao invés de inteiros, a sequência pode ser de caracteres:
prompt: echo {a..m..2}
a c e g i k m
A ordem pode ser crescente ou decrescente e o mecanismo de expansão emprega
as salvaguardas razoáveis contra erros. As expansões podem ser aninhadas.
Qual o resultado dos comandos abaixo?
prompt: echo {2..8..3}
prompt: echo {8..2..3}
Decubra quais são os anos bissextos entre 2019 e 2027, sem usar
divisões por quatro. Use o programa cal para verificar sua
resposta -- os meses devem ser indicados em Inglês.
prompt: cal feb 2019
Agora é uma boa hora para você gastar cinco minutos experimentando com as
expansões, especialmente as aninhadas. Como sempre, imagine uma sequência,
escreva a expressão, verifique se a expressão faz mesmo o que você quer, e
só então execute o comando para verificar o resultado.
Expansão de Til O caractere '~' (til) sozinho é substituído pelo
caminho completo do diretório home do usuário. Til prefixando um username
é expandido para o caminho completo do diretório daquele usuário. Os dois
comandos abaixo são equivalentes; a segunda forma seria mais útil se o
usuário fosse outro que não eu próprio.
prompt: ls ~
prompt: ls ~roberto
Expansão de Parâmetros Um "parâmetro" pode ser uma variável usada na
programação de Bash, ou um argumento passado para a shell ou para
um script --detalhes sobre estes na próxima aula. Algumas das
possibilidades são mostradas no que se segue.
prompt: echo ${PWD}
A expansão do parâmetro PWD, que é uma variável da shell, é passada para o
comando echo que exibe no terminal o caminho completo do diretório
corrente. Com a adição do '#' (cerquinha), a expansão retorna o número de
caracteres do parâmetro expandido:
prompt: echo ${#PWD}
24
Suponha que ao parâmetro src possam ser atribuídos todos os nomes de
arquivos fonte C num diretório, e que se deseje o nome do arquivo sem
o sufixo .c . A expansão abaixo remove o sufixo '.c':
prompt: src=arquivo.c # inicializa parâmetro
prompt: echo ${src%.c}
arquivo
esta expansão remove o que está após o '%' (porcento) do parâmetro. Seu
simétrico é o operador '#' (cerquinha) que remove o prefixo:
prompt: echo ${src#arq}
uivo.c
Formalizando a definição: com ${parameter%word} ou ${parameter%%word}, word
é expandida com "expansão de caminhos" (descrita abaixo) e produz um
padrão. Se o padrão casa no final do valor estendido de parameter (o
sufixo), então o resultado é a versão estendida de parameter com o sufixo
mais curto (%) removido, ou o sufixo mais longo (%%) removido.
O mesmo vale para os prefixos, trocando-se '%' por '#'.
Há mais, muito mais, na página de manual e nos exercícios.
É sempre uma boa ideia colocar o parâmetro entre chaves para garantir que
não haja confusão entre o parâmetro, sua expansão, e o texto nas
vizinhanças.
Segue um exemplo para diferenciar '%' de '%%'. [Obrigado Tiago Vignatti]
prompt: a="nome.bizarro.do.arquivo.txt"
prompt: echo ${a%.*}
nome.bizarro.do.arquivo
prompt: echo ${a%%.*}
nome
Substituição de Comando O comando é substituído pela saída padrão
produzida pela execução do comando. O comando por substituir deve estar
entre parênteses: '$(comando argumentos)'.
O comando seq gera uma sequência de números segundo os argumentos passados
ao programa. A substituição do comando "seq 2 8" gera a mesma sequência
que a expansão de chaves '{2..8}':
prompt: echo $(seq 2 8)
2 3 4 5 6 7 8
A última quebra de linha produzida pelo programa é eliminada da saída pela
substituição. Substituições de comando podem ser aninhadas.
Note que o exemplo acima é ineficiente porque Bash provê exatamente o mesmo
recurso, sem que seja necessário executar outro programa, que no caso é
seq. Há casos em que o resultado do programa não pode ser produzido por
Bash e então a substituição pode ser extremamente útil.
Mais um exemplo. O que faz o comando abaixo? Pense antes de executá-lo.
prompt: seq $(ls | wc -c) $(ls -l | wc -c)
Expansão Aritmética Uma expressão aritmética é avaliada e seu
resultado substituído. O formato da expansão é '$((expressão))'.
prompt: echo $((2*8))
16
Os componentes da expressão sofrem expansão de parâmetros, expansão de
strings, substituição de comandos, e remoção de "quotes" (veja abaixo)
antes da avaliação. Mais sobre a avaliação das expressões na próxima aula.
Qual o resultado do comando abaixo? Repare que há uma mudança com relação
ao último exemplo da substituição de comando.
prompt: echo $(( $(ls | wc -l) * $(ls -l | wc -l) ))
Separação de Palavras ("Word Splitting"). IFS (Internal Field
Separator) é uma variável da shell que armazena os caracteres considerados
"separadores" de palavras. Tipicamente o valor de IFS é
SPACE TAB NEWLINE:
prompt: echo -n "$IFS" | od -a
0000000 sp ht nl
o programa od (octal dump), com a opção '-a', traduz para ASCII sua
entrada. O resultado do pipeline mostra que o conteúdo da variável IFS é
mesmo SPACE (sp), TAB (ht ou horizontal tab), e NEWLINE (nl).
Bash examina os resultados da expansão de parâmetros, substituição de
comandos, e expansão aritmética, que não ocorrem entre aspas duplas, e
efetua a separação de palavras. Sequências de caracteres IFS no início e
no final do resultado das expansões mencionadas são ignoradas, e sequências
de caracteres IFS que não estão no início ou no final são considerados um
único separador de palavras -- vários espaços são considerados como UM espaço.
Expansão de Caminhos Após a separação de palavras, Bash examina cada
palavra procurando pelos caracteres '*', '?' e '['. Se algum destes é
encontrado, a palavra é considerada como um "padrão" e substituída por uma
lista de nomes de arquivos em ordem alfabética.
O caractere '.' no início de um nome, ou antes de uma barra ('/') deve ser
casado explicitamente. Em outros casos, '.' é considerado um caractere
normal.
Para casar um caminho, o caractere '/' deve ser casado explicitamente.
O casamento de padrões com os meta-caracteres '*', '?' e conjuntos foi
discutido anteriormente.
Quote Removal Após todas as outras expansões, todas as ocorrências
dos caracteres contra-barra (\), aspa simples ('), e aspa dupla ("") que
não resultaram de alguma das outras expansões são eliminadas. A diferença
entre aspas simples e duplas é discutida numa próxima aula.
Resumo
- Bash, comandos e programas
- História e sua utilidade
- Meta-caracteres '*' '?' '[x-z]'
- Substituições
- Parâmetros de/para Bash
- O que faz grep?
- O que faz echo?
- O que faz cat?
- O que faz tac?
- O que faz head?
- O que faz tail?
- O que faz seq?
- O que faz tr?
- O que faz file?
- O que faz cal?
- O que faz wc?
- O que faz od?
Exercícios
1) história: Qual o efeito do seguinte "comando"? Antes de testar o
resultado verifique se não ocorrerá nada de catastrófico pela execução
da sequência resultante.
prompt: !-5 ; !-2 ; !-8
2) substituição de comandos: Encontre um modo de gerar todas as
sequências de números mostradas em Expansão de Chaves com o programa seq.
3) Qual o conteúdo dos seguintes conjuntos?
(a) [A-ZA-z0-9]
(b) [p-z][3-7]
(c) [^a-z]
(d) [[.;:!?]
(e) [a-z_]*[-a-z0-9]@*[-a-z0-9.]
4) word splitting: Explique a diferença entre os comandos abaixo;
prompt: echo $IFS | od
prompt: echo "$IFS" | od -a
prompt: echo -n "$IFS" | od -a
5) expansão de caminhos: Indique, ao escrever um exemplo, os
possíveis resultados das expansões seguintes. Suponha que existem nomes
em quantidade e variedade suficiente para gerar, ao menos, um resultado
para cada padrão. Cada expressão descreve, ou gera, um conjunto de
"nomes". O que você deve fazer é enumerar estes conjuntos.
?([a-c]0[a-c]|[a-c]5[a-c]|[a-c]9[a-c])
*([a-c]0[a-c]|[a-c]5[a-c]|[a-c]9[a-c])
+([a-c]0[a-c]|[a-c]5[a-c]|[a-c]9[a-c])
@([a-c]0[a-c]|[a-c]5[a-c]|[a-c]9[a-c])
!([a-c]0[a-c]|[a-c]5[a-c]|[a-c]9[a-c])
6) Difícil: (word splitting) verifique se é possível alterar o valor
de IFS de forma a que seja possível interpretar corretamente uma
planilha em formato CSV (comma separated values), com campos
compostos de texto e números.
Em duas aulas iniciaremos o estudo de apelidos, programação de Bash e
funções.
--fim da aula--