You are on page 1of 37

IFS - Inter Field Separator

Lendo o artigo intitulado Atribuição de Valores a Variáveis em Shell Scripts me senti tentado a
escrever esta dica.
O shell tem uma variável interna chamada IFS - Inter Field Separator (será Tabajara?) - cujo valor
default podemos obter da seguinte forma:
$ echo "$IFS" | od -h
0000000 0920 0a0a
0000004

O programa od com a opção -h foi usado para gerar um dump hexadecimal da variável. E lá
podemos ver:
Valor Hexadecimal Significado
09 <TAB>
20 Espaço
0a <ENTER>

Ou seja os separadores entre campos (tradução livre de IFS) default são o <TAB>, o espaço em
branco e o <ENTER>. O IFS é usado em diversas instruções, mas seu uso é muito comum em par
com o for e/ou com o read. Vejamos um exemplo semelhante ao dado no Cantinho do Shell de
15/12/2006, que me inspirou a escrever este artigo.
$ cat script1.sh
#!/bin/sh
while read linha
do
awk -F: '{print $1}' /etc/passwd > /dev/null
done < /etc/passwd

Como podemos ver este script não faz nada (sua única saída foi enviada para /dev/null para não
deturpar os tempos de execução), mas vamos usá-lo para avaliação dos tempos de execução.
Este script foi alterado, trocando o awk pelo cut, e ficando com a seguinte cara:
$ cat script2.sh
#!/bin/sh
while read linha
do
echo $linha | cut -f1 -d: > /dev/null
done < /etc/passwd

Mais uma outra alteração, usando 99% de intrínsecos do Shell (exceto o comando echo) desta vez
tomando partido do IFS:
$ cat script3.sh
#!/bin/sh
IFS=:
while read user lixo
do
echo $lixo > /dev/null
done < /etc/passwd

Neste último exemplo, transformamos o separador padrão em dois-pontos (:) e usamos sua
propriedade em conjunto com o read, isto é, o primeiro campo veio para a variável user e o resto
para a variável lixo.
Em seguida, fiz um script usando Shell quase puro (novamente o echo foi o vilão). Repare a
construção ${linha%%:*}, que é um intrínseco (built-in) do Shell que serve para excluir da variável
linha o maior casamento com o padrão especificado (:* - que significa "de dois-pontos em diante"),
ou seja, excluiu de linha tudo a partir do último dois-pontos, contado da direita para a esquerda.

Dica: quem não conhece o macete acima, não pode deixar de ler a seção referente a expansão de
parâmetros em: http://twiki.softwarelivre.org/bin/view/TWikiBar/TWikiBarPapo009
$ cat script4.sh
#!/bin/sh
while read linha
do
echo ${linha%%:*} > /dev/null
done < /etc/passwd

Para finalizar, adaptei o script escrito pelo incansável Rubens Queiroz que, exceto pelo awk, é Shell
puro.
$ cat script5.sh
#!/bin/sh
for user in `awk -F: '{print $1}' /etc/passwd`
do
echo $user > /dev/null
done

Agora, o mais importante: reparem os tempos da execução de cada um deles:


$ time script1.sh
real 0m0.123s
user 0m0.032s
sys 0m0.032s

$ time script2.sh
real 0m0.297s
user 0m0.152s
sys 0m0.084s

$ time script3.sh
real 0m0.012s
user 0m0.004s
sys 0m0.004s

$ time ./script4.sh
real 0m0.012s
user 0m0.004s
sys 0m0.008s

$ time ./script5.sh
real 0m0.014s
user 0m0.012s
sys 0m0.004s

Reparem que estas diferenças de tempo foram obtidas para um arquivo com somente 29 linhas.
Veja:
$ wc -l /etc/passwd
29 /etc/passwd

Um outro uso interessante do IFS é o que vemos a seguir, primeiramente usando o IFS default que
como vimos é <TAB>, Espaço e <ENTER>:
$ Frutas="Pera Uva Maçã"
$ set - $Frutas
$ echo $1
Pera
$ echo $3
Maçã

Agora, vamos alterar o IFS para fazer o mesmo com uma variável qualquer, e para tal vamos
continuar usando o famigerado /etc/passwd:
$ Root=$(head -1 /etc/passwd)
$ echo $Root
root:x:0:0:root:/root:/bin/bash
$ oIFS="$IFS"
$ IFS=:
$ set - $Root
$ echo $1
root
$ echo $7
/bin/bash
$ IFS="$oIFS"

Senhores, neste artigo pretendi mostrar duas coisas:


* O uso do IFS que, infelizmente para nós, é uma variável pouco conhecida do Shell e
* Que quanto mais intrínsecos do Shell usamos, mais veloz e performático fica o script.

Script para embaralhar aleatoriamente as


linhas de um arquivo
O script de hoje ilustra uma forma de se embaralhar, de forma aleatória, as linhas de um arquivo.

1. Criação do Arquivo de teste


$ seq -f "Numero %g" 10 > entrada.txt # Criando arq de teste
$ cat entrada.txt
Numero 1
Numero 2
Numero 3
Numero 4
Numero 5
Numero 6
Numero 7
Numero 8
Numero 9
Numero 10
2. Criação do Script mix.sh
# Agora o script

$ cat mix.sh
#!/bin/sh

# Transforma o IFS em somente <ENTER>


IFS="
"
NumReg=$(cat entrada.txt | wc -l) # Total de Registros

i=0
# j vai variar de 0 até NumReg-1
j=$((RANDOM % $NumReg))
for Reg in $(cat entrada.txt)
do
# Enquanto o array de saida (aSai) tiver cheio...

while [ ${aSai[$j]} ]
do
j=$((RANDOM % $NumReg))
done
aSai[$j]="$Reg" # Move registro para posição randomica no array done
> saida.txt # Cria o arquivo de saida vazio

# Mais um tipo de for :)

for ((i=0; i<=$NumReg-1; i++))


do
echo ${aSai[i]} >> saida.txt # Carrega aleatóriamente o arquivo de saida
done

3. Execução do script
$ ./mix.sh
$ cat saida.txt # Primeira tentativa
Numero 8
Numero 2
Numero 3
Numero 1
Numero 5
Numero 10
Numero 7
Numero 9
Numero 6
Numero 4
$ ./mix.sh
$ cat saida.txt # Segunda tentativa
Numero 5
Numero 9
Numero 7
Numero 8
Numero 1
Numero 3
Numero 2
Numero 10
Numero 4
Numero 6

Um script com esta funcionalidade pode ter várias aplicações. Por exemplo, em um website, se
queremos exibir em determinada página uma lista de links, em que desejamos rotacionar os links
que aparecem no topo da página, podemos usa-lo para que a ordem seja sempre trocada. O script
pode ser acionado via cron, de 10 em 10 minutos, por exemplo.

Como extrair um site plone com wget


Essa solução foi desenvolvida pois precisávamos publicar um site feito em plone (versão 2.5.1) em
uma máquina sem condições de suportar uma instalação de zope/plone.
O problema principal quando se tenta copiar um site feito em plone para outra máquina usando o
wget são os arquivos CSS gerados dinamicamente pelo plone. O wget não encontra esses
arquivos. Como resultado a cópia do site é exibida sem CSS.
Para resolver esse problema foi criado um script python na raiz do site plone que monta os liks
dinâmicos. Aí é só passar o endereço do script na chamada do wget para que ele possa trazer os
CSS também.
Resolvido o problema principal falta resolver os problemas secundários. Alguns links para imagens
e outros arquivos dentro dos HTML e dos CSS copiados pelo wget continuam apontando para a
máquina original apesar desses arquivos terem sido trazidos pelo wget. Para solucionar esse
problema foi feito um script usando o comando sed para alterar os links não resolvidos pelo wget.
O mesmo comando sed resolve também outro problema detectado. Desta vez se trata de um mal
funcionamento do IE que não exibe imagens e gera links internos errados. Trata-se da interpretação
que o IE faz da tag base. O wget gera em todo html uma tag base sem conteúdo (<base href="" /
>). Essa tag é usada no html para informar que o endereço base de todo link da página usa como
base o endereço descrito nessa tag. Outros navegadores interpretam a tag base sem conteúdo. Como
nenhum endereço base é dessa forma, os links da página permanecem como estão. Já o IE interpreta
a tag base vazia como sendo o endereço base raiz, acrescentando uma / em todos os links da
página, o que causa a quebra de todos eles, inclusive links para imagens. Para resolver isso basta
apagar a linha com a tag base vazia de todos os arquivos html copiados. Não encontrando a tag
base, o IE (e os outros navegadores) não fazem nada com os links, que alias foram criados
corretamente pelo wget.
O último ponto a ser corrigido é o fato do plone usar o recurso de herança entre diretórios do zope
para sempre encontrar os CSS. Isso é facilmente resolvido movendo-se todos os CSS para a pasta
Plone Default. Note que isso deve ser feito com os arquivos copiados pelo wget, dessa forma não é
necessário alterar o funcionamento interno do plone com relação aos CSS.

