Introdução ao bash

Das pipelines às barrinhas girantes

Versão 0.1, 02/01/2010.

Este texto pretende ser uma introdução ao bash e ao shell-scripting. Ele assume familiaridade com os comandos básicos do Unix (cd, ls, cp, rm, ps, grep, ...) e com outros aspectos do sistema (organização do sistema de arquivos, etc.). Ao contrário de outros tutoriais, eu pretendo dar explicações precisas do funcionamento do shell, com todos os detalhes "irrelevantes"; embora isso possa tornar o texto mais denso, creio que essa abordagem será mais vantajosa a longo prazo. Sugestões e críticas são bem-vindas.

Índice

O que é o shell?

O shell é um interpretador de comandos. Quando você abre um terminal, o shell inicia em modo interativo: ele lê uma linha de texto, separa-a em palavras, realiza certas substituições e redirecionamentos, e executa o comando correspondente. O comando pode ser interno ao próprio shell (builtin), ou pode ser um programa externo, que será invocado pelo shell.

O shell também pode ser usado em modo não-interativo. Nesse caso, os comandos são lidos de um arquivo, chamado shell script. Mas um script não se limita a uma série de comandos simples; o shell do Unix é uma criatura complexa, com variáveis, estruturas de execução condicional (if) e repetição (for/while), funções, e outras maravilhas que se encontram em linguagens de programação de alto nível. Dessa forma, é possível escrever aplicativos bastante complexos em shell-script, desde utilitários de configuração e manutenção, até programas com interface Web (CGI). A principal limitação do shell é que ele é bem mais lento do que outras linguagens interpretadas.

Existem vários shells no Unix. A maior parte deles deriva do Bourne shell (sh). Neste tutorial, será usado o bash (versão 3.2), que é compatível com o sh, mas que possui várias features não encontradas nele. O bash vem por padrão em todas as distribuições de GNU/Linux normais.

O que o shell faz por você?

No caso mais simples, o shell lê uma linha de comando, separa-a em palavras, e executa o comando correspondente à primeira palavra, passando as palavras seguintes como argumentos para o comando. Por exemplo, em ls -la /var, o shell separa o comando em três palavras, "ls", "-la" e "/var". O comando a ser executado é o "ls", e "-la" e "/var" são os dois argumentos desse comando. O comando, então, é livre para interpretar seus argumentos como bem entender; neste caso, o ls entende o "-la" como uma série de opções e "/var" como um nome de diretório.

Porém, o shell pode fazer muito mais do que separar palavras e rodar programas. Por exemplo, quando você executa um comando como rm *.txt, o shell substitui o *.txt pelos nomes de todos os arquivos com a extensão ".txt" no diretório atual. O comando rm recebe como parâmetros essa lista de arquivos, e não o "*.txt" original; para o rm, é como se o usuário tivesse digitado cada nome de arquivo diretamente na linha de comando.

Outras possibilidades incluem:

Como já foi mencionado, o shell também possui estruturas de controle de execução. Algumas possibilidades:

Muito bem, agora você tem idéia de o que o shell é capaz de fazer. Nos próximos capítulos, você verá como usar essas maravilhas detalhadamente. Se quiser, copie e cole os comandos mostrados em um terminal e veja os resultados.

Criando um shell-script

Antes de mais nada, vamos ver como criar um arquivo e convencer o sistema de que ele é um shell-script. Abra o seu editor de texto favorito e crie um arquivo chamado hello no seu diretório home ou em outro lugar conveniente, com o seguinte conteúdo:

#!/bin/bash
echo "Hello, world!"

Observe a primeira linha. Quando um arquivo iniciado pela seqüência #! (conhecida como sha-bang ou she-bang) é executado, o kernel entende que se trata de um script que deve ser interpretado pelo programa especificado após o #!. Ele então chama o interpretador, passando o nome do script como argumento. (Experimente criar um script contendo #!/bin/ls -l na primeira linha e executá-lo; depois teste com #!/bin/rm.)

Tendo salvo o arquivo, abra um terminal, vá ao diretório do arquivo, e execute chmod +x hello. Isso dará permissão de execução ao arquivo. (No Unix, o que determina se um arquivo é executável é a permissão, e não a extensão.)

