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. grande
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.
shell na cebola

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.
metacaracteres

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

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
  1. Bash, comandos e programas
  2. História e sua utilidade
  3. Meta-caracteres '*' '?' '[x-z]'
  4. Substituições
  5. Parâmetros de/para Bash
  6. O que faz grep?
  7. O que faz echo?
  8. O que faz cat?
  9. O que faz tac?
  10. O que faz head?
  11. O que faz tail?
  12. O que faz seq?
  13. O que faz tr?
  14. O que faz file?
  15. O que faz cal?
  16. O que faz wc?
  17. 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. raptor --fim da aula--