Resumo Executivo
Para facilitar o trabalho criei o script wget.sh que executa o wget e faz os ajustes descritos
acima.
Para implementar essa solução siga os 3 passos:
Passo 1
Crie o seguinte script python na raiz do site plone. Chame-o de archive_portal

archive_portal
style_sheets = ['plone.css','ploneColumns.css','ploneCustom.css']
# Versao estatica
#dac_style_sheets = ['ploneStyles9563.css','ploneStyles0959.css','ploneStyles2195.css',
'ploneStyles3726.css', 'ploneStyles1531.css', 'ploneStyles2712.css']

# Versao dinamica
plone_style_sheets = ["portal_css/"+i.getId() for i in context.portal_css.getCookedResources()]
# Outra forma de fazer versao dinamica
#plone_style_sheets = []
#for i in context.portal_css.getCookedResources():
#plone_style_sheets.append("portal_css/"+i.getId())

graphics = ['bullet.gif','portal_logo']
index = 'index.html'

print "<html><head><title>archive portal</title></head><body>"


print "<a href='%s'>site entry point</a><br>" % context.absolute_url()
print "<a href='%s/%s'>site index</a><br>" % (context.absolute_url(), index)

#for item_name in plone_style_sheets:


#print "<a href='%s/portal_css/%s'>%s</a><br>" % (context.absolute_url(),item_name,
item_name)

for item_name in style_sheets + graphics + plone_style_sheets:


print "<a href='%s/%s'>%s</a><br>" % (context.absolute_url(),item_name, item_name)

for dir in context.portal_catalog(portal_type='Folder',review_state='published'):


print "<a href='%s/%s'>%s</a><br>" % (dir.getURL(), index, dir.id)

print "</body></html>"
return printed

Passo 2

Configure o Wget
Sugiro criar um arquivo .wgetrc no diretório home do usuário que irá rodar o wget.
O arquivo que usei com comentários é o seguinte:
.wgetrc

#input = urls-to-get

# -B URL
#--base=URL
#When used in conjunction with -F, prepends URL to relative links in the file specified by -i.
base = http://www.example.com/prg/dac/dac_plone/dac/index.html

#-nH
# --no-host-directories
# Disable generation of host-prefixed directories. By default, invoking Wget with -r
http://fly.srk.fer.hr/
# will create a structure of directories beginning with fly.srk.fer.hr/. This option disables such
behavior.
# no-host-directories = on nH = on (só funciona na linha de comando)

# Set directory prefix to prefix. The directory prefix is the direc-


# tory where all other files and subdirectories will be saved to,
# i.e. the top of the retrieval tree. The default is . (the current
# directory).
#dir_prefix = /www/ns-home/docs/prg/dac/dac_plone/

# Specify comma-separated lists of file name suffixes or patterns to


# accept or reject (@pxref{Types of Files} for more details).
reject = author,copyright,sendto_form,folder_listing,topic*

# Change which characters found in remote URLs may show up in local


# file names generated from those URLs. Characters that are
# restricted by this option are escaped, i.e. replaced with %HH,
# where HH is the hexadecimal number that corresponds to the
# restricted character.

# By default, Wget escapes the characters that are not valid as part
# of file names on your operating system, as well as control charac-
# ters that are typically unprintable. This option is useful for
# changing these defaults, either because you are downloading to a
# non-native partition, or because you want to disable escaping of
# the control characters.
#restrict_file_names = nocontrol
#restrict_file_names = windows
restrict_file_names = unix

# Do not ever ascend to the parent directory when retrieving recur-


# sively. This is a useful option, since it guarantees that only the
# files below a certain hierarchy will be downloaded.
no_parent = on

# This option causes Wget to download all the files that are neces-
# sary to properly display a given HTML page. This includes such
# things as inlined images, sounds, and referenced stylesheets.
page_requisites = on

# After the download is complete, convert the links in the document


# to make them suitable for local viewing. This affects not only the
# visible hyperlinks, but any part of the document that links to
# external content, such as embedded images, links to style sheets,
# hyperlinks to non-HTML content, etc.
convert_links = on
# When converting a file, back up the original version with a .orig
# suffix. Affects the behavior of timestamping.
backup-converted = on

# If a file of type application/xhtml+xml or text/html is downloaded


# and the URL does not end with the regexp \.[Hh][Tt][Mm][Ll]?, this
# option will cause the suffix .html to be appended to the local
# filename. This is useful, for instance, when you are mirroring a
# remote site that uses .asp pages, but you want the mirrored pages
# to be viewable on your stock Apache server. Another good use for
# this is when you are downloading CGI-generated materials. A URL
# like http://site.com/article.cgi?25 will be saved as arti-
# cle.cgi?25.html.

# Note that filenames changed in this way will be re-downloaded every


# time you re-mirror a site, because Wget can't tell that the local
# X.html file corresponds to remote URL X (since it doesn't yet know
# that the URL produces output of type text/html or applica-
# tion/xhtml+xml. To prevent this re-downloading, you must use convert_links
# and backup-converted so that the original version of the file will be saved as
# X.orig.
html-extension = on

# Log all messages to logfile. The messages are normally reported to


# standard error.
# output-file=/www/ns-home/docs/prg/dac/dac_plone/wgetlogfile ou
# (-o só funciona na linha de comando)

# You can turn on recursive retrieving by default (don't do this if


# you are not sure you know what it means) by setting this to on.
#recursive = off
recursive = on

# Lowering the maximum depth of the recursive retrieval is handy to


# prevent newbies from going too "deep" when they unwittingly start
# the recursive retrieval. The default is 5.
#reclevel = 5
reclevel = inf

# It can be useful to make Wget wait between connections. Set this to


# the number of seconds you want Wget to wait.
#wait = 0

# You can set retrieve quota for beginners by specifying a value


# optionally followed by 'K' (kilobytes) or 'M' (megabytes). The
# default quota is unlimited.
#quota = inf
quota = 1000m

# You can lower (or raise) the default number of retries when
# downloading a file (default is 20).
#tries = 20

# Setting this to off makes Wget not download /robots.txt. Be sure to


# know *exactly* what /robots.txt is and how it is used before changing
# the default!
#robots = on

# By default Wget uses "passive FTP" transfer where the client


# initiates the data connection to the server rather than the other
# way around. That is required on systems behind NAT where the client
# computer cannot be easily reached from the Internet. However, some
# firewalls software explicitly supports active FTP and in fact has
# problems supporting passive transfer. If you are in such
# environment, use "passive_ftp = off" to revert to active FTP.
#passive_ftp = off
passive_ftp = on

# The "wait" command below makes Wget wait between every connection.
# If, instead, you want Wget to wait only between retries of failed
# downloads, set waitretry to maximum number of seconds to wait (Wget
# will use "linear backoff", waiting 1 second after the first failure
# on a file, 2 seconds after the second failure, etc. up to this max).
waitretry = 10

# Set this to on to use timestamping by default:


timestamping = on

# It is a good idea to make Wget send your email address in a `From:'


# header with your request (so that server administrators can contact
# you in case of errors). Wget does *not* send `From:' by default.
#header = From: Your Name <username@site.domain>

# You can set up other headers, like Accept-Language. Accept-Language


# is *not* sent by default.
#header = Accept-Language: en

# You can set the default proxies for Wget to use for http and ftp.
# They will override the value in the environment.
#http_proxy = http://www.example.com/
#ftp_proxy = http://proxy.yoyodyne.com:18023/

# If you do not want to use proxy at all, set this to off.


#use_proxy = on

# You can customize the retrieval outlook. Valid options are default,
# binary, mega and micro.
#dot_style = default

# You can force creating directory structure, even if a single is being


# retrieved, by setting this to on.
#dirstruct = off
# To always back up file X as X.orig before converting its links (due
# to -k / --convert-links / convert_links = on having been specified),
# set this variable to on:
#backup_converted = off

# To have Wget follow FTP links from HTML files by default, set this
# to on:
#follow_ftp = off

Passo 3
Copie os 2 scripts abaixo para um diretório vazio.

wget.sh
#!/bin/bash
# Script que traz um site chamado portal feito em plone
# para outra máquina com o wget
# Autor: Roberto Romani
# data 22/11/2006

echo
echo Passo 1. Executa o wget. Aguarde ...
echo -n Inicio do Passo 1 em:
date
# Deve existir um arquivo .wgetrc no home do usuário com a configuração correta.
wget -nH -o wget_log.txt http://ENDEREÇO_MAQUINA_ORIGEM:PORTA/portal/archive_portal

echo
echo Passo 2 Altera os endereços não resolvidos pelo wget e retira a tag base
# O endereço a ser substituido bem como o endereço substituto são definidos no
# arquivo replace.sh

find portal/. -type f -name \*.css > /tmp/lista.$$


find portal/. -type f -name \*.html >> /tmp/lista.$$

LISTA="/tmp/lista.$$"

[ -z "$LISTA" ] && {
echo -e "${WHITE}Nenhum html foi encontrado."
}