Por fim, execute ./hello. Isso deverá mostrar a mensagem "Hello, world!" na tela. O comando deve ser precedido por ./ porque não se encontra em um diretório do caminho de pesquisa de comandos ($PATH). Se você o copiar para /usr/local/bin, por exemplo, não será necessário incluir o ./.

De onde vêm os comandos

Depois que o shell interpreta uma linha de comando e determina qual é o nome do comando a ser executado, ele precisa encontrar o comando. A primeira coisa que o shell faz é ver se o comando é uma palavra reservada (como if, for, while, done, ...). Se não for, o shell verifica se existe uma função com o nome dado. Se não houver, ele procura por um builtin (comando interno ao shell). Se tudo falhar, o shell então conclui que o comando é um programa externo.

Para determinar onde se encontra o programa externo, o shell procede da seguinte maneira: se o nome do comando começa com /, então trata-se de um caminho absoluto de arquivo. Se o nome não começar com /, mas contiver uma /, trata-se de um caminho relativo ao diretório atual. Se o nome não contiver /, então o shell procura em cada um dos diretórios do caminho de pesquisa de comando (path). O path é determinado por uma variável de ambiente (veremos estas mais adiante), chamada PATH, que contém uma lista de diretórios separados por :. Você pode usar o comando echo "$PATH" para ver quais diretórios estão no path do seu shell.

Você pode alterar o path nos arquivos de configuração do shell. Esses arquivos variam de distribuição para distribuição; os mais comuns são o /etc/profile ou /etc/bash_profile para a configuração global do sistema, e .bashrc para a configuração de cada usuário. Em muitas distribuições, se você criar um diretório bin em seu home, ele será automaticamente incluído no path quando o shell iniciar. Se esse não for o seu caso, você pode incluir o comando PATH="$HOME/bin:$PATH" em um dos arquivos de configuração.

Mas nunca, jamais inclua o diretório atual (.) no path, pois isso tornaria o sistema inseguro. Imagine o que aconteceria se alguém criasse um script chamado ls em um diretório qualquer.

Se você quiser saber onde se encontra um comando, pode usar o comando type -a nome_do_comando.

Variáveis

Como toda linguagem de programação normal, o shell possui variáveis. Em bash, as variáveis são todas strings, embora possam ser tratadas como números inteiros em algumas situações. O bash também suporta vetores (arrays), embora não os trate como objetos de primeira classe.

A sintaxe para declaração de variável é varname=valor. Não pode haver espaços antes ou depois do =. (Isto não é uma regra arbitrária; os espaços mudam o significado do comando.) Um nome de variável consiste de uma letra ou "_", possivelmente seguido de letras, números e "_". Para usar o valor de uma variável, usa-se $varname:

world=Earth
echo "Hello, $world!"

dir=/var/log
ls -la "$dir"

string="string com espaços"
echo "Eis uma $string!"

Se for necessário, pode-se usar ${varname} para separar o nome da variável de caracteres adjacentes:

fruta="laranja"
echo "Eu sabia essa com ${fruta}s..."

Existem variáveis comuns e variáveis de ambiente (environment variables). As variáveis de ambiente de um processo são herdadas por seus descendentes. Alguns programas se comportam de maneira diferente de acordo com as variáveis de ambiente que herda. Há duas maneiras de criá-las:

[Agora já podemos entender por quê os espaços são relevantes. varname = valor executa o comando varname com os dois argumentos especificados; varname= valor executa valor com a variável de ambiente varname igual à string vazia. varname =valor executa varname com o argumento especificado.]

É costume dar nomes em maiúsculas às variáveis de ambiente, mas isso não é obrigatório. Exemplos de variáveis de ambiente são HOME (diretório do usuário), PWD (diretório atual) e PATH (caminho de pesquisa de comandos).

Além das variáveis de ambiente, o shell cria automaticamente uma série de variáveis e "quase-variáveis" (parâmetros). Exemplos de variáveis do shell são: UID (id do usuário; 0=root), PS1 (string do prompt), SECONDS (número de segundos desde que o shell foi iniciado), RANDOM (retorna um número aleatório entre 0 e 32767). Exemplos de parâmetros especiais são: $$ (id do processo (pid) do shell), $? (exit status do último comando executado), $! (pid do último processo iniciado em background). Parâmetros especiais não podem ser alterados por atribuição (não se pode escrever, por exemplo, ?=42).

