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.

1