while read ITEM


do
./dac_replace.sh "$ITEM"
done < $LISTA

echo
echo Passo 3 move arquivos css para a pasta Plone Default
mv portal/portal_css/ploneStyles*.css portal/portal_css/Plone\ Default/

echo
echo -n Fim do script "$0" em:
date

replace.sh
#!/bin/bash

cp -a "$1" "$1.tmp"
sed '/<base href="" \/>/d;s!ENDEREÇO_MAQUINA_ORIGEM/!ENDEREÇO_MAQUINA
_DESTINO!g' "$1.tmp" > "$1"
rm "$1.tmp"

Antes de rodar o script wget.sh, edite o script replace.sh alterando o


ENDEREÇO_MAQUINA_ORIGEM para o endereço de onde o site plone será copiado e
ENDEREÇO_MAQUINA _DESTINO para o endereço onde o a cópia será acessada. Não é
necessário colocar o nome do site, apena o endereço até o nome do site, ou seja o domínio e
diretório anteriores ao nome do site se houver. Caso o plone esteja respondendo em uma porta
específica informe também a porta no endereço origem.
Você precisa alterar também o ENDEREÇO_MAQUINA_ORIGEM na linha que executa o wget no
script wget.sh
Caso o nome do site plone não seja portal substitua a palavra portal pelo nome do site plone a ser
extraído.
Feitas essas alterações basta executar o script wget.sh ele deve gerar um diretório portal (ou com
o nome do site copiado) no diretório em que foi executado contendo uma cópia estática do site
plone original.
Agradecimentos especiais a Rodrigo Senra pela ajuda no desenvolvimento do script python e ao
Rubens Queiroz pela força com o shell script.

Script Shell de Backup Incremental


Por Júnior Mulinari
Uma dica de Shell Script para quem já teve problemas com espaço para armazenar backups.
Trabalho junto a equipe de manutenção do CodigoLivre (http://codigolivre.org.br), e os backups
dos projetos são enormes. Não teríamos espaço suficiente para armazenar os backups diários por 7
dias. A saída foi fazer de forma incremental. O script abaixo faz backups incrementais usando
recursos do GNU/Tar.
Basicamente o script cria um arquivo no formato:
<IDENTIFICA>-<número de incremento>-<dia>-<mês>-<ano>-<dia da semana>.tar.gz

e outro com o mesmo nome mais e prefixo .log, onde consta todos os arquivos e diretórios daquele
backup.
<IDENTIFICA> = é a identificação, caso você queira ter mais de um. No exemplo abaixo está
como "Backup".
<número de incremento> = é a seqüência do incremento, como abaixo estamos dizer para fazer 7
dias, ele irá de 1 a 7.
<dia> = dia da realização do backup.
<mês> = mês da realização do backup.
<ano> = ano da realização do backup.
<dia da semana> = dia da semana da realização do backup, exemplo Sun (Domingo), Mon
(Segunda), Tue (terça), ....
.tar.gz = é o formato de compactação e concatenação.
Você deve criar um arquivo ($PREFIX/etc/list.conf) com a lista de diretórios a serem
backupeados. Na lista deve conter o caminho completo do diretório, exemplo:
/usr/local/apache.
Também é possível criar um arquivo ($PREFIX/etc/exclude.list) com exceções de
backup, como por exemplo, arquivos de videos, musicas, etc.. exemplo:
*.mp3
*.avi

O formato do arquivo deve ser respeitado, colocando uma exclusão por linha.
Outras variáveis estão comentadas no script.

Inicio do script
#!/bin/bash
#
# Identificação dos arquivos
IDENTIFICA=Backup

# Numero de dias do ciclo de backup


DIAS=7

# E-mail do administrador
ADMIN=admin@exemplo.com.br

# A partir de onde ficarão os arquivos


PREFIX=/backup

# Onde os arquivos de backup e logs ficarão armazenados


DIR_DESTINO=/backup/arquivos

# Lista de arquivos a não serem backupeados


EXCLUDE=$PREFIX/etc/exclude.list

# Arquivos temporário do script


TEMP=/tmp/.backup.$$

# Arquivos de controle do GNU/Tar


INC=$PREFIX/etc/incremental.conf

# Formato da data
DATA=$(date +%d-%m-%Y-%a)

# Arquivo com a lista de diretórios a serem backupeados


LISTA=$(cat $PREFIX/etc/list.conf | grep ^\/ | sort | uniq)

# Arquivos gerado pelo script para controle de incremento


CONFIG=$PREFIX/etc/backup.conf
VOLTA=0
if [ ! -e $CONFIG ] ; then
touch $CONFIG
NUMERO=1
else
cp -f $CONFIG $CONFIG.bak
LINHAS=$(cat $CONFIG | grep ^[0-9] | wc -l)
[ $LINHAS -eq $((DIAS+1)) ] && VOLTA=1
FIRST=$(cat $CONFIG | grep ^[0-9]- | head -1)
LAST=$(cat $CONFIG | grep ^[0-9]- | tail -1)
OLD=$(echo $LAST | cut -f1 -d"-")
OLD=${OLD:-0}
BACKUP_OLD=$(echo $FIRST | cut -f1 -d"-")
FILE_OLD=$(echo $FIRST | cut -f- -d"-")
if [ $OLD -eq $DIAS ] ; then
NUMERO=1
mv -f $INC $INC.bak
else
NUMERO=$((OLD+1))
fi
fi

DESTINO=$DIR_DESTINO/$IDENTIFICA-$NUMERO-$DATA

SEND_MAIL () {
sendmail $ADMIN << FIMEMAIL
Subject: Backup CL $(date +%d-%m-%Y)

Backup realizado no arquivo: $DESTINO.tar.gz

FIMEMAIL

df -h > $DESTINO.log
tar --totals --ignore-failed-read --exclude-from=$EXCLUDE -zcvg $INC -f $DESTINO.tar.gz
$LISTA >> $DESTINO.log 2>&1
df -h >> $DESTINO.log
SEND_MAIL $DESTINO.log
echo $NUMERO-$DATA >> $CONFIG
if [ $VOLTA -eq 1 ] ; then
rm -f $DIR_DESTINO/$IDENTIFICA-$FILE_OLD.tar.gz
rm -f $DIR_DESTINO/$IDENTIFICA-$FILE_OLD.log
sed 1d $CONFIG > $TEMP
mv -f $TEMP $CONFIG
fi

Estamos usando o script há mais de um ano, e não tivemos problemas com a execução, o que está
faltando é um script que automatize a extração do backup.
Geração de arquivos de índice em html
Em dos sites que mantenho, chamado Contando Histórias, eu criei uma página onde relaciono todo
o conteúdo do site. Esta página é gerada através de um shell script que conta o número de
mensagens existentes, divide este número por dois, e monta uma tabela com duas colunas. Para
entender melhor o que é feito, nada melhor do que visitar a página de arquivo do site.
Vamos então ao script e à explicação de seu funcionamento.
#!/bin/bash

homedir=/html/contandohistorias

cd $homedir/html

# O laço que se segue trabalha


# sobre todos os arquivos do diretório
# /html/contandohistorias/inc que
# tenham a terminação "inc". Estes são arquivos
# no formato html, gerados pelo software txt2tags
# (txt2tags.sourceforge.net).

for file in *.inc


do

# O arquivo php final é formado a partir do nome do


# arquivo terminado em "inc". Este nome é atribuído
# à variável $php, definida no próximo comando

php=`echo $file | sed 's/inc/php/'`

# No arquivo html a primeira linha contém o título


# da mensagem, formatada como título de nível 1
# (H1). O título é extraído desta linha com o
# comando sed e em seguida convertido em uma
# referência html, para ser usada mais tarde na
# montagem do arquivo geral.

sed -n 1p $file | sed 's:<H1>::;s:</H1>:</A>:' | sed "s:^:<BR><A HREF=/historias/$php>:" >>


/tmp/idx.tmp

# Usamos o comando tac para inverter a ordem das


# mensagens, deixando as mais recentes em primeiro
# lugar na listagem.

tac /tmp/idx.tmp > /tmp/idx.$$ && mv /tmp/idx.$$ /tmp/idx.tmp


done

cp /tmp/idx.tmp $homedir/inc/listagem.inc

# Fazemos a seguir a contagem de linhas do arquivo


# idx.tmp, que representa o total de mensagens já
# enviadas. A variável $half é obtida dividindo
# por 2 o número total de linhas do arquivo

lines=`wc -l /tmp/idx.tmp | awk '{print $1}'`


half=`expr $lines / 2`

# Usamos agora o comando split para partir o


# arquivo em dois. A diretiva "-l" sinaliza que
# a divisão do arquivo deve ser feita levando-se
# em conta o número de linhas (lines).

split -l $half /tmp/idx.tmp

# o comando split gera dois arquivos "xaa" e


# "xbb". Estes dois arquivos formarão as duas
# colunas da tabela.

mv xaa $homedir/inc/coluna1.inc
mv xab $homedir/inc/coluna2.inc

# A seguir, fazemos a construção do arquivo


# php final, através da inclusão dos diversos
# elementos da página: cabeçalho (Head.inc), barra
# de navegação (navbar.inc), barra da esquerda
# (esquerda.inc), e as duas colunas da tabela
# (coluna1.inc e coluna2.inc).

echo "<?PHP include(\"/html/contandohistorias/inc/Head.inc\"); ?>


<div id=top>
<H1>Contando Histórias</H1>
<?PHP include(\"/html/contandohistorias/inc/navbar.inc\"); ?>
</div>

<div id=mainleft>
<?PHP include(\"/html/contandohistorias/inc/esquerda.inc\"); ?>
</div>

<div id=maincenter>
<h1>Arquivo Lista Contando Histórias</h1>
<table>
<tr valign=top>
<td>
<?PHP include(\"/html/contandohistorias/inc/coluna1.inc\"); ?>
</td>
<td>
<?PHP include(\"/html/contandohistorias/inc/coluna2.inc\"); ?>
</td>
</tr>
</table>
</html>
</body>" > $homedir/arquivo.php
rm /tmp/idx.*

Script para geração de thumbnails


O script de hoje é uma colaboração de Fabio Costa e seu objetivo é gerar uma página com
miniaturas das imagens de um determinado diretório.
O tratamento das imagens é feito com o utilitário convert, parte do pacote ImageMagick.
#!/bin/bash

i=0
thumbspage="thumbs.html"
thumbsdir="thumbs"
thumbssuffix="thumbs"
pagename="Página de miniaturas"
force=

while getopts 'hn:p:d:s:f' OPT_LETRA


do
case $OPT_LETRA in
'h') echo "SCRIPT para geração de páginas de thumbanils"
echo
echo "thumbs_gen [OPÇÕES]"
echo
echo "Opções:"
echo
echo "-h retorna essa mensagem de erro"
echo "-n [nome_pagina] cabeçalho da página de miniaturas"
echo "-p [arq_pagina] nome do arquivo da página"
echo "-d [dir_thumbs] diretório onde ficarão as miniaturas das"
echo " imagens do diretorio"
echo "-s [sufixo] sufixo a ser adicionado ao nome das miniaturas"

echo "-f sobrescreve arquivos e diretórios existentes"


exit 0
;;
'n') pagename=$OPTARG
;;
'd') thumbsdir=$OPTARG
;;
's') thumbssufix=$OPTARG
;;
'p') thumbspage=$OPTARG
;;
'f') force="1"
esac
done