Há também os parâmetros correspondentes aos argumentos passados ao script pela linha de comando, $1, $2, ..., $9, ${10}, etc. $# contém o número de argumentos recebidos, $* contém todos os argumentos em uma única string, e $@ contém todos os argumentos separadamente. Veremos mais sobre isso em capítulos posteriores.

Aspas

Se digitamos ls -la foo bar, o ls tenta listar dois arquivos, "foo" e "bar". Mas e se eu quisesse listar um único arquivo chamado "foo bar"? Ou um arquivo chamado "*.txt"? Ou um arquivo chamado "$PWD"?

Para isso, usamos aspas simples ('), aspas duplas (") e a barra invertida (\). Elas fazem com que caracteres com propriedades especiais, como o espaço e o asterisco, sejam tratados como caracteres comuns. Quando isso acontece, dizemos que o caractere foi citado (quoted).

Note que é possível usar aspas duplas dentro de aspas duplas (echo "Ela disse \"foo\" e desapareceu"), mas não é possível usar aspas simples dentro de aspas simples, pois o \ perde seus poderes dentro de aspas simples.

Valores de variáveis fora de aspas duplas também são separados em palavras. Por exemplo:

opts="-la /var"
ls $opts                  # executa ls com dois argumentos, "-la" e "/var".
ls "$opts"                # executa ls com um argumento, "-la /var".

file="nome com espaços"
cat $file                 # nay!
cat "$file"               # yea!

Por isso, não use variáveis sem aspas duplas a menos que você tenha certeza absoluta de que o valor da variável nunca conterá espaços, asteriscos e outros caracteres especiais, ou que você realmente queira que o valor seja separado em palavras. Na dúvida, use aspas; isso lhe evitará muita dor de cabeça.

Pipelines

O conceito de pipe é provavelmente o conceito mais revolucionário do Unix. Ele permite que diversas ferramentas que realizam uma única tarefa sejam combinadas de maneira a realizar algo mais complexo. Exemplos:

A idéia por trás dos pipes é simples. No Unix, cada programa ao iniciar recebe já abertos três 'arquivos': a entrada padrão (stdin), a saída padrão (stdout), e a saída de erros (stderr). O pipe liga a saída padrão de um comando com a entrada padrão de outro. Os programas então são escritos para receber dados pela stdin e emitir o resultado pela stdout; tais programas são denominados filtros. Um filtro bem escrito pode ser usado em diversas situações, e você vai querer escrever muitos de seus scripts como filtros para desfrutar das vantagens das pipelines. Veremos como fazer isso mais adiante. Agora, vejamos como ler da stdin e escrever na stdout.

Entrada e saída

O comando para leitura é o read. Ele aceita diversas opções, mas a sintaxe básica é read varname. Ela lê uma linha da stdin e a salva na variável especificada. É possível ler mais de uma variável com o mesmo comando: read var1 var2; neste caso, a linha é separada em palavras e cada palavra é atribuída à variável correspondente na seqüência. Se houver mais palavras do que variáveis, a última variável fica com as palavras restantes. Se houver menos palavras do que variáveis, as variáveis restantes ficam vazias. Note que, devido à separação de palavras, espaços no começo/final da linha são perdidos; se você deseja manter esses espaços (e freqüentemente você deseja), use o comando IFS= read varname. (IFS (internal field separator) é uma variável do shell que determina quais caracteres são separadores de palavra. O comando atribui a string vazia a IFS e lê a linha.)

O principal comando de saída é o echo. Ele também aceita várias opções; a sintaxe é echo [opções] string. Uma das opções mais úteis é -n: ela omite a quebra de linha que normalmente é impressa no final do texto. Vamos testar esses comandos:

#!/bin/bash
echo -n "Qual é o seu nome? "
read nome
echo "Saudações, $nome!"

Exit status, verdadeiro e falso, execução condicional

No Unix, todo processo ao terminar retorna ao processo que o invocou um valor entre 0 e 255 chamado exit status. Programas bem comportados retornam zero se executaram corretamente e um valor diferente de zero se ocorreu algum erro durante a execução (muitas vezes o valor indica qual foi o erro encontrado).

Quando um comando é executado, o shell torna seu exit status acessível pelo parâmetro $?. Experimente o seguinte:

ls /var
echo $?             # mostra 0
ls /nao_existe
echo $?             # mostra 2

Daí deriva a noção do shell de verdadeiro ou falso: o valor 0 é considerado verdadeiro, e qualquer valor diferente de 0 é considerado falso. (Isso é o contrário da maior parte das linguagens de programação. Note, entretanto, que em geral existe apenas um tipo de sucesso, mas vários tipos de erros possíveis.) Podemos então usar o exit status de um comando como condição para a execução ou não de outros comandos. E é exatamente isto que faz o if:

if ls /var; then
    echo "/var existe."
else
    echo "/var não existe ou não é acessível..."
fi

Um comando muito útil aqui é o test: ele permite fazer diversos testes com números, strings e arquivos (use help test para ver todas as opções). O comando [ argumentos ] é equivalente a test argumentos.

if [ -e /tmp/foo ]; then
    echo "O arquivo existe."
fi

Entre os testes mais comuns estão:

Lembre-se de usar aspas duplas se existir a possibilidade de a string conter espaços. (Na dúvida, use aspas, mesmo com os testes numéricos.) Note que há operadores separados para números e strings. Note também que os operadores \< e \> precisam ser citados para evitar que o shell os interprete como um redirecionamento (que veremos mais adiante).

O comando test aceita também alguns operadores especiais:

A palavra-chave ! inverte o valor-verdade de um comando. Por exemplo:

if ! grep -q foo /etc/passwd; then
    echo "O arquivo /etc/passwd NÃO contém a palavra 'foo'."
fi

! [ TESTE ] é equivalente a [ ! TESTE ]. (Cuidado com o ! no modo interativo: ele é um caractere especial que manipula o histórico do shell. Em shell scripts, ele pode ser usado normalmente.)

Algumas vezes queremos testar várias condições de uma só vez. Para isso, temos o comando elif (abreviação de "else if"):

echo -n "Digite um nome de arquivo: "
read file
if [ -f "$file" ]; then
    echo "É um arquivo comum."
elif [ -d "$file" ]; then
    echo "É um diretório."
elif [ -e "$file" ]; then
    echo "É outro tipo de animal."
else
    echo "Isso não existe!"
fi

O exit status de um script é o exit status do último comando executado. Se você quiser encerrar o script com outro valor, use o comando exit valor. Se o comando exit é usado sem especificar um valor, o script é encerrado com o exit status do último comando executado (como se tivesse atingido o final do script). É recomendado que você faça seus scripts seguirem a convenção de retornar 0 quando a execução é bem sucedida e outro valor quando ocorrer um erro.

NOTA: É útil configurar o prompt para mostrar o exit status do último comando executado. Você pode fazer isso alterando o prompt (variável PS1) em um dos arquivos de configuração do shell para mostrar o conteúdo do parâmetro $?, ou incluindo a seguinte linha no final de um dos arquivos:

[[ $PS1 == *'$?'* ]] || PS1="${PS1//\\\$/(\$?)\\\$}"

(Embora essa linha possa parecer esotérica por enquanto, veremos que ela faz sentido mais adiante. Você pode copiá-la e colá-la em um terminal para ver o que acontece.)

*** TO-DO ********
- conectivos
- globbing
- parameter substitution
- case/select
- while
- redirecionamento
- command substitution
- escrevendo filtros
- debugando scripts
- funções
- locales
- processando opções da linha de comando
- manipulando arquivos
- find, xargs, printf, stat
- grep, sed e outras pessoas
- locking
- scripts CGI

Copyright © 2010 Vítor Bujés Ubatuba De Araújo
O conteúdo deste site, a menos que de outra forma especificado, pode ser utilizado livremente, com ou sem alterações, desde que seja mencionado o autor, preferivelmente com a URL do documento original.