Bash em Exemplos - Parte 2
Mais fundamentos de programação bashDaniel 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
- Aceitando argumentos
- Construções de programação Bash
- Amor condicional
- Problemas com comparações de strings
- Construções de laço
- Declarações case
- Funções e namespaces
- Namespace
- Juntando tudo
- Recursos
- Sobre o autor
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
- Leia o artigo introdutório sobre o bash, Bash em exemplos, Parte 1
- Visite a página do bash GNU
- Confira o manual de referência on line do bash
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 ebuildDaniel 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
- Por quê o bash?
- Revisão do processo de construção
- Generalizando o código
- Acrescentando funcionalidades
- Modularizando o código
- Arquivos de configuração
- Empacotando tudo
- Recursos
- Sobre o autor
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:
- Se existir um script configure em $"{SRCDIR}", executar o mesmo como
segue:
./configure --prefix=/usr
Caso contrário, não executar este passo - 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
- Baise o tarbal dos fontes (sed-3.02.tar.gz) de ftp://ftp.gnu.org/pub/gnu/sed
- Leia o artigo introdutório sobre o bash, Bash em exemplos, Parte 1
- Leia o artigo introdutório sobre o bash, Bash em exemplos, Parte 2
- Visite a página do bash GNU
- Confira o manual de referência on line do bash
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.