cabecalho="
<HTML>
<HEAD>
<TITLE>
$pagename
</TITLE>
<BODY>
<TABLE width=100%>
<TR>
<H1>$pagename</H1>
"

if [ ! -d "$thumbsdir" ]
then
mkdir $thumbsdir
else
if [ -z "$force" ]
then
echo "Atenção: Diretório de thumbnails existe! Saindo..."
exit 1
fi
fi

if [ -f "$thumbspage" -a -z "$force" ]
then
echo "Atenção: Arquivo HTML da galeria existe! Saindo..."
exit 1
fi

echo $cabecalho > $thumbspage

for file in `ls | grep -E -e '(gif|jpg|png|bmp)'`; do


mainfile=`echo $file | cut -d. -f1`
thumbfile="${thumbsdir}/${mainfile}_${thumbssuffix}.jpg"
convert -scale 300x200 $file $thumbfile
case $i in
0)
linha="
</tr>
<tr>
<td>
<a href='$file'>
<img src='$thumbfile'>
</a>
</td>"
echo $linha >> $thumbspage
i=1
;;
1)
linha="
<td>
<a href='$file'>
<img src='$thumbfile'>
</a>
</td>"
echo $linha >> $thumbspage
i=0
;;
esac
done

rodape='
</TR>
</TABLE>
</HTML>'

echo $rodape >> $thumbspage

Here Strings
Primeiro um programador com complexo de inferioridade criou o redirecionamento de entrada e
representou-o com um sinal de menor (<) para representar seus sentimentos. Em seguida, outro
sentindo-se pior ainda, criou o here document representando-o por dois sinais de menor (<<)
porque sua fossa era maior. O terceiro, pensou: "estes dois não sabem o que é estar por baixo"...
Então criou o here strings representado por três sinais de menor (<<<).
Brincadeiras a parte, o here strings é utilíssimo e, não sei porque, é um perfeito desconhecido. Na
pouquíssima literatura que sobre o tema, nota-se que o here strings é freqüentemente citado como
uma variante do here document, com a qual discordo pois sua aplicabilidade é totalmente diferente
daquela.
Sua sintaxe é simples:
$ comando <<< $cadeia

Onde cadeia é expandida e alimenta a entrada primária (stdin) de comando.


Como sempre, vamos direto aos exemplos dos dois usos mais comuns para que vocês próprios
tirem suas conclusões.

Uso #1
Substituindo a famigerada construção echo "cadeia" | comando, que força um fork,
criando um subshell e onerando o tempo de execução. Vejamos alguns exemplos:
$ a="1 2 3"
$ cut -f 2 -d ' ' <<< $a # normalmente faz-se: echo $a | cut -f 2 -d ' '
2
$ echo $NomeArq
Meus Documentos
$ tr "A-Z " "a-z_" <<< $NomeArq Substituindo o echo ... | tr ...
meus_documentos
$ bc <<<"3 * 2"
6
$ bc <<<"scale = 4; 22 / 7"
3.1428

Para mostrar a melhoria no desempenho, vamos fazer um loop de 500 vezes usando o exemplo
dados para o comando tr:
$ time for ((i=1; i<= 500; i++)); { tr "A-Z " "a-z_" <<< $NomeArq >/dev/null; }

real 0m3.508s
user 0m2.400s
sys 0m1.012s
$ time for ((i=1; i<= 500; i++)); { echo $NomeArq | tr "A-Z " "a-z_" >/dev/null; }

real 0m4.144s
user 0m2.684s
sys 0m1.392s

Veja agora esta seqüência de comandos com medidas de tempo:


$ time for ((i=1;i<=100;i++)); { who | cat > /dev/null; }

real 0m1.435s
user 0m1.000s
sys 0m0.380s
$ time for ((i=1;i<=100;i++)); { cat <(who) > /dev/null; }

real 0m1.552s
user 0m1.052s
sys 0m0.448s
$ time for ((i=1;i<=100;i++)); { cat <<< $(who) > /dev/null; }

real 0m1.514s
user 0m1.056s
sys 0m0.412s

Observando este quadro você verá que no primeiro usamos a forma convencional, no segundo
usamos um named pipe temporário para executar uma substituição de processos e no terceiro
usamos here string. Notará também que ao contrário do exemplo anterior, aqui o uso de here string
não foi o mais veloz. Mas repare bem que neste último caso o comando who está sendo executado
em um subshell e isso onerou o processo como um todo.
Vejamos uma forma rápida de inserir uma linha como cabeçalho de um arquivo:
$ cat num
1 2
3 4
5 6
7 8
9 10
$ cat - num <<< "Impares Pares"
Impares Pares
1 2
3 4
5 6
7 8
9 10

Uso #2
Outra forma legal de usar o here string é casando-o com um comando read, não perdendo de vista o
que aprendemos sobre IFS (veja mais sobre está variável no Papo de Botequim). O comando cat
com as opções -vet mostra o <ENTER> como $, o <TAB> como ^I e os outros caracteres de
controle com a notação ^L onde L é uma letra qualquer. Vejamos então o conteúdo de uma variável
e depois vamos ler cada um de seus campos:
Também podemos ler direto para um vetor (array) veja:
$ echo $Frutas
Pera:Uva:Maçã
$ IFS=:
$ echo $Frutas
Pera Uva Maçã # Sem as aspas o shell mostra o IFS como branco
$ echo "$Frutas"
Pera:Uva:Maçã # Ahhh, agora sim!
$ read -a aFrutas <<< "$Frutas" # A opção -a do read, lê para um vetor
$ for i in 0 1 2
> do
> echo ${aFrutas[$i]} # Imprimindo cada elemento do vetor
> done
Pera
Uva
Maçã

Bem , por enquanto é só! Mas se vocês descobrirem algo de novo no uso de here strings, façam o
favor de nos enviar para tornar o Cantinho um cantão cheio de dicas de bash.

IFS - Inter Field Separator


Lendo o artigo intitulado Atribuição de Valores a Variáveis em Shell Scripts me senti tentado a
escrever esta dica.
O shell tem uma variável interna chamada IFS - Inter Field Separator (será Tabajara? :) - cujo
valor default podemos obter da seguinte forma:
$ echo "$IFS" | od -h
0000000 0920 0a0a
0000004

