ver conteudo | ver menu

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:

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)