Canais via SatéliteApontador de SatélitesTeste

Buscar Conteúdo

Links Patrocinados

Bash em Exemplos - Parte 1

Programação Fundamental no Bourne again shell (bash)

Daniel Robbins
President and CEO, Gentoo Technologies, Inc.
Março de 2000

Ao aprender como programar na linguagem de script bash, sua interação diária com o Linux se tornará mais divertida e produtiva, e você estará apto a montar programas a partir dos componentes padrão do Unix (como pipelines e redireção) que você já conhece e adora. Nesta série de três partes, Daniel Robbins irá ensinar a você como programa no bash através de exemplos. Ele irá cobrir o básico (o que torna esta série excelente para iniciantes) e irá apresentar funcionalidades mais avançadas conforme a série avançar.

Conteúdo

Você pode estar se perguntando por que iria aprender programação Bash. Bem, aqui tem alguns motivos:

Você já está rodando ele

Se você checar, você provavelmente vai descobrir que está rodando o bash neste exato momento. Mesmo se você trocou o seu shell default, provavelmente o bash ainda está sendo executado em algum lugar em seu sistema, por que ele é o shell padrão do Linux e é usado para uma variedade de finalidades. Como o bash já está sendo executado, qualquer scripts bash adicionais que você executar são inerentemente eficientes em termos de uso de memória, por que eles compartilham a memória com qualquer processo bash em execução. Por que carregar um interpretador de 500K, se você já está executando algo que irá fazer o serviço, e fazer bem?

Você já está usando ele

Não somente você já está rodando o bash, mas você está de fato interagindo com o bash no seu dia-a-dia. Ele está sempre ali, então faz sentido aprender como usá-lo em todo seu potencial. Fazendo isto você irá tornar sua experiência com o bash mais divertida e produtiva, mas por que você deveria aprender programação no bash? Fácil, por que você já pensa em termos de execução de comandos, CPiar arquivos, fazer pipes e redireção de saída. Você não deveria aprender uma linguagem que permite você usar e construir em cima destas construções poderosas que você já sabe como usar? O shell libera o potencial de um sistema Unix, e o bash é o shell do Linux. É a cola de alto-nível entre você e a máquina. Faça seu conhecimento do bash crescer, e você automaticamente irá aumentar sua produtividade sob o Linux e UNIX -- é assim simples.

Confusão com o Bash

Aprender o bash do jeito errado pode ser um processo que cria bastante confusão. Muitos novatos teclam "man bash" para ver a página man do bash, somente para serem confrontados com uma descrição técnica e concisa da funcionalidade do shell. Outros teclam "info bash" (para ver a documentação GNU info), fazendo com que ou a página man seja reapresentada, ou (se eles tem sorte) a documentação levemente mais amigável do info aparecer.

Enquanto isto pode ser um pouco desapontador para novatos, a documentação padrão do bash não pode ser tudo para todos, e agradar a todos que já estão familiarizados com a programação shell em geral. Definitivamente existe muita informação técnica excelente na página man, mas sua utilidade para novatos é limitada.

É aqui que esta série entra. Nela, irei mostrar a você como realmente usar os componentes de programação do bash, de forma que você vai poder escrever seus próprios scripts. Ao invés de descrições técnicas, irei apresentar explicações claras, de forma que você não irá somente saber o que algo faz, mas quando você realmente vai usar este "algo". No final desta série de três partes, você vai saber escrever seus próprios scripts bash intrincados, e estar capacitado a confortavelmente usar o bash e suplementar seu conhecimento pela leitura (e entendendo!) a documentação padrão do bash. Vamos começar.

Variáveis de ambiente

Sob o bash e quase todos os outros shells, o usuário pode definir variáveis de ambiente, que são armazenas internamente como strings ASCII. Uma das coisas mais úteis sobre as variáveis de ambiente é que elas são uma parte padrão do modelo de processos do UNIX. Isto significa que as variáveis de ambiente não são exclusivas aos shell scripts, mas podem ser utilizadas por programas compilados também. Quando "exportamos" uma variável ambiente sob o bash, qualquer programa subseqüente que seja executado pode ler nossa configuração, seja ele um script shell ou não. Um bom exemplo é o comando vipw, que normalmente permite que o root edite o arquivo de senhas do sistema. Configurando a variável ambiente EDITOR com o nome do seu editor de texto favorito, você está configurando o vipw a usar este editor ao invés do vi, uma coisa bastante útil se você está acostumado com o xemacs e não gosta do vi.

A maneira padrão de definir uma variável de ambiente sob o bash é:

$ myvar='This is my environment variable!'

O comando acima define uma variável de ambiente chamada "myvar" e contém a string "This is my environment variable!". Existem várias coisas a serem notadas acima: primeiro, não há espaços em nenhum lado do sinal "="; qualquer espaço irá resultar em um erro (tente e veja por si mesmo). A segunda coisa a ser notada é que enquanto podemos dispensar as plicas se estamos definindo uma palavra, elas são necessárias quando o valor da variável de ambiente é mais que uma palavra (contém espaços ou tabulações).

Sobre Quoting

Para informações extremamente detalhadas sobre como as cotas (aspas ou plicas) podem ser utilizadas no bash, você deve olhar na seção "QUOTING", na página man do bash. A existência de sequências de caracteres especiais que são "expandidas" (substituídas) com outros valores complica a forma que as strings são tratadas no bash. Iremos ver nesta série somente as funcionalidade de quoting mais utilizadas.

Em terceiro lugar, enquanto podemos utilizar aspas duplas ao invés de aspas simples, fazer isto no exemplo acima teria causado um erro. Por quê? Por que usando aspas simples (plicas), nós desabilitamos uma funcionalidade do bash chamada de expansão, em que caracteres especiais e seqüências de caracteres são substituídas por valoers. Por exemplo, o caracter "!" é o caracter de expansão de histórico, que o bash normalmente substitui com um comando previamente usado (não iremos cobrir exçansão de histórico nesta série de artigos, por que não é frequentemente usada na programação do bash. Para mais informação sobre esta funcionalidade, veja a seção "HISTORY EXPANSION" na página man do bash). Enquanto esta funcionalidade tipo macro pode ser útil, agora queremos um ponto de exclamação literal no fim de nossa variável de ambiente, ao invés de uma macro.

Agora, vamos dar uma olhada em como estas variáveis de ambiente são utilizadas. Aqui temos um exemplo:

$ echo $myvar
This is my environment variable!

Quando precedemos o nome da variável de ambiente com um $, obrigamos o bash a substituir o mesmo com o valor de myvar. Na terminologia do bash, isto é chamado de "expansão de variável". Mas, e se tentarmos o seguinte:

$ echo foo$myvarbar
foo

O que queriamos era que aparecesse "fooThis is my environment variable!bar", mas não foi o que aconteceu. O que houve de errado? Em poucas palavras, a facilidade de expansão de variáveis do bash ficou confusa. Ela não conseguia saber se queríamos expandir a variável $m, $my, $myvar, $myvarbar, etc. Como podemos informar ao bash de forma mais explícita e clara a qual variáveil estamos nos referindo? Tente isto:

$ echo foo${myvar}bar
fooThis is my environment variable!bar

Como você pode ver, podemos colocar o nome da variável de ambiente entre chaves quando ela não está claramente separada do texto que a cerca. Enquanto $myvar é mais rápido de escrever e irá funcionar a maior parte do tempo, ${myvar} pode ser tratada corretamente na maioria das situações. Por outro lado, ambos fazem a mesma coisa, e você irá encontrar as duas formas de expansão de variável no resto desta série. Você vai querer lembrar de usar a forma mais explícita com chaves quando sua variável de ambiente não está isolada do texto que a cerca por brancos (espaços ou tabulações).

Lembre que mencionamos que podemos "exportar" variáveis. Quando exportamos uma variável ambiente, ela automaticamente fica disponível no ambiente de qualquer scrip ou executável que seja executado posteriormente. Os shell scripts podem acessar a variável de ambiente usando o suporte a variáveis de ambiente interno do shell, enquanto programas C podem usar a chamada de função getenv(). A seguir há um exemplo de código C que você pode copiar e compilar -- Ele permite que se entenda as variáveis de ambiente da perspectiva do C:

myvar.c -- Um exemplo de programa C que usa variáveis de ambiente

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char *myenvvar = getenv("EDITOR");
    printf( "The editor environment variable is set to %s\n", myenvvar );
}

Salve o trecho acima em um arquivo chamado myenv.c e então compile o mesmo com o comando:

$ gcc myenv.c -o myenv

Agora, você tem um executável em seu diretório que, quando executado, irá escrever o valor da variável de ambiente EDITOR, se houver uma. Isto é o que acontece quando executo este programa no meu computador:

$ ./myenv
The editor environment variable is set to (null)	

Hmm. Como a variável não está configurada para nenhum valor, o programa C recebe uma string nula. Vamos tentar configurar a variável para algum valor específico:

$ EDITOR=xemacs
$ ./myenv
The editor environment variable is set to (null)	

Você esperava que myenv imprimisse o valor "xemacs", mas isto não aconteceu, por que não exportamos a variável de ambiente EDITOR. Desta vez, vamos fazer isto funcionar:

$ export EDITOR
$ ./myenv
The editor environment variable is set to xemacs

Agora você viu com seus próprios olhos que outro processo (no caso nosso programa em C) não pode ver a variável de ambiente até que ela seja exportada. Se você quiser, pode definir e exportar a variável usando uma linha:

$ export EDITOR=xemacs

Funciona exatamente da mesma forma que a versão com duas linhas. Agora é uma boa hora para mostrar como apagar uma variável de ambiente usando o comando unset:

$ unset EDITOR
$ ./myenv
The editor environment variable is set to (null)	

Cortando strings

dirname e basename

Note que tanto o comando dirname quanto o comando basename não examinam nenhum diretório ou arquivo no disco, eles são apenas comandos de manipulação de string.

Cortar strings -- ou seja, dividir a string original em pedaços separados, menores -- é uma das tarefas executadas diariamente pelos shell scripts. Muitas vezes, os shell scripts precisam receber um nome de caminho completo, e encontrar o nome do arquivo ou do diretório. Enquanto isto é possível (e divertido!) de fazer no bash, o executável padrão Unix basename faz isto muito bem:

$ basename /usr/local/share/doc/foo/foo.txt
foo.txt
$ basename /usr/home/drobbins
drobbins

O basename é uma ferramenta bastante útil para cortar strings. Sua contraparte, chamada dirname, retorna a "outra" parte do caminho que o basename jogou fora:

$ dirname /usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$ dirname /usr/home/drobbins
/usr/home

Substituição de comandos

Uma coisa bastante útil para saber é como criar uma variável de ambiente que contém o resultado de um comando executável. Isto é fácil de fazer:

$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo

O que fizemos acima é chamado de "substituição de comando". Várias coisas devem ser notadas neste exemplo. Na primeira linha, simplesmente colocamos o comando que queríamos executar em back quotes (crase ou acento grave). Estas não são aspas simples ou plicas padrão, mas vem da tecla que normalmente fica acima da tecla Tab (nos teclados US). Podemos fazer exatamente a mesma coisa com a sintaxe alternativa de substituição de comando do bash:

$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)
$ echo $MYDIR
/usr/local/share/doc/foo

Como você pode ver, o bash fornece várias formas de se fazer exatamente a mesma coisa. Usando substituição de comandos, podemos colocar qualquer comando ou pipeline de comandos entre `` ou $(), e atribuir o resultado a uma variável de ambiente. Muito útil! Segue um exemplo de como usar um pipeline com a substituição de comando:

bash-2.03$ MYFILES=$(ls /etc | grep pa)
bash-2.03$ echo $MYFILES
pam.d passwd

Cortando strings como um profissional

Enquanto basename e dirname são grandes ferramentas, existem horas que precisamos executar cortes em strings mais avançados que as manipulações de nome de diretório. Quando precisamos de cortes mais avançados, podemos usar a funcionalidade avançada de expansão de variáveis do bash. Já usamos a expansão de variável padrão, que é o ${MYVAR}. Mas o bash pode executar alguns cortes bastante úteis. Veja estes exemplos:

$ MYVAR=foodforthought.jpg
$ echo ${MYVAR##*fo}
rthought.jpg
$ echo ${MYVAR#*fo}
odforthought.jpg

No primeiro exemplo, escrevemos ${MYVAR##*fo}. O que exatamente significa isto? Basicamente, dentro do ${} nós escrevemos o nome de uma varíavel de ambiente, dois ##s, e uma expressão coringa ("*fo"). Então o bash pegou MYVAR, encontrou a maior substring do início da string "foodforthought.jpg" que combina com a expressão "*fo", e cortou ela do início da string. Isto é um pouco difícil de entender a princípio, então para ter um entendimento de como esta opção especial "##" funciona, vamos seguir passo a passo como o bash completou esta expansão. Primeiro, ele começou procurando por substrings no início de "foodforthought.jpg" que combinam com a expressão "*fo". Aqui estão as substrings que ele checou:

f
fo                Combina com *fo
foo
food
foodf
foodfo            Combina com *fo
foodfor
foodfort
foodforth
foodfortho
foodforthou
foodforthoug
foodforthough
foodforthought
foodforthought.
foodforthought.j
foodforthought.jp
foodforthought.jpg

Após procurar por combinações na string, você pode ver que o bash encontrou duas combinações. Ele seleciona a maior combinação, remove ela do início da string original, e retorna o resultado.

A segunda forma de expansão de variável apresentada acima parece ser idêntica à primeira, exceto que ela usa somente um "#" -- e o bash faz um processo quase idêntico. Ele checa o mesmo conjunto de substrings que o nosso primeiro exemplo fez, exceto que ele remove a combinação menor de nossa string original, e retorna o resultado. Assim, logo que ele encontra a substring "fo", ele a remove de nossa string e retorna "odforthought.jpg".

Isto pode parecer extremamente críptico, de forma que eu vou mostrar uma forma simples de lembrar esta funcionalidade. Quando estiver procurando pela maior combinação, use ## (por que ## é maior que #). Quando estiver procurando pela combinação mais curta, use #. Como vemos, não é difícil de lembrar! Espere, como você vai lembrar que o caracter "#" é usado para remover do "início" de uma string? Simples! você deve notar que em um teclado US, o Shift-4 é o "$", que é o caracter de expansão de variável. No teclado, imediatamente na esquerda do "$" está o "#". Assim, voc~e pode ver que o "#" está "no início" de "$", e assim (de acordo com nosso mneumônico), o "#" remove caracteres do início da string. Você pode estar se perguntando como removemos caracteres do fim da string. Se você "chutar" que usamos o caracter imediatamente à direita do "$" no teclado US ("%"), você acertou! Aqui tem alguns rápidos exemplso de como cortar porções finais de strings:

$ MYFOO="chickensoup.tar.gz"
$ echo ${MYFOO%%.*}
chickensoup
$ echo ${MYFOO%.*}
chickensoup.tar

Como você pode ver, as opções de expansão de variável % e %% funcionam de forma idêntica ao # e ##, exceto que removem a substring que combina no fim da string. Note que vicê não precisa usar o "*" se você quer remover uma substring específica do fim:

$ MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken

Neste exemplo, não importa se usamos o "%%" ou o "%", uma vez que somente uma combinação é possível. E lembre-se, se você esquecer se deve usar o "#" ou o "%", olhe para as teclas 3, 4 e 5 no seu teclado e descubra por si.

Podemos usar outra forma de expansão de variável para selecionar uma substring específica, baseado em um deslocamento e comprmento de caracteres específicos. Tente escrever as seguintes linhas no bash:

$ EXCLAIM=cowabunga
$ echo ${EXCLAIM:0:3}
cow
$ echo ${EXCLAIM:3:7}
abunga

Esta forma de cortar strings é bastante útil. Basta especificar o carcter a partir do qual iniciar e o comprimento da substring, todos separados por dois pontos.

Aplicando o corte de strings

Agora que aprendemos tudo sobre cortar strings, vamos escrever um pequeno script shell simples. Nosso script irá aceitar um nome de arquivo como argumento, e vai imprimir se ele parece ser um tarball. Para determinar se é um tarball, ele vai procurar pelo padrão ".tar" no fim do arquivo. Aqui está o script:

mytar.sh -- um script de exemplo

#!/bin/bash

if [ "${1##*.}" = "tar" ]
then
        echo This appears to be a tarball.
else
        echo At first glance, this does not appear to be a tarball.
fi

Para executar este script, entre o mesmo em um arquivo chamado mytar.sh, e execute o comando "chmod 755 mytar.sh" para torná-lo executável. A seguir, tente o mesmo com um tarball, conforme segue:

$ ./mytar.sh thisfile.tar
This appears to be a tarball.
$ ./mytar.sh thatfile.gz
At first glance, this does not appear to be a tarball.

OK, funciona, mas não é muito funcional. Antes que o tornemos um pouco mais útil, vamos dar uma olhada na declaração "if" acima. Nela, temos uma expressão booleana. No bash, o operador de comparação "=" verifica igualdade de strings. No bash, todas expressões booleanas ficam entre colchetes. Mas o que a expressão booleana está testando? Vamos dar uma olhada no lado esquerdo. De acordo com o que aprendemos sobre corte de strings, "${##1*.}" irá remover a combinação mais longa de "*." do início da string contida na variável de ambiente "1", retornando o resultado. Isto fará com que tudo que esteja após o último "." no arquivo será retornado. Obviamente, se o arquivo termina em ".tar", iremos obter "tar" como resultado, e a condição será verdadeira.

Você pode estar se perguntando o que a variável de ambiente "1" é, em primeiro lugar. Muito simples -- $1 é o primeiro argumento da linha de comando do script, $2 é o segundo, etc. OK, agora que já revisamos a função, podemos dar nossa primeira olhada nas declarações "if".

Declaração if

Como a maioria das linguagens, o bash possu sua própria forma de condicional. Quando estiver usando condicionais, use a forma acima, ou seja, coloque o "if" e o "then" em linhas separadas, e coloque o "else" e o "fi" obrigatório em alinhamento horizontal com o "if". Isto faz com que o código seja mais fácil de ler e depurar. Alpem da forma "if, else", existem várias outras formas de declaração "if":

if [ condition ]
then
        action
fi

Este "if" somente executa uma ação se condition for verdadeiro, senão ele não executa ação alguma e continua executando quaisquer linhas que seguem o "fi".

if [ condition ]
then
	action
elif [ condition2 ]
then
	action2
.
.
.
elif [conditionN ]
then
	actionN
else
	actionx
fi

A forma "elif" acima irá testar cada condição consecutivamente e executar a ação correspondente à primeira condição verdadeira. Se nenhuma das condições for verdadeira, ele irá executar a ação "else", se houver uma presente, e então continuará executando as linhas que seguem a declaração "if, elif, else".

A próxima vez

Agora que cobrimos a funcionalidade mais básica do bash, é hora de aproveitar o embalo e preparar-se para escrever alguns scripts reais. No próximo artigo, irei cobrir construções de repetição, funções, espaços de nomes, e outros tópicos essenciais. Então estaremos prontos para escrever alguns scripts mais complicados. No terceiro artigo, iremos nos concentrar quase exclusivamente em um script bastante complexo e funções, bem como várias opções de projeto de scripts bash. Até lá!

Recursos

Sobre o autor

Residindo em Albuquerque, Novo México, Daniel Robbins é o Chief Architect do Gentoo Project, CEO da Gentoo Technologies, Inc., o mentor do Linux Advanced Multimedia Project (LAMP), e um autor-contribuinte dos livros da Macmillan Caldera OpenLinux Unleashed, SuSE Linux Unleashed, e Samba Unleashed. Daniel está envolvido com computadores de alguma forma desde o segundo grau, quando ele foi exposto pela primeira vez à linguagem de programação Logo, bem como a uma dose podencialmente perigosa de Pac Man. Isto provavelmente explica por que ele tem servido desde então como Lead Graphic Artist na SONY Eletronic Publishing/Psygnosis. Daniel gosta de passar o tempo com sua esposa, Mary, que está esperando uma criança para esta primavera. Ele pode ser encontrado no email drobbins@gentoo.org.


Bash em Exemplos - Parte 2

Mais fundamentos de programação bash

Daniel Robbins
President and CEO, Gentoo Technologies, Inc.
Abril de 2000

Em seu artigo introdutório sobre o bash, Daniel Robbins guia o leitor para mais alguns elementos básicos da linguagen de script do bash, e as razões para usar o bash. Neste artigo, o segundo da série, Daniel continua de onde parou, e examina mais construções básicas do bash, como declarações condicionais, laços, e mais.

Conteúdo

Vamos começar com uma breve dica sobre tratamento de argumentos de linha de comando, e então vamos ver algumas construções básicas do bash.

Aceitando argumentos

No programa de exemplo no artigo introdutório, usamos a variável de ambiente "1", que se refere ao primeiro argumento da linha de comando. de forma similar, você pode usar "2", "3", etc. para se referir ao segundo e terceiro argumentos passados ao seu script. Veja um exemplo:

#!/usr/bin/env bash

echo name of script is $0
echo first argument is $1
echo second argument is $2
echo seventeenth argument is $17
echo number of arguments is $#

O exemplo é auto-explicativo, exceto por dois pequenos detalhes. Primeiro, "$0" será expandido para o nome do script, conforme chamado na linha de comando, e "$#" será expandido para o número de argumentos passados ao script. Brinque com o script acima, passando diferentes tipos de argumentos de linha de comando para entender como ele funciona.

Às vezes, é interessante referir-se a todos os argumentos de linha de comando de uma vez. Para isto, o bash tem a variável "$@", que é expandida para todos os parâmetros da linha de comando separados por espaços. Veremos um exemplo do seu uso quando formos olhar os laços "for", um pouco mais adiante neste artigo.

Construções de programação Bash

Se você já programou em uma linguagem procedural, como C, Pascal, Python, ou Perl, então você já está familiarizado com construções de programação padrão como as declarações "if", laços "for", e outros. O bash possui suas próprias versões destas construções padrão. Nas próximas seções, eu irei introduzir várias construções do bash e demonstrar as diferenças entre estas construções e outras que você já deve estar familiarizado, de outras linguagens de programação. Se você não tem muita experiência com programação, não se preocupe. Eu irei incluir informações suficientes, e exemplo, de forma que você possa seguir o texto.

Amor condicional

Se você já programou algum código relacionado a arquivos em C, você sabe que é necessário bastante esforço para ver se um arquivo em particular é mais novo que outro. Isto acontece por que duas chamadas e estruturas stat() são necessárias antes que a comparação possa ser feita. Não é grande coisa, mas se você está executando muitas operações de arquivos, não leva muito tempo para descobrir que o C não é muito apropriado para fazer scripts de operações baseadas em arquivos. Uma das coisas legais sobre o bash é que ele possui operadores de comparação de arquivos internos, de forma que é fácil escrever uma declaração "if" que pergunta "$myvar é maior que 4?" assim como perguntar "o arquivo /tmp/myfile pode ser lido?".

A tabela abaixo lista os operadores de comparação do bash mais frequentemente usados. Os exemplos na tabela mostram como usar cada opção. O exemplo foi feito para ser colocado logo após o "if", como no exemplo abaixo:

if [ -z "$myvar" ]
then
	echo "myvar is not defined"
fi
Operador Descrição Exemplo
Operadores de comparação de arquivos
-e filename verdadeiro se filename existe [ -e /var/log/syslog ]
-d filename verdadeiro se filename é um diretório [ -d /tmp/mydir ]
-f filename verdadeiro se filename é um arquivo regular [ -f /usr/bin/grep ]
-L filename verdadeiro se filename é um link simbólico [ -L /usr/bin/grep ]
-r filename verdadeiro se filename pode ser lido [ -r /var/log/syslog ]
-w filename verdadeiro se filename pode ser sobrescrito/gravado [ -w /var/mytmp.txt ]
-x filename verdadeiro se filename pode ser executado [ -x /usr/bin/grep ]
filename1 -nt filename2 verdadeiro se filename1 é mais novo que filename2 [ /tmp/install/etc/services -nt /etc/services ]
filename1 -ot filename2 verdadeiro se filename1 é mais velho que filename2 [ /boot/bzImae -ot arch/i386/boot/bzImage ]
Operadores de comparação de string (note o uso das aspas, uma boa forma de se proteger contra espaços em branco corrompendo seu código)
-z string verdadeiro se string tem comprimento igual a zero [ -z "$myvar" ]
-n string veradeiro se string em comprimento diferente de zero [ -n "$myvar" ]
string1 = string2 verdadeiro se string1 é igual a string2 [ "$myvar" = "one two three" ]
string1 != string2 verdadeiro se string1 não for igual a string2 [ "$myvar" != "one two three" ]
Operadores de comparação aritmética
num1 -eq num2 igual [ 3 -eq $mynum ]
num1 -ne num2 diferente [ 3 -ne $mynum ]
num1 -lt num2 menor que [ 3 -lt $mynum ]
num1 -le num2 menor que ou igual [ 3 -le $mynum ]
num1 -gt num2 maior que [ 3 -gt $mynum ]
num1 -ge num2 maior que ou igual [ 3 -ge $mynum ]

Uma coisa interessante sobre operadores condicionais é que você pode geralmenet escolher se quer executar uma comparação aritmética ou uma comparação de strings. Por exemplo, os dois trechos de código a seguir funcionam de forma idêntica:

if [ $myvar -eq 3 ]
then
	echo "myvar equals 3"
fi
if [ "$myvar" = "3" ]
then
	echo "myvar equals 3"
fi

Entretanto, a sua implementação é um pouco diferente -- o primeiro utiliza operadores de comparação aritmética, e o segundo utiliza operadores de comparação de strings. A outra diferença (além do -eq e =) é o uso de aspas ao redor da variável de ambiente e do 3 no segundo exemplo. Isto informa ao bash que estamos comparando duas strings, ao invés de dois números.

Problemas com comparações de strings

A maior parte do tempo, mesmo que voc~e possa omitir o uso de aspas quando está usando operadores de string, isto não é uma boa idéia. Por quê? Por que seu código irá funcionar perfeitamente, a menos que uma variável de ambiente contenha um espaço ou uma tabulação, o que fará com que o bash fique confuso. Aqui temos um exemplo:

if [ $myvar = "foo bar oni" ]
then
	echo "yes"
fi

No exemplo acima, se myvar for igual a foo, o código irá funcionar conforme esperado, e não irá escrever nada. Entretanto, se myvar for igual a "foo bar oni", o código irá falhar com o seguinte erro:

[: too many arguments

Neste caso, o espaço em branco entre as três palavras confunde o bash. É como se você tivesse escrito a seguinte condição:

[ foo bar oni = "foo bar oni" ]

Como a variável de ambiente não foi colocada entre aspas, o bash pensa que você informou muitos argumentos entre os colchetes. Isto é muito importante entender: se você tem o hábito de colocar argumentos string e variáveis de ambiente entre aspas, você vai eliminar muitos erros. Veja aqui como a comparação com "foo bar oni" deveria ter sido escrita:

if [ "$myvar" = "foo bar oni" ]
then
	echo "yes"
fi

Mais sobre quoting

Se você quer que suas variáveis de ambiente sejam expandidas, você deve colocar elas entre aspas, ao invés de apóstrofos ou plicas. Apóstrofos desabilitam expansão de variáveis (bem como expansão de histórico).

O código acima irá funcionar conforme o esperado e não irá criar nenhuma surpresa desagradável.

Construções de laço

OK. já cobrimos os testes condicionais, agora é hora de explorar as construções de laço do bash. Começaremos com o laço padrão "for". Aqui temos um exemplo básico:

#!/usr/bin/env bash

for x in one two three four
do
	echo number $x
done

Saída

number one
number two
number three
number four

O que exatamente aconteceu? A parte "for x" do nosso laço "for" definiu uma nova variável de ambiente (também chamada de variável de controle de laço), chamada x, que recebeu sucessivamente os valores "one", "two", "three", e "four". Depois de cada atribuição, o corpo do laço (o código entre o "do" ... "done") foi executado uma vez. No corpo, nos referimos à variável de controle de laço x usando a sintaxe padrão de expansão de variável, como qualquer outra variável de ambiente. Note também que o laço "for" sempre aceita um tipo de lista de palavras depois da declaração "in". Neste caso, especificamos quatro palavras do inglês. Além disto, a lista de palavras pode também se referir a arquivos no disco ou até mesmo máscara de arquivos. Dê uma boa olhada no seguinet exemplo para ver como as máscara de arquivos podem ser usadas:

#!/usr/bin/env bash

for myvile in /etc/r*
do
	if [ -d "$myfile" ]
	then
		echo "$myfile (dir)"
	else
		echo "$myfile"
	fi
done

Saída

/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc

O código acima fez um laço sobre cada arquivo em /etc que começa com "r". Para isto, o bash pega nossa máscara /etc/r* e expande ela, substituindo-a pela string /etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc antes de executar o laço. Dentro do laço, o operador condicional "-d" foi usado para executar duas diferentes ações, dependendo se myfile é um diretório ou não. Se for um diretório, um "(dir)" foi acrescentado à linha da saída.

Podemos também utilizar múltiplas máscaras e mesmo variáveis de ambiente na lista de palavras:

for x in /etc/r??? var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
	cp $x /mnt/mydir
done

O bash irá executar expansão de máscara e de variáveis em todos os locais corretos, e potencialmente criar uma lista de palavras bastante longa.

Enquanto todos nossos exemplos de expansão de máscaras usaram caminhos absolutos, você pode usar também caminhos relativos, como no exemplo abaixo:

fr x in ../* mystuff/*
do
	echo $x is a silly file
done

No exemplo acima, o bash executou a expansão da máscara relativa ao diretório de trabalho atual, exatamente como quando você usa caminhos relativos na linha de comando. Brinque um pouco com expansão de máscaras. Você irá perceber que se usa caminhos absolutos em sua máscara, o bash irá expandir a máscara para uma lista de caminhos absolutos. Caso contrário, o bash irá usar caminhos relativos na lista de palavras gerada. Se você simplesmente se referir a arquivos no diretório de trabalho atual (por exmeplo, se você escrever "for x in *"), a lista de arquivos resultantes não receberá nenhum prefixo com informação de diretório. Lembre que as informações de diretório ou caminho podem ser cortadas usando o programa "basename", conforme o exemplo abaixo:

for x in /var/log/*
do
	echo `basename $x` is a file living in /var/log
done

Obviamente, geralmente é útil executar laços que operam nos argumentos de linha de comando do script. Aqui temos um exemplo de como usar a variável "$@", introduzida no início deste artigo:

#!/usr/bin/env bash

for thing in "$@"
do
	echo you typed ${thing}.
done

saída

$ allargs hello there you silly
you typed hello.
you typed there.
you typed you.
you typed silly.

Declarações case

As declarações case são outras construções condicionais que são úteis. Aqui temos um trecho:

case "${x##*.}" in
	gz)
		gzunpack ${SROOT}/${x}
		;;
	bz2)
		bz2unpack ${SROOT}/${x}
		;;
	*)
		echo "Archive format not recognizes."
		exit
		;;
esac

No trecho acima, o bash primeiro expande "${x##*.}". No código "$x" é o nome de um arquivo, e "${x##.*}" tem o efeito de cortar todo o texto exceto o que segue o último ponto no nome do arquivo. Então, o bash compara a string resultante contra os valores listados na esquerda dos ")"s. Neste caso, "${x##.*}" é comparado contra "gz", depois com "bz2", e, finalmente, "*". Se "${x##.*}" combinar ocm qualquer destas strings ou padrões, as linhas que seguem imediatamente o ")" são executadas, até o ";;", ponto no qual o bash continua a executar as linhas após o "esac". Se nenhum padrão ou string combina, nenhuma linha de código é executada. Entretanto, neste trecho de código em particular, pelo menos um bloco de código será executado, por que o padrão "*" irá combinar com qualquer coisa que não tiver combinado com "gz" ou "bz2".

Funções e namespaces

No bash, você pdoe até mesmo definir funções, de forma semelhante à usada por outras linguagens procedurais, como Pascal e C. No bash, as funções podem até mesmo aceitar argumentos, usando um sistema bastante similar à forma que scripts aceitam argumentos de linha de comando. Vamos dar uma olhada em uma definição simples de função e seguiremos daí:

tarview() {
	echo -n "Displaying contents of $1 "
	if [ ${1##*.} = tar ]
	then
		echo "(uncompressed tar)"
		tar tvf $1
	elif [ ${1##*.} = gz ]
	then
		echo "(gzip-compressed tar)"
		tar tzvf $1
	elif [ ${1##*.} = bz2 ]
		echo "(bzip2-compressed tar)"
		cat $1 | bzip2 -d | tar tvf -
	fi
}

Outro caso

O código acima poderia ter sido escrito usando uma declaração "case". Você consegue descobrir como?

Acima, definimos uma função chamada "tarview", que aceita um argumento, um tarball de algum tipo. Quando a função é executada, ela identifica que tipo de tarball o argumento é (não compactado, gzipado, ou bzip2-ado), escreve uma mensagem informativa de uma linha, e então mostra o conteúdo do tarball. Aqui está como a função acima deveria ser chamada (seja de um script ou de uma linha de comando, após ter sido digitada, colada, ou "sourced"):

$ tarview shorten.tar.gz
Displaying contents of shorten.tar.gz (gzip-compressed tar)
drwxr-xr-x ajr/abbot         0 1999-02-27 16:17 shorten-2.3a/
-rwxr-xr-x ajr/abbot      1143 1999-02-27 16:17 shorten-2.3a/Makefile
-rwxr-xr-x ajr/abbot      1199 1999-02-27 16:17 shorten-2.3a/INSTALL
-rwxr-xr-x ajr/abbot       839 1999-02-27 16:17 shorten-2.3a/LICENSE
...

Use-as interativamente

Não esqueça que funções, como a função acima, podem ser colcadas em seu ~/.bashrc ou ~/.bash_profile, de forma que estejam disponíveis para uso sempre que você estiver no bash.

Como você pode ver, argumentos podem ser referenciados dentro de definições de funções usando o mesmo mecanismo usado para referenciar argumentos de linha de comando. A única coisa que pode não funcionar completamete como esperado é a variável "$0", que será expandida para a string "bash" (se você está executando a função a partir do shell, interativamente), ou para o nome do script do qual a função foi chamada.

Namespace

Com certa freqüência, você vai precisar criar variáveis de ambiente dentro de uma funão. Mesmo que possível, existe uma tecnicalidade que você deve conhecer. Na maior parte das linguagens compiladas (como o C), quando você cria uma variável dentro de uma função, ela é colocada em um namespace local separado. Assim, se você define uma função em C chamada myfunction, e define nela uma variável chamada "x", qualquer variável global (fora da função) que seja chamada de "x" não será afetada por esta, eliminando efeitos colaterais.

Enquanto isto é verdadeiro no C, o mesmo não ocorre no bash. No bash, se você criar uma variável de ambiente dentro de uma função, ela é acrescentada ao namespace global. Isto significa que irá sobrescrever quaqleur variável global fora da função, e irá continuar a existir mesmo depois que a função encerrar:

#!/usr/bin/env bash

myvar="hello"

myfunc() {

	myvar="one two three"
	for x in $myvar
	do
		echo $x
	done
}

myfunc

echo $myvar $x

Quando este script é executado, ele produz a saída "one two three three", mosgrando como "$myvar" definido na função sobrescreveu a variável global "$myvar", e como a variável de controle de laço "$x" continuou a existir mesmo após a função terminar (e que também teria sobrescrito qualquer variável global "$x", se alguma tivesse sido definida).

Neste exemplo simples, o bug é fácil de ser encontrado e compensado pelo uso de nomes de variáveis alternativos. Entretanto, esta não é a abordagem correta. A melhor forma de resolver este problema é evitar a possível sobrescrita de variáveis globais em primeiro lugar, usando o comando "local". Quando usamos "local" para criar variáveis em uma função, elas serão mantidas em um namespace local, e não irão sobrescrever quaisquer variáveis globais. Aqui está como implementar o código acima de forma que nenhuma variável global seja sobrescrita:

#!/usr/bin/env bash

myvar="hello"

myfunc() {
	local x
	local myvar="one two three"
	for x in $myvar
	do
		echo $x
	done
}

myfunc

echo $myvar $x

Esta função irá dar a saída "hello" -- a variável global "$myvar" não será sobrescrita, e "$x" não existe fora de myfunc. Na primeira linha da função, criamos x, uma variável local que será usada mais tarde, enquanto no segundo exemplo (local myvar="one two three"), criamos uma uma variável local myvar e atribuímos um valor à mesma. A primeira forma é útil para manter variáveis de controle de laço locais, uma vez que não temos como escrever "for local x in $myvar". Esta função não irá sobrescrever nenhuma variável global, e você é encorajado a fazer todas as suas funções desta forma. A única vez que você não deve utilizar "local" é quando você precisa modificar explicitamente uma variável global.

Juntando tudo

Agora que já cobrimos a funcionalidade mais essencial do bash, é hora de ver como desenvolver uma aplicação completa baseada no bash. No próximo artigo, fazemos isto. Até lá!

Recursos

Sobre o autor

Residindo em Albuquerque, Novo México, Daniel Robbins é o Chief Architect do Gentoo Project, CEO da Gentoo Technologies, Inc., o mentor do Linux Advanced Multimedia Project (LAMP), e um autor-contribuinte dos livros da Macmillan Caldera OpenLinux Unleashed, SuSE Linux Unleashed, e Samba Unleashed. Daniel está envolvido com computadores de alguma forma desde o segundo grau, quando ele foi exposto pela primeira vez à linguagem de programação Logo, bem como a uma dose podencialmente perigosa de Pac Man. Isto provavelmente explica por que ele tem servido desde então como Lead Graphic Artist na SONY Eletronic Publishing/Psygnosis. Daniel gosta de passar o tempo com sua esposa, Mary, que está esperando uma criança para esta primavera. Ele pode ser encontrado no email drobbins@gentoo.org.


Bash em Exemplos - Parte 3

Explorando o sistema ebuild

Daniel Robbins
President and CEO, Gentoo Technologies, Inc.
Abril de 2000

Neste artigo final da série Bash em Exemplos, Daniel Robbins dá uma boa olhada no sistema ebuild do gentoo Linux, um excelente exemplo do poder do bash. Passo a passo, ele mostra como o sistema ebuild foi implementado, e toca em muitas técnicas úteis do bash, e estratégias de projeto. No fim do artigo, você terá uma boa idéia do que está envolvido na produção de uma aplicação completa baseada no bash, bem como terá iniciado a codificação de seu próprio sistema de auto-construção.

Conteúdo


Entra o sistema ebuild

Eu realmente estava olhando para diante para este terceiro e último artigo Bash em Exemplos, por que agora que já cobrimos os fundamentos da programação bash na Parte 1 e Parte 2, podemos nos direcionar para tópicos mais avançados, como desenvolvimento de aplicações bash e projeto de pogramas. Para este artigo, eu vou dar a vocês uma boa dose de experiência de desenvolvimento prática e real de bash, apresentando um projeto em que gastei muitas horas codificando e refinando: o sistema ebuild do Gentoo Linux.

Sou o arquiteto chefe do Gentoo Linux, um Linux OS avançado, atualmente em estágio beta. Uma das minhas responsabilidades primárias é garantir que todos os pacotes binários (similares a pacotes RPM) sejam criados propriamente e funcionem juntos. Como você provavelmente sabe, um sistema padrão Linux não é composto de uma única árvore de fontes unificada (como o BSD), mas é feito de cerca de mais de 25 pacotes principiais que trabalham juntos. Alguns dos pacotes incluem:

Pacote Descrição
linux O kernel
util-linux Uma coleção de programas miscelânea relacionados ao Linux
e2fsprogs Uma coleção de utilitários relacionados ao filesystem ext2
glibc A biblioteca GNU C

Cada pacote está em seu próprio tarball e é mantido por desenvolvedores independentes ou times de desenvolvedores. Para criar uma distribuição, cada pacote tem que ser separadamente baixado, compilado, e empacotado. Cada vez que um pacote deve ser corrigido, atualizado, ou melhorado, os passos de compilação e empacotamento devem ser repetidos (e eles ficam velhos realmente rápido). Para ajudar a eliminar os passos repetitivos envolvidos na criação e atualização de pacotes, eu criei o sistema ebuild, escrito quase que inteiramente em bash. Para melhorar seu conhecimento de bsh, irei mostrar como eu implementei as porções que fazem o desenpacotamento e compilação do sistema ebuild, passo a passo. Conforme eu explico cada passo, também irei discutir pro que certas decisões de projeto foram feitas. No fim do artigo, não somente você terá um excelente entendimento de projetos de programação bash de larga escala, mas você também terá implementado uma boa porção de um sistema de auto criação completa.

Por quê o bash?

O bash é um componente essencial do sistema ebuild do Gentoo Linux. Ele foi escolhido como linguagem primária do ebuild por várias razões. Primeiro, ele possui uma sintaxe simples e familiar que é especialmente apropriada para chamar programas externos. Um sistema auto-build é a "cola' que automatiza a chamada de programas externos, e o bash é bastante apropriado para este tipo de aplicação. Segundo, o suporte do bash para funções permite que o sistema ebuild tenha um código modular e fácil de entender. Terceiro, o sistema ebuild aproveita-se do suporte do bash para variáveis de ambiente, permitindo que mantenedores de pacotes e desenvolvedores o configurem facilmente, enquanto está rodando.

Revisão do processo de construção

antes de olharmos no sistema ebuild, vamos revisar o que está envolvido em fazer com que um pacote esteja compilado e instalado. Para nosso exemplo, iremos olhar o pacote "sed", um utilitário de edição de textos em linha padrão do GNU, que é parte de todas as distribuições do Linux. Primeiro, baixe o tarball do fonte (sed-3.02.tar.gz) (veja Recursos). Iremos armazenar este arquivo em /usr/src/distfiles, um diretório ao qual iremos nos referir usando a variável de ambiente "$DISTDIR". "$DISTDIR" é o diretório em que todos os tarballs de fontes originais residem. É um grande depósito de código fonte.

Nosso próximo passo é criar um diretório temporário chamado "work", que irá abrigar os fontes descompactados. Iremos nos referir a este diretório mais tarde usando a variável de ambiente "$WORKDIR". Para isto, iremos trocar de diretório para um em que tenhamos permissão de escrita, e iremos escrever o seguinte:

Descompactando o sed em um diretório temporário

$ mkdir work
$ cd work
$ tar xzf /usr/src/distfiles/sed-3.02.tar.gz

O tarball é então descompactado, criando um diretório chamado sed-3.02 que contém todo o código fonte. Iremos nos referir ao diretório sed-3.02 mais tarde usando a variável de ambiente "$SRCDIR". Para compilar o programa, usamos o seguintes comandos:

Compilando o sed

$ cd sed-3.02
$ ./configure --prefix=/usr
(autoconf gera os arquivos makefile apropriados, pode demorar um pouco)

$ make
(o pacote é compilado a partir dos fontes, também pode demorar um pouco)

Vamos omitir o passo final, o "make install", já que estamos apenas cobrindo a descompactação e compilação neste artigo. Se quisermos escrever um scrpt bash para executar todos estes passos para nós, ele poderia se parecer com o seguinet:

Script bash de exemplo para executar o processo de descompactar/compilar

#!/usr/bin/env bash

if [ -d work ]
then
# remove old directory if it exists
	rm -rf work
fi
mkdir work
cd work
tar xzf /usr/src/distfiles/sed-3.02.tar.gz
cd sed-3.02
./configure --prefix=/usr
make

Generalizando o código

Apesar deste script de autocompilação funcionar, ele não é muito flexível. Basicamente, o script só contém a listagem de todos os comandos que escrevemos na linha de comando. Apesar desta solução funcionar, seria legal fazer um script mais genérico que possa ser configurado rapidamente para descompactar e compilar qualquer pacote pela alteração de algumas linhas. Desta forma, é menos trabalho para o administrador do pacote acrescentar novos pacotes à distribuição. Vamos dar o primeiro passo nesta direção usando bastante variáveis de ambiente, tornando nosso script mais genérico:

Um script novo, mais genérico

#!/usr/bin/env bash

# P is the package name

P=sed-3.02

# A is the archive name

A=${P}.tar.gz

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}

if [ -z "$DISTDIR" ]
then
	# set DISTDIR to /usr/src/distfiles if not already set
	DISTDIR=/usr/src/distfiles
fi
export DISTDIR

if [ -d ${WORKDIR} ]
then
	# remove old work directory if it exists
	rm -rf ${WORKDIR}
fi

mkdir ${WORKDIR}
cd ${WORKDIR}

tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make

Acrescentamos muitas variáveis de ambiente ao código, mas ele ainda faz basicamente a mesma coisa. Entretanto, agora, para compilar qualquer tarball de fonte padrão GNU baseado no autoconf, podemos simplesmente copiar este arquivo para um novo arquivo (com um nome apropriado para refletir o nome do novo pacote que ele compila), e então trocar os valores de "$A" e "$P" para os novos valores. Todas as outras variáveis de ambiente automaticamente são ajustadas para as configurações corretas, e o script irá funcionar como esperado. Enquanto isto é útil, existe ainda alguns melhoramentos que podem ser feitos ao código. Este códito em particular é muito maior que o script de "transcrição" que criamos. Como um dos objetivos de qualquer projeto de programação deve ser a redução da complexidade para o usuário, seria legal diminuir dramaticamente o código, ou, pelo menos, organizá-lo melhor. Podemos fazer isto com um truque legal -- dividir o código em dois arquivos separados. Salve este arquivo como "sed-3.02.ebuild":

sed-3.02.ebuild

#the sed ebuild file -- very simple!
P=sed-3.02
A=${P}.tar.gz

Nosso primeiro arquivo é trivial, e contém somente as variáveis de ambiente que devem ser configuradas por pacote. Aqui está o seguindo script, que contém o cérebro da operação. Salve este como "ebuild" e torne-o executável:

O script ebuild

#!/usr/bin/env bash

if [ $# -ne 1 ]
then
	echo "one argument expected."
	exit 1
fi

if [ -e "$1" ]
then
	soruce $1
else
	echo "ebuild file $1 not found."
	exit 1
fi

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work
export SRCDIR=${WORKDIR}/${P}

if [ -z "$DISTDIR" ]
then
	# set DISTDIR to /usr/src/distfiles if not already set
	DISTDIR=/usr/src/distfiles
fi
export DISTDIR

if [ -d ${WORKDIR} ]
then
	# remove old work directory if it exists
	rm -f ${WORKDIR}
fi

mkdir ${WORKDIR}
cd ${WORKDIR}
tar xzf ${DISTDIR}/${A}
cd ${SRCDIR}
./configure --prefix=/usr
make

Agora que dividimos nosso sistema em dois arquivos, eu aposto que você está tentando imaginar como ele funciona. Basicamente, para compilar o sed, escreva:

$ ./ebuild sed-3.02.ebuild

Quando o "ebuild" é executado, ele primeiro tenta fazer um "source" de "$1". O que isto significa? Do meu artigo anterior, lembre-se que "$1" é o primeiro argumento da linha de comando -- neste caso, "sed-3.02.ebuild". No bash, o comando "source" lê declarações bash de um arquivo, e executa elas como se elas estivessem no lugar em que o comando "source" está. Assim, "source ${1}" faz com que o script "ebuild" execute os comandos em "sed-3.02.ebuild", que faz com que "$P" e "$A" sejam definidas. Esta alteração de projeto é realmente útil, por que se queremos compilar outro programa ao invés do sed, simplesmente criamos um novo arquivo .ebuild e passamos o mesmo como um argumento para nosso script "ebuild". Desta forma, os arquivos .ebuild são realmente simples, enquanto o cérebro complicado do sistema ebuild fica armazenado em um lugar -- nosso script "ebuild". Desta forma, podemos atualizar ou melhorar o sistema ebuild simplesmente editando o script "ebuild", mantendo os detalhes de implementação fora dos arquivos ebuild. Segue um arquivo ebuild exemplo para o gzip:

gzip-1.2.4a.ebuild

#another really simple ebuild script!
P=gzip-1.2.4a
A=${P}.tar.gz

Acrescentando funcionalidades

OK, estamos fazendo algum progresso. Mas existe uma funcionalidade adicional que eu gostaria de acrescentar. Eu gostaria que o script ebuild aceitasse um segundo argumento de linha de comando, que será "compile", "unpack", ou "all". Este segundo argumento de linha de comando informa ao script ebuild qual passo em particular do script eu quero executar. Desta forma, eu posso informar ao ebuild para descompactar o arquivo, mas não compilar ele (caso eu queira inspecionar o código fonte antes que a compilação inicie). Para isto, irei acrescentar uma declaração case que irá testar a variável "$2", e fazer coisas diferentes baseado em seu valor. Aqui está o nosso novo código:

ebuild, revisão 2

#!/usr/bin/env bash

if [ $# -ne 2 ]
then
	echo "Please specify two args - .ebuild file and unpack, compile or all"
	exit 1
fi

if [ -z "$DISTDIR" ]
then
	# set DISTDIR to /usr/src/distfiles if not already set
	DISTDIR=/usr/src/distfiles
fi
export DISTDIR

ebuild_unpack() {
	#make sure we're in the right directory
	cd ${ORIGDIR}

	if [ -d ${WORKDIR} ]
	then
		rm -rf ${WORKDIR}
	fi

	mkdir ${WORKDIR}
	cd ${WORKDIR}
	if [ ! -e ${DISTDIR}/${A} ]
	then
		echo "${DISTDIR/${A} does not exist. Please download first."
		exit 1
	fi
	tar xzf ${DISTDIR/${A}
	echo "Unpacked ${DISTDIR}/${A}."
	#source is now correctly unpacked
}

ebuild_compile() {
	#make sure we're in the right directory
	cd ${SRCDIR}
	if [ ! -d "${SRCDIR}" ]
	then
		echo "${SRCDIR} does not exist -- please unpack first."
		exit 1
	fi
	./configure --prefix=/usr
	make
}

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work

if [ -e "$1" ]
then
	source $1
else
	echo "Ebuild file $1 not found."
	exit 1
fi

export SRCDIR=${WORKDIR}/${P}

case "${2}" in
	unpack)
		ebuild_unpack
		;;
	compile)
		ebuild_compile
		;;
	all)
		ebuild_unpack
		ebuild_compile
		;;
	*)
		echo "Please specify unpack, compile or all as the second arg"
		exit 1
		;;
esac

Fizemos muitas alterações, então vamos primeiro revisá-las. Primeiro, colocamos os passos de compilação e desarquivamento em suas próprias funções, chamadas ebuild_compile() e ebuild_unpack(), respectivamente. Este foi um movimento inteligenes, uma vez que o código vai ficando mais complicado, e as novas funções dão uma modularidade, que ajuda a manter as coisas organizadas. Na primeira linha de cada função, eu explicitamente fiz um "cd" para o diretório que queria estar por que, conforme nosso código está ficando mais modular que linear, é mais provável que cometamos um deslize e executemos uma função no diretório de trabalho errado. O comando "cd" explicitamente coloca-nos no lugar certo, e evita que cometamos um erro mais tarde -- um passo importante -- especialmente se você irá excluir arquivos em funções.

Além disto, eu acrescentei uma checagem útil no início da função ebuild_compile(). Agora, ela checa para certificar-se que "$SRCDIR" exista, e, caso não exista, imprime uma mensagem de erro informando o usuário para primeiro desempacotar o arquivo, e então sai. Se você quiser, pode alterar este comportamento, de forma que se "$SRCDIR" não existir, nosso script ebuild irá descompactar o arquivo fonte automaticamente. Você pdoe fazer isto substituindo o código do ebuild_compile() pela seguinte versão:

Uma nova alteração em ebuild_compile()

ebuild_compile() {
	#make sure we're in the right directory
	if [ ! -d "${SRCDIR}" ]
	then
		ebuild_unpack
	fi
	cd ${SRCDIR}
	./configure --prefix=/usr
	make
}

Uma das alterações mais óbvias em nossa segunda versão do script ebuild é a nova declaração case no fim do código. Esta declaração case simplesmente checa o segundo argumento da linha de comando, e executa a ação correta, dependendo de seu valor. Se agora escrevermos:

$ ebuild sed-3.02.ebuild

iremos obter uma mensagem de erro. O ebuild agora quer que digamos a ele o que fazer, como abaixo:

ebuild sed-3.02.ebuild unpack

ou

ebuild sed-3.02.ebuild compile

ou

ebuild sed-3.02.ebuild all

Se você fornecer um segundo argumento diferente de qualquer destas opções listadas, receberá uma mensagem de erro (a cláusula *), e o programa terminará.

Modularizando o código

Agora que o código está bastante avançado e funcional, você pode ficar tentado a criar vários scripts ebuild para descompactar e compilar seus programas favoritos. Se você o fizer, cedo ou tarde irá cruzar com alguns fontes que não usam o autoconf ("./configure") ou possivelmente outros que possuam processos de compilação não-padrão. Precisamos nos certificar de fazer mais algumas alterações para o sistema ebuild para acomodar estes programas. Mas, antes disto, é uma boa idéia pensar um pouco sobre como fazer isto.

Uma das coisas boas de ter escrito explicitamente "./configure --prefix=/usr; make' em nosso estágio de compilação é que, na maior parte do tempo, isto funciona. Mas precisamos que o sistema ebuild acomode fontes que não usem o autoconf ou Makefiles normais. Para resolver este problema, eu proponho que nosso script ebuil deve, por padrão, fazer o seguinte:

  1. Se existir um script configure em $"{SRCDIR}", executar o mesmo como segue:
    ./configure --prefix=/usr
    
    Caso contrário, não executar este passo
  2. Executar o seguinte comando:
    make
    

Uma vez que o ebuild somente executa o ebuild se ele realmente existir, podemos acomodar automaticamente os programas que não usam o autoconf e possuem makefiles padrão. Mas se um simples "make" não fizer o mesmo truque para alguns fontes? Precisamos uma forma de contornar nossos valores defaults com algum código específico para tratar estas situações. Para isto, iremos transformar nossa função ebuild_compile() em duas funções. A primeira função, que pode ser vista como uma função "pai", ainda será chamada de ebuild_compile(). Entretanto, teremos uma nova função, chamada user_compile(), que contém somente nossas ações de compilação razoáveis:

ebuild_compile() dividido em duas funções

user_compile() {
	#we're already in ${SRCDIR}
	if [ -e configure ]
	then
		#run configure script if it exists
		./configure --prefix=/usr
	fi
	#run make
	make
}

ebuild_compile() {
	if [ -d "${SRCDIR}" ]
	then
		echo "${SRCDIR} does not existe -- please unpack first."
		exit 1
	fi
	#make sure we're in the right directory
	cd ${SRCDIR}
	user_compile
}

Pode não parecer óbvio o que eu estou fazendo agora, mas confie em mim. Enquanto o código funciona de forma quase idêntica a nossa versão anterior do ebuild, podemos fazer agora algo que não poderíamos fazer antes -- podemos sobrescrever o user_compile() no sed-3.02.ebuild. Assim, se o user_compile() padrão não atende nossas necessidades, podemos definir um novo em nosso arquivo .ebuild que contém os comandos necessários para compilar o pacote. Por exemplo, aqui temos um ebuild para o e2fsprogs-1.18, que requer uma linha "./configure" um pouco diferente:

e2fsprogs-1.18.ebuild

#este arquivo ebuild sobrescreve o user_compile() padrão
P=e2fsprogs-1.18
A=${P}.tar.gz

user_compile() {
	./configure --enable-elf-shlibs
	make
}

Agora, o e2fsprogs será compilado exatamente da forma que queremos. Mas, na maioria dos pacotes, poderemos omitir qualquer função user_compile() no arquivo .ebuild, e a função default user_compile() será usada.

Como exatamente o script ebuild sabe qual função user_compile() usar? Isto é, na verdade, bastante simples. No script ebuild, a função padrão user_compile() é definida antes que o arquivo e2fsprogs-1.18.ebuild seja "sourced". Se houver uma função user_compile() no e2fsprogs-1.18.ebuild, ela sobrescreve a versão default definida anteriormente. Se não, a função default user_compile() é usada.

Isto é muito legal, acrescentamos bastante flexibilidade sem precisar de nenhum código complexo desnecessário. Não iremos cobrir isto aqui, mas você pode fazer modificações similares ao ebuild_unpack() de forma que os usuários consigam contornar o processo padrão de desempacotamento. Isto pode ser útil se qualquer patch deve ser aplicado, ou se os arquivos estão contidos em múltiplos arquivos. Também é uma boa idéia modificar nosso código de desempacotamento de forma que ele reconheça tarballs compactados pelo bzip2 por default.

Arquivos de configuração

Já cobrimos bastantes técnicas do bash, e agora é hora de cobrirmos mais uma. Geralmente, é útil para um programa ter um arquivo de configurações globais que resida no /etc. Felizmente, isto é fácil de fazer usando o bash. Simplesmente crie o seguinte arquivo e salve-o como /etc/ebuild.conf:

/etc/ebuild.conf

# /etc/ebuild.conf: set system-wide ebuild options in this file

# MAKEOPTS are options passed to make
MAKEOPTS="-j2"

Neste exemplo, incluímos apenas uma opção de configuração, mas você poderia ter incluído muitas mais. Uma das coisas bonitas sobre o bash é que este arquivo pode ser executado via o "source", de forma muito simples. Este é um truque de projeto que funciona com a maioria das linguagens interpretadas. Depois que /etc/ebuild.conf é "sourced", "$MAKEOPTS" está definida dentro de seu script ebuild. Iremos usar ele para permitir que o usuário passe opções para o make. Normalmente, esta opção pode ser utilizada para permitir ao usuário informar ao ebuild para fazer um make em paralelo.

O que é um make em paralelo?

Para aumentar a velocidade de compilação em máquinas multiprocessadas, o make suporta a compilação em paralelo de programas. Isto significa que, em vez de somente compilar um código fonte por vez, o make compila um número especificado pelo usuário de arquivos fonte simultaneamente (assim aqueles processadores extras em máquinas multiprocessadas são usadas). O make paralelo é habilitado passando a opção -j # para o make, conforme abaixo:

make -j4 MAKE="make -j4"

Este código instrui o make a compilar quatro programas simultaneamente. O argumento MAKE="make -j4" diz ao make para passar a opção -j4 para qualquer processo-filho que o make venha a lançar.

Aqui está a versão final do nosso programa ebuild:

ebuild, versão final

#!/usr/bin/env bash

if [ $# -ne 2 ]
then
	echo "Please specify ebuild file and unpack, compile or all"
	exit 1
fi

source /etc/ebuild.conf

if [ -z "$DISTDIR" ]
then
	# set DISTDIR to /usr/src/distfiles if not already set
	DISTDIR=/usr/src/distfiles
fi
export DISTDIR

ebuild_unpack() {
	#make sure we're in the right directory
	cd ${ORIGDIR}

	if [ -d ${WORKDIR} ]
	then
		rm -rf ${WORKDIR}
	fi

	mkdir ${WORKDIR}
	cd ${WORKDIR}
	if [ ! -e ${DISTDIR}/${A} ]
	then
		echo "${DISTDIR}/${A} does not exist. Please download first."
		exit 1
	fi
	tar xzf ${DISTDIR}/${A}
	echo "Unpacked ${DISTDIR}/${A}."
	#ource is now correctly unpacked
}

user_compile() {
	#we're already in ${SRCDIR}
	if [ -e configure ]
	then
		#run configure script if it exists
		./configure --prefix=/usr
	fi
	#run make
	make $MAKEOPTS MAKE="make $MAKEOPTS"
}

ebuild_compile() {
	if [ ! -d "${SRCDIR}" ]
	then
		echo "${SRCDIR} does not exist -- please unpack first."
		exit 1
	fi
	#make sure we're in the right directory
	cd ${SRCDIR}
	user_compile
}

export ORIGDIR=`pwd`
export WORKDIR=${ORIGDIR}/work

if [ -e "$1" ]
then
	source $1
else
	echo Ebuild file $1 not found."
	exit 1
fi

export SRCDIR=${WORKDIR}/${P}

case "${2}" in
	unpack)
		ebuild_unpack
		;;
	compile)
		ebuild_compile
		;;
	all)
		ebuild_unpack
		ebuild_compile
		;;
	*)
		echo "Please specify unpack, compile or all as the second arg"
		exit 1
		;;
esac

Note que o "sourcing" de /etc/ebuild.conf é feito perto do início do arquivo. Note também que usamos "$MAKEOPTS" em nossa função user_compile() default.Você pode estar se perguntando como isto irá funcionar -- afinal de contas, nós estamos nos referindo a "$MAKEOPTS" antes de fazer o "source" em /etc/ebuild.conf, que é o que define "$MAKEOPTS" em princípio. Para nossa sorte, isto estáOK, por que a expansão de variáveis somente acontece quando user_compile() é executado. Na hora que user_compile() é executado, /etc/ebuild.conf já foi "sourced", e "$MAKEOPTS" está configurada com o valor correto.

Empacotando tudo

Nós cobrimos muitas técnicas de programação bash neste artigo, mas somente tocamos na superfície do poder do bash. Por exemplo, o sistema ebuild em produção no Gentoo Linux não somente desempacota e compila cada pacote, mas ele também:

  • Automaticamente faz o download das fontes se elas não estiverem em "$DISTDIR"
  • Verifica que as fontes não estão corrompidas usando MD5 message digest
  • Se solicitado, instala a aplicação compilada no sistema que está rodando, registrando todos os arquivos instalados, de forma que o pacote pode ser facilmente desinstalado mais tarde.
  • Se solicitado, empacota a aplicação compilada em um tarball (e compacta da forma que você preferir), de forma que pode ser instalada mais tarde, em outro computador, ou durante o processo de instalação baseado em CD (se você estiver montando um CD de distribuição).

Além disto, o sistema ebuild em produção possui várias outras opções de configuração global, permitindo que o usuário especifique opções como quais as flags de otimização utilizadas durante a compilação, e se o suporte opcional a pacotes como o GNOME e o slang devem ser habilitados por padrão nos pacotes que suportam esta opção.

Está claro que o bash pode realizar muito mais do que o que foi tocado nesta série de artigos. Espero que você tenha aprendido bastante sobre esta incrível ferramenta, e está excitado sobre o uso do bash para tornar mais rápido e melhorar seus projetos de desenvolvimento.

Recursos

Sobre o autor

Residindo em Albuquerque, Novo México, Daniel Robbins é o Chief Architect do Gentoo Project, CEO da Gentoo Technologies, Inc., o mentor do Linux Advanced Multimedia Project (LAMP), e um autor-contribuinte dos livros da Macmillan Caldera OpenLinux Unleashed, SuSE Linux Unleashed, e Samba Unleashed. Daniel está envolvido com computadores de alguma forma desde o segundo grau, quando ele foi exposto pela primeira vez à linguagem de programação Logo, bem como a uma dose podencialmente perigosa de Pac Man. Isto provavelmente explica por que ele tem servido desde então como Lead Graphic Artist na SONY Eletronic Publishing/Psygnosis. Daniel gosta de passar o tempo com sua esposa, Mary, que está esperando uma criança para esta primavera. Ele pode ser encontrado no email drobbins@gentoo.org.


Chess Buddy (6600)


Download (Chess Buddy.jar, 77.2 KB)