O programa od com a opção -h foi usado para gerar um dump hexadecimal da variável. E lá
podemos ver:
Valor Hexadecimal Significado
09 <TAB>
20 Espaço
0a <ENTER>
Ou seja os separadores entre campos (tradução livre de IFS) default são o <TAB>, o espaço em
branco e o <ENTER>. O IFS é usado em diversas instruções, mas seu uso é muito comum em par
com o for e/ou com o read. Vejamos um exemplo semelhante ao dado no Cantinho do Shell de
15/12/2006, que me inspirou a escrever este artigo.
$ cat script1.sh
#!/bin/sh
while read linha
do
awk -F: '{print $1}' /etc/passwd > /dev/null
done < /etc/passwd

Como podemos ver este script não faz nada (sua única saída foi enviada para /dev/null para não
deturpar os tempos de execução), mas vamos usá-lo para avaliação dos tempos de execução.
Este script foi alterado, trocando o awk pelo cut, e ficando com a seguinte cara:
$ cat script2.sh
#!/bin/sh
while read linha
do
echo $linha | cut -f1 -d: > /dev/null
done < /etc/passwd

Mais uma outra alteração, usando 99% de intrínsecos do Shell (exceto o comando echo) desta vez
tomando partido do IFS:
$ cat script3.sh
#!/bin/sh
IFS=:
while read user lixo
do
echo $lixo > /dev/null
done < /etc/passwd

