Académique Documents
Professionnel Documents
Culture Documents
Aritmética em Shell
Antigamente usávamos o comando expr para fazer operações aritméticas e muita
gente ainda usa, pois é compatível com quaquer ambiente.
Exemplo:
$ expr 7 \* 5 / 3 # 7 vezes 5 = 35 dividido por 3 = 11
11
Neste artigo porém, vamos ver outras formas não tanto conhecidas, porém mais
simples de usar, mais elaboradas e com precisão maior.
O uso do bc
Uma forma bacana de fazer cálculos em Shell – usada normalmente quando a
expressão aritmética é mais complexa, ou quando é necessário trabalharmos com
casas decimais – é usar a instrução calculadora do UNIX/LINUX. O bc. Veja como:
Exemplo:
$ echo "(2 + 3) * 5" | bc # Parênteses usados para dar precedência
25
Para trabalhar com números reais (números não necessariamente inteiros),
especifique a precisão (quantidade de decimais) com a opção scale do comando bc.
Assim vejamos o penúltimo exemplo:
$ echo "scale=2; 7*5/3" | bc
11.66
Outros exemplos:
$ echo "scale=3; 33.333*3" | bc
99.999
$ num=5
$ echo "scale=2; ((3 + 2) * $num + 4) / 3" | bc
9.66
Obviamente todos os exemplos acima no caso de linux, poderiam (e deveriam) ser
escritos usando Here Strings. Veja os últimos como ficariam:
$ bc <<< "scale=3; 33.333*3"
99.999
$ num=5
$ bc <<< "scale=2; ((3 + 2) * $num + 4) / 3"
9.66
Uma vez apareceu na lista (excelente por sinal) de Shell script no Yahoo
(http://br.groups.yahoo.com/group/shell-script/) um cara com a seguinte dúvida: "eu
tenho um arquivo cujos campos estão separados por <TAB> e o terceiro deles possui
números. Como posso calcular a soma de todos os números desta coluna do
arquivo?"
Mandei a seguinte resposta:
$ echo $(cut -f3 num | tr '\n' +)0 | bc
20.1
Vamos por partes para entender melhor e primeiramente vamos ver como era o
arquivo que fiz para teste:
$ cat num
a b 3.2
a z 4.5
w e 9.6
q w 2.8
Como pode-se ver, está dentro do padrão do problema, onde eu tenho como
terceiro campo números reais. Vamos ver o que faria a primeira parte da linha de
comandos, onde eu transformo os caracteres <ENTER> (new-line) em um sinal de mais
(+):
$ cut -f3 num | tr '\n' +
3.2+4.5+9.6+2.8+
Se eu mandasse desse jeito para o bc, ele me devolveria um erro por causa daquele
sinal de mais (+) solto no final do texto. A minha saída foi colocar um zero no final,
pois somando zero o resultado não se alterará. Vamos ver então como ficou:
$ echo $(cut -f3 num | tr '\n' +)0
3.2+4.5+9.6+2.8+0
Tudo bem, o problema está resolvido, mas em shell sempre tem um jeitinho de se
escrever um código menor ainda. Veja isso:
$ cut -f3 num | paste -sd+
3.2+4.5+9.6+2.8
Ou seja, o cut corta a terceira coluna e manda os números para um paste, cuja
opção -s transforma colunas em linhas e o -d+, define o sinal de mais (+) como
delimitador. Feito isso, é só mandar isso tudo para o bc proceder à soma com os
reais.
$ cut -f3 num | paste -sd+ | bc
20.1
Isso é o que se costuma chamar one-liner, isto é, códigos que seriam complicados
em outras linguagens (normalmente seria necessário criar contadores e fazer um
loop de leitura somando o terceiro campo ao contador) e em Shell são escritos em
uma única linha.
Há também gente que chama isso de método KISS, que é o acrônimo de Keep It
Simple Stupid.
Mas o potencial de uso desta calculadora não se encerra aí, existem diversas
facilidades por ela propiciadas. Veja só este exemplo:
$ echo "obase=16; 11579594" | bc
B0B0CA?
$ echo "ibase=16; B0B0CA?" | bc # B, zero, B, zero, C, e A
11579594
Nestes exemplos vimos como fazer mudanças de base de numeração com o uso do
bc. Na primeira explicitamos a base de saída ( obase) como 16 (hexadecimal) e na
segunda, dissemos que a base da entrada ( ibase) era 10 (decimal).
Baseando
Se você quiser trabalhar com bases diferentes da decimal, basta usar o formato:
base#numero
Onde base é um número decimal entre 2 e 64 representando o sistema de
numeração, e numero é um número no sistema numérico definido por base. Se base#
for omitida, então 10 é assumida como default. Os algarismos maiores que 9 são
representados por letras minúsculas, maiúsculas, @ e _, nesta ordem.
Se base for menor ou igual a 36, maiúsculas ou minúsculas podem ser usadas
indiferentemente para definir algarismos maiores que 9 (não está mal escrito, os
algarismos do sistema hexadecimal, por exemplo, variam entre 0 (zero) e F).
Vejamos como isso funciona:
$ echo $[2#11]
3
$ echo $((16#a))
10
$ echo $((16#A))
10
$ echo $((2#11 + 16#a))
13
$ echo $[64#a]
10
$ echo $[64#A]
36
$ echo $((64#@))
62
$ echo $((64#_))
63
Nestes exemplos usei as notações $((...)) e $[...] indistintamente, para
demonstrar que ambas funcionam.
Funciona também uma mudança automática para a base decimal, desde que você
esteja usando a convenção numérica do C, isto é, em 0xNN, o NN será tratado como
um hexadecimal e em 0NN, o NN será visto como um octal. Veja o exemplo:
Exemplo
$ echo $((10)) # decimal
10
$ echo $((010)) # octal
8
$ echo $((0x10)) # hexadecimal
16
$ echo $((10+010+0x10)) # Decimal + octal + hexadecimal
64
Ah, já ia me esquecendo! As expressões aritméticas com os formatos $((...)), $
[...] e com o comando let usam os mesmos operadores usados na instrução expr,
além dos operadores unários (++, --, +=, *=, ...) e condicionais que acabamos
de ver.
Testes usando expressões regulares
No Papo de Botequim 004, nós falamos tudo sobre comandos condicionais, mas
faltou um que não existia àquela época. Neste mesmo Papo de Botequim, na seção
E tome de test nós chegamos a falar de uma construção do tipo:
[[ Expressao ]] && cmd
Onde o comando cmd será executado caso a expressão condicional Expressao seja
verdadeira. Disse ainda que Expressao poderia ser estipulada de acordo com as
regras de Geração de Nome de Arquivos (File Name Generation). A partir do bash
versão 3, foi incorporado a esta forma de teste um operador representado por =~,
cuja finalidade é fazer comparações com Expressões Regulares.
Exemplo:
$ echo $BASH_VERSION
#Conferindo se a versão do Bash é igual ou superior a 3.0.0
3.2.17(15)-release
$ Cargo=Senador
$ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && echo É político
É político
$ Cargo=Senadora
$ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && echo É político
É político
$ Cargo=Diretor
$ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && echo É político
$
Vamos dar uma esmiuçada na Expressão Regular ^(Governa|Sena|Verea)dora?$: ela
casa com tudo que começa (^) por Governa, ou (|) Sena, ou (|) Verea, seguido de dor
e seguido de um a opcional (?). O cifrão ($) serve para marcar o fim. Em outras
palavras esta Expressão Regular casa com Governador, Senador, Vereador,
Governadora, Senadora e Vereadora.
Colorindo a tela
Como você já havia visto no Papo de Botequim 007, o comando tput serve para
fazer quase tudo referente a formatação de tela, mas o que não foi dito é que com
ele também pode-se usar cores de frente (dos caracteres) e de fundo. Existem
também outras formas de fazer o mesmo, acho porém, esta que veremos agora,
mais intuitiva (ou menos “desintuitiva”). A tabela a seguir mostra os comandos para
especificarmos os padrões de cores de frente ( foreground) ou de fundo (background):
Obtendo cores com o comando tput
Comando Efeito
tput setaf n Especifica n como a cor de frente
(foreground)
tput setab n Especifica n como a cor de fundo
(background)
Bem, agora você já sabe como especificar o par de cores, mas ainda não sabe as
cores. A tabela a seguir mostra os valores que o n (da tabela anterior) deve assumir
para cada cor:
Valores das cores com o comando tput
Valor Cor
0 Preto
1 Vermelho
2 Verde
3 Marrom
4 Azul
5 Púrpura
6 Ciano
7 Cinza claro
Neste ponto você já pode começar a brincar com as cores.
- Mas peraí, ainda são muito poucas!
- É, tem toda razão... O problema é que ainda não lhe disse que se você colocar o
terminal em modo de ênfase (tput bold), estas cores geram outras oito. Vamos
montar então a tabela definitiva de cores:
Valores das cores com o comando tput
Valor Cor Cor após tput
bold
0 Preto Cinza escuro
1 Vermelho Vermelho claro
2 Verde Verde claro
3 Marron Amarelo
4 Azul Azul Brilhante
5 Púrpura Rosa
6 Ciano Ciano claro
7 Cinza claro Branco
Exemplo
Como exemplo, vejamos um script que mudará a cor de sua tela de acordo com sua
preferência.
$ cat mudacor.sh
#!/bin/bash
tput sgr0
clear
CL=
until [[ $CL == 0[1-8] || $CL == [1-8] ]]
do
read -p "
Escolha a cor da letra: " CL
done
CF=
until [[ $CF == 0[1-8] || $CF == [1-8] ]]
do
read -p "
Escolha a cor de fundo: " CF
done
Para melhorar a situação, vejamos uns exemplos; porém, vejamos primeiro quais
são os arquivos do diretório corrente que começam por .b:
$ ls -la .b*
-rw------- 1 d276707 ssup 21419 Dec 26 17:35 .bash_history
-rw-r--r-- 1 d276707 ssup 24 Nov 29 2004 .bash_logout
-rw-r--r-- 1 d276707 ssup 194 Nov 1 09:44 .bash_profile
-rw-r--r-- 1 d276707 ssup 142 Nov 1 09:45 .bashrc
Para listar esses arquivos em ordem de tamanho, podemos fazer:
$ find . -name ".b*" -printf '%s\t%p\n' | sort -n
24 ./.bash_logout
142 ./.bashrc
194 ./.bash_profile
21419 ./.bash_history
No exemplo que acabamos de ver, o \t foi substituído por um na saída de forma a
tornar a listagem mais legível. Para listar os mesmos arquivos classificados por data
e hora da última alteração:
$ find . -name ".b*" -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n' | sort
2004-11-29 11:18:51 ./.bash_logout
2005-11-01 09:44:16 ./.bash_profile
2005-11-01 09:45:28 ./.bashrc
2005-12-26 17:35:13 ./.bash_history
Vamos começar com o mais simples: um exemplo sem nome e direto no prompt:
$ coproc while read Entra # coproc ativo
> do
> echo -=-=- $Entra -=-=-
> done
[2] 3030
$ echo Olá >&${COPROC[1]} # Manda Olá para a pipe da saída
$ read -u ${COPROC[0]} Sai # Lê do pipe da entrada
$ echo $Sai
-=-=- Olá -=-=-
$ kill $COPROC_PID # Isso não pode ser esquecido...
Como você viu, o vetor COPROC, está associado a dois pipes; o ${COPROC[1]} que
contém o endereço do pipe de saída, e por isso a saída do echo esta redirecionada
para ele e o ${COPROC[0]} que contém o endereço do pipe de entrada, e por isso
usamos a opção -u do read que lê dados a partir de um descritor de arquivo
definido, ao invés da entrada padrão.
Como o coprocesso utilizava a sintaxe sem nome, o padrão do nome do vetor é
COPROC.
Só mais uma teoriazinha chata:
$ echo ${COPROC[@]} # Lista todos os elementos do vetor
59 54
Como você viu ${COPROC[0]} estava usando o pipe apontado por /proc/$$/fd/59 e $
{COPROC[1]} usava /proc/$$/fd/54. Agora chega de teoria mesmo! Vamos agora
usar nome neste mesmo exemplo, para ver que pouca coisa muda:
$ coproc teste {
> while read Entra
> do
> echo -=-=- $Entra -=-=-
> done
> }
[6] 3192
$ echo Olá >&${teste[1]}
$ read -u ${teste[0]} Sai
$ echo $Sai
-=-=- Olá -=-=-
$ kill $teste_PID
Nesse momento, é bom mostrar uma coisa interessante: Quais são os processos em
execução?
$ ps # Somente no Bash em execução
PID TTY TIME CMD
1900 pts/0 00:00:01 bash
2882 pts/0 00:00:00 ps
Vamos executar 2 coprocessos simultâneos:
$ coproc nome1 { # Coprocesso nome1
> while read x
> do
> echo $x
> done; }
[1] 2883
$ coproc nome2 { # Coprocesso nome2
> while read y
> do
> echo $y
> done; }
bash: aviso: execute_coproc: coproc [2883:nome1] still exists
[2] 2884
Xiiii! Acho que deu zebra! Mas será que deu mesmo? Repare que além do PID 2883
de nome1, ele também me devolveu o PID 2884, que deve ser de nome2. Vamos ver o
que está acontecendo:
$ ps
PID TTY TIME CMD
1900 pts/0 00:00:01 bash Esse já existia
2883 pts/0 00:00:00 bash Esse está executando nome1
2884 pts/0 00:00:00 bash Esse está executando nome2
2885 pts/0 00:00:00 ps
Parece que foi só um aviso, pois os dois PIDs informados quando iniciamos os dois
coprocessos, estão ativos. Então vamos testar esses 2 caras:
$ echo xxxxxxxxx >&${nome1[1]} # Mandando cadeia para nome1
$ echo yyyyyyyyy >&${nome2[1]} # Mandando cadeia para nome2
$ read -u ${nome1[0]} Recebe
$ echo $Recebe
xxxxxxxxx
$ read -u ${nome2[0]} Recebe
$ echo $Recebe
yyyyyyyyy
$ kill $nome1_PID
$ kill $nome2_PID
Vetores associativos
A partir do Bash 4.0, passou a existir o vetor associativo. Chama-se vetor
associativo, aqueles cujos índices são alfabéticos. As regras que valem para os
vetores inteiros, valem também para os associativos, porém antes de valorar estes
últimos, é obrigatório declará-los.
Exemplo:
$ declare -A Animais # Obrigatório para vetor associativo
$ Animais[cavalo]=doméstico
$ Animais[zebra]=selvagem
$ Animais[gato]=doméstico
$ Animais[tigre]=selvagem
É impossível gerar todos os elementos de uma só vez, como nos vetores inteiros.
Assim sendo, não funciona a sintaxe:
Animais=([cavalo]=doméstico [zebra]=selvagem [gato]=doméstico
[tigre]=selvagem)
$ echo ${Animais[@]}
doméstico selvagem doméstico selvagem
$ echo ${!Animais[@]}
gato zebra cavalo tigre
Repare que os valores não são ordenados, ficam armazenados na ordem que são
criados, diferentemente dos vetores inteiros que ficam em ordem numérica.
Supondo que esse vetor tivesse centenas de elementos, para listar separadamente
os domésticos dos selvagens, poderíamos fazer um script assim:
$ cat animal.sh
#!/bin/bash
# Separa animais selvagens e domésticos
declare -A Animais
Animais[cavalo]=doméstico # Criando vetor para teste
Animais[zebra]=selvagem # Criando vetor para teste
Animais[gato]=doméstico # Criando vetor para teste
Animais[tigre]=selvagem # Criando vetor para teste
Animais[urso pardo]=selvagem # Criando vetor para teste
for Animal in "${!Animais[@]}" # Percorrendo vetor pelo índice
do
if [[ "${Animais[$Animal]}" == selvagem ]]
then
Sel=("${Sel[@]}" "$Animal") # Gerando vetor p/ selvagens
else
Dom=("${Dom[@]}" "$Animal") # Gerando vetor p/ domésticos
fi
done
# Operador condicional, usado para descobrir qual
#+ vetor tem mais elementos. Veja detalhes na seção
#+ O interpretador aritmético do Shell
Maior=$[${#Dom[@]}>${#Sel[@]}?${#Dom[@]}:${#Sel[@]}]
clear
tput bold; printf "%-15s%-15s\n" Domésticos Selvagens; tput sgr0
for ((i=0; i<$Maior; i++))
{
tput cup $[1+i] 0; echo ${Dom[i]}
tput cup $[1+i] 14; echo ${Sel[i]}
}
Gostaria de chamar a sua atenção para um detalhe: neste script me referi a um
elemento de vetor associativo empregando ${Animais[$Animal]} ao passo que me
referi a um elemento de um vetor inteiro usando ${Sel[i]}. Ou seja, quando
usamos uma variável como índice de um vetor inteiro, não precisamos prefixá-la
com um cifrão ($), ao passo que no vetor associativo, o cifrão ( $) é obrigatório.
Expansão de chaves
O Bash 4.0 incorporou duas novas formas de fazer expansão de chaves:
Sequência numérica com preenchimento de zeros à esquerda;
Possibilidade de usarmos um incremento (passo) em uma sequência
numérica.
Exemplos:
$ echo {0010..0019} # Preenchendo com 2 zeros à esquerda
0010 0011 0012 0013 0014 0015 0016 0017 0018 0019
$ echo {05..15} # Preenchendo com zeros à esquerda
05 06 07 08 09 10 11 12 13 14 15
$ echo {-5..19..3} # Incrementando de 3 em 3 (Passo 3)
-5 -2 1 4 7 10 13 16 19
$ echo {000..100..10} # Preenchendo com zeros e passo 10
000 010 020 030 040 050 060 070 080 090 100