Neste último exemplo, transformamos o separador padrão em dois-pontos (:) e usamos sua
propriedade em conjunto com o read, isto é, o primeiro campo veio para a variável user e o resto
para a variável lixo.
Em seguida, fiz um script usando Shell quase puro (novamente o echo foi o vilão). Repare a
construção ${linha%%:*}, que é um intrínseco (built-in) do Shell que serve para excluir da
variável linha o maior casamento com o padrão especificado (:* - que significa "de dois-pontos em
diante"), ou seja, excluiu de linha tudo a partir do último dois-pontos, contado da direita para a
esquerda.
Dica: quem não conhece o macete acima, não pode deixar de ler a seção referente a expansão de
parâmetros em: http://twiki.softwarelivre.org/bin/view/TWikiBar/TWikiBarPapo009
$ cat script4.sh
#!/bin/sh
while read linha
do
echo ${linha%%:*} > /dev/null
done < /etc/passwd

Para finalizar, adaptei o script escrito pelo incansável Rubens Queiroz que, exceto pelo awk, é Shell
puro.
$ cat script5.sh
#!/bin/sh
for user in `awk -F: '{print $1}' /etc/passwd`
do
echo $user > /dev/null
done
Agora, o mais importante: reparem os tempos da execução de cada um deles:
$ time script1.sh

real 0m0.123s
user 0m0.032s
sys 0m0.032s
$ time script2.sh

real 0m0.297s
user 0m0.152s
sys 0m0.084s
$ time script3.sh

real 0m0.012s
user 0m0.004s
sys 0m0.004s
$ time ./script4.sh

real 0m0.012s
user 0m0.004s
sys 0m0.008s
$ time ./script5.sh

real 0m0.014s
user 0m0.012s
sys 0m0.004s

Reparem que estas diferenças de tempo foram obtidas para um arquivo com somente 29 linhas.
Veja:
$ wc -l /etc/passwd
29 /etc/passwd

Um outro uso interessante do IFS é o que vemos a seguir, primeiramente usando o IFS default que
como vimos é <TAB>, Espaço e <ENTER>:
$ Frutas="Pera Uva Maçã"
$ set - $Frutas
$ echo $1
Pera
$ echo $3
Maçã

Agora, vamos alterar o IFS para fazer o mesmo com uma variável qualquer, e para tal vamos
continuar usando o famigerado /etc/passwd:
$ Root=$(head -1 /etc/passwd)
$ echo $Root
root:x:0:0:root:/root:/bin/bash
$ oIFS="$IFS"
$ IFS=:
$ set - $Root
$ echo $1
root
$ echo $7
/bin/bash
$ IFS="$oIFS"

Senhores, neste artigo pretendi mostrar duas coisas:


• O uso do IFS que, infelizmente para nós, é uma variável pouco conhecida do Shell; e
• Que quanto mais intrínsecos do Shell usamos, mais veloz e performático fica o script.

touch e redirecionamento
99% das pessoas usam o comando touch para criar um arquivo, porém veja só esta comparações
de tempo:
$ time for ((i=1; i<=200; i++)); { touch xpto; }

real 0m1.572s
user 0m1.012s
sys 0m0.428s

$ time for ((i=1; i<=200; i++)); { > xpto; }

real 0m0.007s
user 0m0.008s
sys 0m0.000s

Como era de se esperar o tempo decorrido para criar 200 vezes o arquivo xpto é muito maior
usando o touch do que usando redirecionamento. A explicação para isso é bastante simples:
1. O touch foi feito para alterar o timestamp de arquivos, e por isso seu código é um
pouquinho pesado. Então quando você usa este comando, você perde um tempo para
carregar o módulo e outro para executá-lo a nível de kernel;
2. quando você usa redirecionamento, não existe nenhuma carga de código (é um intrínseco do
shell) e é executado localmente.

O comando xargs
Existe um comando, cuja função primordial é construir listas de parâmetros e passá-la para a
execução de outros programas ou instruções. Este comando é o xargs e deve ser usado da seguinte
maneira:
xargs [comando [argumento inicial]]

Caso o comando, que pode ser inclusive um script Shell, seja omitido, será usado por default o
echo.
O xargs combina o argumento inicial com os argumentos recebidos da entrada padrão, de forma a
executar o comando especificado uma ou mais vezes.
Exemplo:
Vamos produrar em todos os arquivos abaixo de um determinado diretório uma cadeia de caracteres
usando o comando find com a opção type f para pesquisar somente os arquivos normais,
desprezando diretórios, arquivos especiais, arquivos de ligações, etc, e vamos torná-la mais genérica
recebendo o nome do diretório inicial e a cadeia a ser pesquisada como parâmetros. Para isso
fazemos:
$ cat grepr
#
# Grep recursivo
# Pesquisa a cadeia de caracteres definida em $2 a partir do diretorio $1
#
find $1 -type f -print|xargs grep -l "$2"

Na execução deste script procuramos, a partir do diretório definido na variável $1, todos os
arquivos que continham a cadeia definida na variável $2.
Exatamente a mesma coisa poderia ser feito se a linha do programa fosse a seguinte:
find $1 -type f -exec grep -l "$2" {} \;

Este processo tem duas grandes desvantagens sobre o anterior:


I. A primeira é bastante visível: o tempo de execução deste método é muito superior ao
daquele, isso porque o grep será feito em cada arquivo que lhe for passado pelo find,
um-a-um, ao passo que com o xargs, será passada toda, ou na pior das hipóteses, a
maior parte possível, da lista de arquivos gerada pelo find;
II. Dependendo da quantidade de arquivos encontrados que atendem ao find, poderemos
ganhar aquela famosa e fatídica mensagem de erro "Too many arguments" indicando
um estouro da pilha de execução do grep. Como foi dito no item anterior, se usarmos
o xargs ele passará para o grep a maior quantidade de parâmetros possível, suficiente
para não causar este erro, e caso necessário executará o grep mais de uma vez.
ATENÇÃO! Aê pessoal do linux que usa o ls colorido que nem porta de tinturaria: nos exemplos a
seguir que envolvem esta instrução, você devem usar a opção --color=none, senão existem grandes
chances dos resultados não ocorrerem como o esperado.
Vamos agora analisar um exemplo que é mais ou menos o inverso deste que acabamos de ver. Desta
vez, vamos fazer um script para remover todos os arquivos do diretório corrente, pertencentes a um
determinado usuário.
A primeira idéia que surge é, como no caso anterior, usar um comando find, da seguinte maneira:
$ find . -user cara -exec rm -f {} \;

Quase estaria certo, o problema é que desta forma você removeria não só os arquivos do cara no
diretório corrente, mas também de todos os outros subdiretórios "pendurados" neste. Vejamos então
como fazer:
$ ls -l | grep " cara " | cut -c55- | xargs rm

Desta forma, o grep selecionou os arquivos que continham a cadeia cara no diretório corrente
listado pelo ls -l. O comando cut pegou somente o nome dos arquivos, passando-os para a remoção
pelo rm usando o comando xargs como ponte.
Duvidas? julio.neves@gmail.com
Deseja fazer curso de Programação em Shell? julio.neves@tecnohall.com.br
Mais xargs
Continuamos hoje o que começamos no último artigo, isto é, a ver as facilidades oferecidas pelo
comando xargs.
O xargs é também uma excelente ferramenta de criação de one-liners (scripts de somente uma
linha). Veja este para listar todos os donos de arquivos (inclusive seus links) "pendurados" no
diretório /bin e seus subdiretórios.
$ find /bin -type f -follow | \
xargs ls -al | tr -s ' ' | cut -f3 -d' ' | sort -u

Muitas vezes o /bin é um link (se não me engano, no Solaris o é) e a opção -follows obriga o find a
seguir o link. O comando xargs alimenta o ls -al e a seqüência de comandos seguinte é para pegar
somente o 3º campo (dono) e classificá-lo devolvendo somente uma vez cada dono (opção -u do
comando sort).

Você pode usar as opções do xargs para construir comandos extremamente poderosos. Para
exemplificar isso e começar a entender as principais opções desta instrução, vamos supor que temos
que remover todos as arquivos com extensão .txt sob o diretório corrente e apresentar os seus nomes
na tela. Veja o que podemos fazer:
$ find . -type f -name "*.txt" | \
xargs -i bash -c "echo removendo {}; rm {}"

A opção -i do xargs troca pares de chaves ({}) pela cadeia que está recebendo pelo pipe (|). Então
neste caso as chaves ({}) serão trocadas pelos nomes dos arquivos que satisfaçam ao comando find.

Olha só a brincadeira que vamos fazer com o xargs:


$ ls | xargs echo > arq.ls
$ cat arq.ls
arq.ls arq1 arq2 arq3
$ cat arq.ls | xargs -n1
arq.ls
arq1
arq2
arq3

Quando mandamos a saída do ls para o arquivo usando o xargs, comprovamos o que foi dito
anteriormente, isto é, o xargs manda tudo que é possível (o suficiente para não gerar um estouro de
pilha) de uma só vez. Em seguida, usamos a opção -n 1 para listar um por vez. Só para dar certeza
veja o exemplo a seguir, quando listaremos dois em cada linha:
$ cat arq.ls | xargs -n 2
arq.ls arq1
arq2 arq3

Mas a linha acima poderia (e deveria) ser escrita sem o uso de pipe (|), da seguinte forma:
$ xargs -n 2 < arq.ls

Outra opção legal do xargs é a -p, na qual o xargs pergunta se você realmente deseja executar o
comando. Digamos que em um diretório você tenha arquivo com a extensão .bug e .ok, os .bug
estão com problemas que após corrigidos são salvos como .ok. Dá uma olhadinha na listagem deste
diretório:
$ ls dir
arq1.bug
arq1.ok
arq2.bug
arq2.ok
...
arq9.bug
arq9.ok

Para comparar os arquivos bons com os defeituosos, fazemos:


$ ls | xargs -p -n2 diff -c
diff -c arq1.bug arq1.ok ?...y
....
diff -c arq9.bug arq9.ok ?...y

Para finalizar, o xargs também tem a opção -t, onde vai mostrando as instruções que montou antes
de executá-las. Gosto muito desta opção para ajudar a depurar o comando que foi montado.

Então podemos resumir o comando de acordo com a tabela a seguir:


Opção Ação
-i Substitui o par de chaves ({}) pelas cadeias recebidas
-nNum Manda o máximo de parâmetros recebidos, até o máximo de Num para o comando a ser
executado
-lNum Manda o máximo de linhas recebidas, até o máximo de Num para o comando a ser
executado
-p Mostra a linha de comando montada e pergunta se deseja executá-la
-t Mostra a linha de comando montada antes de executá-la

Named pipe
Um outro tipo de pipe é o named pipe, que também é chamado de FIFO. FIFO é um acrônimo de
"First In First Out" que se refere à propriedade em que a ordem dos bytes entrando no pipe é a
mesma que a da saída. O name em named pipe é, na verdade, o nome de um arquivo. Os arquivos
tipo named pipes são exibidos pelo comando ls como qualquer outro, com poucas diferenças:
$ ls -l fifo1
prw-r-r-- 1 julio dipao 0 Jan 22 23:11 fifo1|

o p na coluna mais à esquerda indica que fifo1 é um named pipe. O resto dos bits de controle de
permissões, quem pode ler ou gravar o pipe, funcionam como um arquivo normal. Nos sistemas
mais modernos uma barra vertical (|) colocado ao fim do nome do arquivo, é outra dica, e nos
sistemas LINUX, onde a opção de cor está habilitada, o nome do arquivo é escrito em vermelho por
default.
Nos sistemas mais antigos, os named pipes são criados pelo programa mknod, normalmente situado
no diretório /etc. Nos sistemas mais modernos, a mesma tarefa é feita pelo mkfifo. O programa
mkfifo recebe um ou mais nomes como argumento e cria pipes com estes nomes. Por exemplo, para
criar um named pipe com o nome pipe1, faça:
$ mkfifo pipe1

Exemplo:
Como sempre, a melhor forma de mostrar como algo funciona é dando exemplos. Suponha que nós
tenhamos criado o named pipe mostrado anteriormente. Em outra sessão ou uma console virtual,
faça:
$ ls -l > pipe1

e em outra, faça:
$ cat < pipe1

A saída do comando executado na primeira console foi exibida na segunda. Note que a ordem em
que os comandos ocorreram não importa.
Se você prestou atenção, reparou que o primeiro comando executado, parecia ter "pendurado". Isto
acontece porque a outra ponta do pipe ainda não estava conectada, e então o kernel suspendeu o
primeiro processo até que o segundo "abrisse" o pipe. Para que um processo que usa pipe não fique
em modo de wait, é necessário que em uma ponta do pipe tenha um processo "tagarela" e na outra
um "ouvinte".
Uma aplicação muito útil dos named pipes é permitir que programas sem nenhuma relação possam
se comunicar entre si, os named pipes também são usados para sincronizar processos, já que em um
determinado ponto você pode colocar um processo para "ouvir" ou para "falar" em um determinado
named pipe e ele daí só sairá, se outro processo "falar" ou "ouvir" aquele pipe.

Como usar o SSH dentro de um loop


Colaboração: Ricardo Barioni
Já tentou executar um comando SSH dentro de um loop?
while
do ...
ssh ...
done

Pois é, o comando SSH encerra o loop na primeira passada, e vc fica feliz pensando que está
rodando até o fim do while. Ledo engado.
Para isso, coloque a opção "-n" no SSH, assim:
while
do ...
ssh -n ....
done

Acontece que o SSH envia um "exit(0)" em sua saída, o qual é interpretado no sub-shell criado pelo
loop, e lá se vai a conclusão seu loop pro espaço.

Macetes diversos
Colaboração: Júlio Neves
Dica 1 : Repare esta seqüência de comandos no prompt:
/home/jneves> cd bin
-bash: cd: bin: No such file or directory
/home/jneves> CDPATH=/usr/local
/home/jneves> cd bin
/usr/local/bin
/usr/local/bin>

Com esse exemplo eu quis mostrar que a variável CDPATH atua como o PATH, sendo que esta
última contém os diretórios que devem ser percorridos na procura de arquivos e a primeira contém
os os diretórios que devem ser percorridos na procura por subdiretórios.
Para agilizar o meu lado, repare o conteúdo do meu CDPATH:
/home/jneves> echo $CDPATH
.:..:~

Desta forma, quando eu faço um cd, os subdiretórios serão pesquisados no diretório corrente, no seu
diretório pai e no meu diretório home, nesta ordem.
Dica 2 : Já que falamos na variável PATH, que tal usarmos o comando tr para facilitar a
legibilidade do seu conteúdo:
/home/jneves> echo $PATH
/bin:/usr/bin:/sbin:~:.
/home/jneves> echo $PATH | tr ':' '\n'
/bin
/usr/bin
/sbin
~
.

Neste exemplo o tr trocou os dois-pontos (:) por new-line (ENTER), facilitando a leitura do
conteúdo da variável.
Dica 3 - Quando você executa diversos comandos encadeados em um pipe (|), o return code dado
por echo $? reflete apenas o resultado de saída do último comando executado no pipe. O array
PIPESTATUS, por sua vez, armazena em cada elemento o resultado respectivo de cada um dos
comandos do pipe. ${PIPESTATUS[0]} tem o return code do primeiro comando, $
{PIPESTATUS[1]} contém o return code do segundo, e assim por diante.
O exemplo a seguir mostra um script que executa um pipe de três comandos, e imprime o return
code de cada um dos comandos:
/home/jneves> date | grep Wed | wc -l
/home/jneves> echo ${PIPESTATUS[*]}
0 1 0

Na última linha temos a impressão do array ${PIPESTATUS}: 0 (zero) indicando o sucesso do


primeiro comando, 1 indicando que o grep falhou ao procurar pela cadeia Wed, e novamente 0
(zero) para o sucesso do comando wc -l.

O Comando Paste
Colaboração: Júlio Neves
O paste é um comando pouco usado por sua sintaxe ser pouco conhecida. Vamos brincar com 2
arquivos criados da seguinte forma:
$ seq 10 > inteiros
$ seq 2 2 10 > pares

Para ver o conteúdo dos arquivos criados, vamos usar o paste na sua forma careta:
$ paste inteiros pares
1 2
2 4
3 6
4 8
5 10
6
7
8
9
10

Agora vamos transformar a coluna do pares em linha:


$ paste -s pares
2 4 6 8 10

O separador default é <TAB>, mas isso pode ser alterado com a opção -d. Então para calcular a
soma do conteúdo de pares primeiramente faríamos:
$ paste -s -d'+' pares # também poderia ser -sd'+'
2+4+6+8+10

e depois passaríamos esta linha para a calculadora (bc) e então ficaria:


$ paste -s -d'+' pares | bc
30

Assim sendo, para calcular o fatorial do número contido em $Num, basta:


$ seq $Num | paste -sd'*' | bc

Substituição de Processos
Colaboração: Júlio Neves
Hoje vou mostrar que o Shell também usa os named pipes de uma maneira bastante singular, que é
a substituição de processos (process substitution). Uma substituição de processos ocorre quando
você põe um < ou um > grudado na frente do parêntese da esquerda. Teclando-se o comando:
$ cat <(ls -l)

Resultará no comando ls -l executado em um subshell como é normal, porém redirecionará a saída


para um named pipe temporário, que o Shell cria, nomeia e depois remove. Então o cat terá um
nome de arquivo válido para ler (que será este named pipe e cujo dispositivo lógico associado é
/dev/fd/63), e teremos a mesma saída que a gerada pela listagem do ls -l, porém dando um ou mais
passos que o usual.
Como poderemos constatar isso? Fácil... Veja o comando a seguir:
$ ls -l >(cat)
l-wx------ 1 jneves jneves 64 Aug 27 12:26 /dev/fd/63 -> pipe:[7050]

É... Realmente é um named pipe.


Você deve estar pensando que isto é uma maluquice de nerd, né? Então suponha que você tenha 2
diretórios: dir e dir.bkp e deseja saber se os dois estão iguais (aquela velha dúvida: será que meu
backup está atualizado?). Basta comparar os dados dos arquivos dos diretórios com o comando
cmp, fazendo:
$ cmp <(cat dir/*) <(cat dir.bkp/*) || echo backup furado

ou, melhor ainda:


$ cmp <(cat dir/*) <(cat dir.bkp/*) >/dev/null || echo backup furado

Este é um exemplo meramente didático, mas são tantos os comandos que produzem mais de uma
linha de saída, que serve como guia para outros. Eu quero gerar uma listagem dos meus arquivos,
numerando-os e ao final dar o total de arquivos do diretório corrente:
while read arq
do
((i++)) # assim nao eh necessario inicializar i
echo "$i: $arq"
done < <(ls)
echo "No diretorio corrente (``pwd``) existem $i arquivos"

Tá legal, eu sei que existem outras formas de executar a mesma tarefa. Mas tente fazer usando
while, sem usar substituição de processos que você verá que este método é muito melhor.

"case" em bash
Colaboração: Rodrigo Bernardo Pimentel
Quando se quer testar uma série de condições, pode-se usar "if", "elif" e "else". Porém, quando o
teste é para um mesmo valor de variável, a repetição pode-se tornar inconveniente:
if [ "$REPOSTA" = "sim" ]; then
faz_coisas

elif [ "$RESPOSTA" = "nao"]; then


exit 0

elif [ "$RESPOSTA" = "depende_do_tempo" ]; then


faz_coisas_se_o_tempo_permitir

elif [ "$RESPOSTA" = "depende_do_humor" ]; then


faz_coisas_se_o_humor_permitir

...

E por aí vai. As checagens são necessárias, afinal de contas precisamos reagir diferentemente a cada
uma das condições. Mas a repetição de 'elif [ "$RESPOSTA" = "..."]; then' torna-se cansativa.
Para esse tipo de problema, existe uma construção em bash chamada "case" (existe também em
outras linguagens como C). Sua estrutura é:
case "$variavel" in
primeira_opcao)
comando1
comando2
...
;;

segunda_opcao)
outro_comando
ainda_outro
...
;;
...

*)
ultimo_comando

esac

Isso testa "$variavel" com cada uma das opções. Quando achar uma adequada, executa os
comandos até ";;" e sai do loop (não tenta outras opções mais adiante). O nosso exemplo com "if"
acima ficaria:
case "$RESPOSTA" in
sim)
faz_coisas
;;

nao)
exit 0
;;

depende_do_tempo)
faz_coisas_se_o_tempo_permitir
;;

depende_do_humor)
faz_coisas_se_o_humor_permitir
;;

*)
echo 'NDA!'

esac

Notem que "*" é um "catchall", ou seja, se o valor da variável "RESPOSTA" não for "sim", "nao",
"depende_do_tempo" ou "depende_do_humor", serão executandos os comandos após o "*)" (que
não precisa terminar em ";;").
Normalmente, os scripts de inicialização do sistema (em /etc/init.d ou /etc/rc.d/init.d, dependendo
da distribuição) necessitam de um parâmetro, um dentre "start", "stop", "status", "restart" ou
"reload" (às vezes mais). A situação ideal para se usar um "case", certo? O pessoal das distribuições
também acha. Portante, se você quiser exemplos de "case", procure nos scripts de inicialização da
sua distribuição!

Bash - Estruturas Básicas


Colaboração: Rodrigo Bernardo Pimentel
É comum queremos executar a mesma função, ou uma função parecida, sobre uma série de
argumentos, um por vez. Em bash, há duas estruturas básicas pra isso: for e while.
O for, em bash, é diferente do for em linguagens como C ou Perl. Nessas linguagens, o for é
simplesmente um while mais completo. Em bash, o for atua sobre uma seqüência de parâmetros
(não necessariamente numéricos ou seqüenciais). Por exemplo:
[rbp@muppets ~]$ for i in 1 2 3 4 5; do echo $i; done
1
2
3
4
5
[rbp@muppets ~]$

Ou,
[root@muppets ~]$ for login in rbp sizinha queiroz; do adduser $login; done
[root@muppets ~]$

Você pode inclusive usar o for a partir de uma saída de comando. Por exemplo, digamos que você
tenha um arquivo com uma série de nomes de usuários a serem acrescentados ao sistema, como no
exemplo acima:
[root@muppets ~]$ for login in `cat /tmp/usuarios`; do adduser $login; done
[root@muppets ~]$

O while, por sua vez, tem funcionamento semelhante à maioria das linguagens procedurais mais
comuns. Um comando ou bloco de comandos continua a ser executado enquanto uma determinada
condição for verdadeira. Por exemplo, imitando o exemplo acima:
[rbp@muppets ~]$ while [ $i -le 5 ]; do echo $i; i=$(($i + 1)); done
1
2
3
4
5
[rbp@muppets ~]$

Aos poucos:
while [ $i -le 5 ]

O while deve ser seguido de "verdadeiro" ou "falso". Se for "verdadeiro", o bloco é executado e o
teste é repetido. Se for falso, não.
No caso, [ $i -le 5 ] é uma forma de usarmos o comando test. Esse comando testa uma
expressão e retorna verdadeiro ou falso. Poderia ser escrito como:
while test $i -le 5

$i -le 5 é a expressão testada. O programa test aceita alguns operadores. Entre eles:

-lt (less than) primeiro argumento é menor do que o segundo


-le (less or equal) primeiro argumento é menor ou igual ao segundo
-gt (greater than) primeiro argumento é maior do que o segundo
-ge (greater or equal) primeiro argumento é maior ou igual ao segundo
= primeiro argumento é igual ao segundo
!= primeiro argumento é diferente do segundo
O programa test pode também fazer testes unários, ou seja, com só um argumento. Por
exemplo,
arq="/tmp/arquivo"
tmp=$arq
i=1
while [ -e $tmp ]; do
i=$(($i+1))
tmp=$arq$i
done
touch $tmp

Esse scriptzinho (note que não o fiz na linha de comando, mas indentado, para usá-lo a partir de um
arquivo; funciona dos dois jeitos) só sai do loop quando achar um nome de arquivo que não exista.
Ou, de forma mais didática, "enquanto existir (-e) o arquivo cujo nome está na variável $tmp, ele
continua executado o bloco de comandos".
Alguns dos operadores unários mais comuns são:
-e arquivo ou diretório existe
-f é um arquivo (em oposição a ser um diretório)
-d é um diretório
-x arquivo tem permissão de execução para o usuário atual
-w arquivo tem permissão de escrita pelo usuário atual
-r arquivo tem permissão de leitura pelo usuário atual
Para mais detalhes, man test.
Continuando:
do echo $i

Logo após um while <teste>, é preciso iniciar o primeiro comando com do. Os seguintes (se
houver), não.
A próxima linha mostra um exemplo de algo que não tem nada a ver com a estrutura do while em
si, mas é um truquezinho legal de bash:
i=$(($i + 1))

A construção $((expressão)) é um operador matemático em bash. Isso é expandido para o


resultado da expressão. Por exemplo,
[rbp@muppets ~]$ echo $((2 + 3)) =
5
[rbp@muppets ~]$ echo $((2 - 3)) # Funciona com números negativos =
-1
[rbp@muppets ~]$ echo $((2 * 3)) =
6
[rbp@muppets ~]$ echo $((10 / 2)) =
5
[rbp@muppets ~]$ echo $((3 / 2)) # Não usa casas decimais =
1
[rbp@muppets ~]$ =

Mas, como diz um amigo meu, voltando...


done

Isso termina "oficialmente" o loop while. A propósito, como pode ter sido notado, termina o for
também.
Como foi dito acima, o while espera "verdadeiro" ou "falso". Eu nunca disse que esperava isso só
do programa test.
Com efeito, qualquer expressão pode ser usada, e seu valor de retorno será utilizado para determinar
se é "verdadeiro" ou "falso". Para quem não sabe, todo programa em Unix retorna um valor ao
terminar sua execução. Normalmente, se tudo correu bem o valor retornado é 0 (zero). Se há algum
erro, o valor retornado, diferente de zero, indica o tipo de erro (veja as manpages dos programas;
man fetchmail seção exit codes é um bom exemplo). Portanto, ao contrário do que
programadores de C ou Perl poderiam achar intuitivo (dentro de um while, ou uma condição em
geral), um programa que retorna 0 é considerado "verdadeiro" aos olhos do "while``.
Assim, podemos fazer:
while w | grep -qs rbp; do
sleep 5s
done
echo 'rbp acaba de sair do sistema!'

Nesse exemplo, o while checa o retorno da expressão w | grep -qs rbp. Isso retorna
"verdadeiro" quando o grep acha rbp na saída do comando w. Toda vez que achar, espera 5
segundos e checa de novo. Quando não achar, sai do loop e mostra um aviso de que a última sessão
do rbp foi fechada.
Pra finalizar: se você quiser fazer um loop infinito, pode usar : (dois pontos) como condição
sempre verdadeira:
while : ; do
echo 'Emacs rules!'
done

Isso vai imprimir uma constatação sábia infinitamente, até você usar C-c (Ctrl + C). Normalmente,
isso é utilizado com alguma condição de parada.

Algumas Dicas de Bash


Colaboração: Rodrigo Bernardo Pimentel
O "history" (ou histórico) no bash, apesar de extremamente poderoso, é muito pouco usado, além do
tradicional "setinha pra cima" para exibir os últimos comandos.
Seu comportamento é regido por algumas variáveis:

HISTSIZE
Tamanho (em número de comandos) do histórico.

HISTFILE
Arquivo em que serão salvos os comandos (normalmente ~/.bash_history). =

HISTFILESIZE
Tamanho máximo (em linhas) do arquivo selecionado acima. Se um valor for especificado, o
arquivo será truncado para conter apenas o número especificado de linhas.
Eu, por exemplo, uso HISTSIZE e HISTFILESIZE como 5000.
Outra variável interessante é:

HISTCONTROL
Pode ter um dentre três valores: "ignorespace", "ignoredups" ou "ignoreboth". Se se usar o primeiro,
comandos começando com espaço não vão para o histórico. Com o segundo (e isso é
particularmente interessante), se um comando é executado mais de uma vez em seguida, só uma
ocorrência vai para o histórico. O terceiro implementa ambas as funcionalidades.
Finalmente, para quem costuma quebrar comandos em várias linhas (quem sabe isso não vai em
outra dica? ;) , pode ser interessante fazer:
export command_oriented_history=1
(ou qualquer outro valor, basta a variável estar "setada") Isso faz com que o bash tente salvar todas
as linhas do comando, ao invés de salvar cada linha como se fosse um comando diferente (o
comportamento padrão).
O caracter associado ao histórico é '!' (exclamação). Aqui, é mais fácil compreender com a ajuda de
exemplos:
!bla - repete o último comando começando com "bla". !?bla - repete o último comando contendo
"bla". !x - repete o comando de número "x" no histórico. Similarmente, !-y - repete o "y-ésimo"
comando, do atual pra trás. !! - repete o último comando (ou seja, igual a !-1) !# - repete tudo o que
foi digitado na linha até aquele ponto. Por exemplo, "ls !#" geraria "ls ls".
Depois de especificada a linha de que se quer tratar (com os comandos anteriores), pode-se
selecionar parte delas e modificá-la. Esse tratamento das linhas é introduzido por ':' (dois pontos).
A seleção, em sua forma mais simples, é feito indicando-se um número relativo à posição do
argumento que você quer selecionar no comando (0 - zero - é o comando em si). Assim, por
exemplo:
[rbp@muppets ~]$ ls bla =
ls: bla: No such file or directory
[rbp@muppets ~]$ !!:0 =
ls

A linha "!!:0" pegou a última linha e executou só o "argumento zero" (o comando em si).
Poderíamos também fazer algo como:
[rbp@muppets ~]$ man xscreensaver =
[rbp@muppets ~]$ !!:1 =
xscreensaver

Pode-se selecionar uma seqüência de argumentos, com "x-y" (do argumento de número "x" ao de
número "y", inclusive). Por exemplo,
[rbp@muppets ~]$ cat script1 script2 script3 =
(...)
[rbp@muppets ~]$ !!:1-2 =
script1 script2
(...)
[rbp@muppets ~]$ =

Complementando "^" é o argumento de número 1 (isto é, o primeiro argumento depois do comando


em si), "$" é o último. Podem-se fazer abreviações como "x-" (igual a "x-$"), "-x" (igual a "1-x") ou
"*" (igual a "1-$").
Para se modificar o comando em questão (ou a porção selecionada dele), o método mais comum é
muito semelhante ao sed:
[rbp@muppets ~]$ mkdir diretorio1 =
[rbp@muppets ~]$ !!:s/1/2 =
mkdir diretorio2

O "s/1/2" substituiu "1" por "2" no comando anterior.


Juntando tudo:
[rbp@muppets ~]$ export EDITOR=emacs =
[rbp@muppets ~]$ !!:1:s/EDITOR=// =
emacs

Finalmente, uma dica: substituição do último comando executado pode ser feita rapidamente com
^a^b - substitui "a" por "b" no último comando.

Selecionando registros de um arquivo texto


com o awk
Precisei fazer um shell script onde deveria selecionar alguns registros (linhas) de um arquivo texto
com base em determinada data e hora. Utilizei o awk e achei muito interessante, vejam como o
utilizei:
Conteúdo do arquivo texto chamado id01.txt:
10/01/2006 01:05:50 771.000000
10/01/2006 01:06:50 773.000000
10/01/2006 01:07:50 774.000000
10/01/2006 01:08:50 773.000000
10/01/2006 01:09:50 771.000000
10/01/2006 01:10:51 769.000000
10/01/2006 01:11:51 768.000000
10/01/2006 01:12:51 767.000000
10/01/2006 01:13:51 769.000000
10/01/2006 01:14:51 772.000000
10/01/2006 01:15:51 774.000000
10/01/2006 01:16:51 774.000000
10/01/2006 01:17:51 773.000000
10/01/2006 01:18:51 771.000000
10/01/2006 01:19:51 769.000000
10/01/2006 01:20:51 767.000000
10/01/2006 01:21:52 768.000000
10/01/2006 01:22:52 771.000000
10/01/2006 01:23:52 773.000000
As colunas são: data, hora e temperatura.
Para selecionar os registros do dia 10/01/2006, entre 01:10:00 h e 01:20:00 h, por exemplo,
podemos usar o awk da seguinte forma:
# awk '$1$2 >= "10/01/200601:10:00" && $1$2 <= "10/01/200601:20:59"' id01.txt
Que produzirá o seguinte resultado:
10/01/2006 01:10:51 769.000000
10/01/2006 01:11:51 768.000000
10/01/2006 01:12:51 767.000000
10/01/2006 01:13:51 769.000000
10/01/2006 01:14:51 772.000000
10/01/2006 01:15:51 774.000000
10/01/2006 01:16:51 774.000000
10/01/2006 01:17:51 773.000000
10/01/2006 01:18:51 771.000000
10/01/2006 01:19:51 769.000000
10/01/2006 01:20:51 767.000000
Algumas explicações:
O awk funciona assim: awk ' [padrão] [{ação}] '.
Perceba que ambos os parâmetros são opcionais e dentro de aspas simples. Caso a ação não seja
especificado o padrão é a exibição de toda a linha lida (entenda linha como a seqüência de
caracteres até o encontro de um "Enter" ou carriage-return). Caso o padrão não seja especificado,
todas as linhas do arquivo sofrerão a ação especificada.
Veja também que, como queria verificar dois valores, juntei as variáveis $1 e $2 ($1$2) e NÃO
acrescentei espaço no conteúdo a ser comparado ("10/01/200601:10:00).
Para imprimir apenas a terceira coluna, no exemplo acima, basta acrescentar a ação {print $3},
assim:
# awk ' $1$2 >= "10/01/200601:10:00" && $1$2 <= "10/01/200601:20:59" {print $3} ' id01.txt
Em tempo, o && significa "e" e o || significa "ou".
O awk é muito poderoso, e imagino que para aqueles que ainda não o conhecem e saibam
programar que a dica acima poderá ser útil e um incentivo a conhecê-lo um pouco mais
profundamente.

Removendo linhas duplicadas de um arquivo


texto
Este comando me salvou a vida! Além de colocar um arquivo em ordem, removeu as linhas
duplicadas:
$ sort arqtexto.txt | uniq >arqnovo.txt

You might also like