Feeds:
Posts
Comentários

Posts Tagged ‘cat’

Papo de Botequim – Parte 1

Este blog contém um livro completo de Shell que escrevi para 12 fascículos da Linux Magazine.

julio-neves.jpg

Diálogo entreouvido entre um Linuxer e em empurrador de mouse:

– Quem é o Bash?

– O Bash é o filho mais novo da família Shell.

– Pô cara! Estás a fim de me deixar maluco? Eu tinha uma dúvida e você me deixa com duas!

– Não, maluco você já é há muito tempo. Desde que se decidiu a usar aquele sistema operacional que você tem que dar dez boots por dia e não tem domínio nenhum sobre o que esta acontecendo no seu computador. Mas deixa isso prá lá, vou te explicar o que é Shell e os componentes de sua família e ao final da explanação você dirá: “Meu Deus do Shell! Porque eu não optei pelo Linux antes?”.

O Ambiente Linux

Para você entender o que é e como funciona o Shell, primeiro vou te mostrar como funciona o ambiente em camadas do Linux. Dê uma olhada no gráfico abaixo:

Visão do shell em relação do Kernel do Linux Neste gráfico dá para ver que a camada de hardware é a mais profunda e é formada pelos componentes físicos do seu computador. Envolvendo esta, vem a camada do kernel que é o cerne do Linux, seu núcleo, e é quem bota o hardware para funcionar, fazendo seu gerenciamento e controle. Os programas e comandos que envolvem o kernel, dele se utilizam para realizar as tarefas aplicativas para que foram desenvolvidos. Fechando tudo isso vem o Shell que leva este nome porque em inglês, Shell significa concha, carapaça, isto é, fica entre o usuário e o sistema operacional, de forma que tudo que interage com o sistema operacional, tem que passar pelo seu crivo.

O Ambiente Shell

Bom já que para chegar ao núcleo do Linux, no seu kernel que é o que interessa a todo aplicativo, é necessária a filtragem do Shell, vamos entender como ele funciona de forma a tirar o máximo proveito das inúmeras facilidades que ele nos oferece.

O Linux por definição é um sistema multiusuário – não podemos nunca esquecer disto – e para permitir o acesso de determinados usuários e barrar a entrada de outros, existe um arquivo chamado /etc/passwd que além fornecer dados para esta função de “leão-de-chácara” do Linux, também provê informações para o login daqueles que passaram por esta primeira barreira. O último campo de seus registros informa ao sistema qual Shell a pessoa vai receber ao se “logar” (ARGH!!!).

Pinguim com placa de dica Quando eu disse que o último campo do /etc/passwd informa ao sistema qual é o Shell que o usuário vai receber ao se “logar”, é para ser interpretado ao pé-da-letra, isto é, se neste campo do seu registro estiver prog, a pessoa ao acessar o sistema receberá a tela de execução do programa prog e ao terminar a sua execução ganhará imediatamente um logout. Imagine o quanto se pode incrementar a segurança com este simples artifício.

Lembra que eu te falei de Shell, família, irmão? Pois é, vamos começar a entender isto: o Shell, que se vale da imagem de uma concha envolvendo o sistema operacional propriamente dito, é o nome genérico para tratar os filhos desta idéia que, ao longo dos anos de existência do sistema operacional Unix foram aparecendo. Atualmente existem diversos sabores de Shell, dentre estes eu destaco o sh (Bourne Shell), o ksh (Korn Shell), bash (Bourne Again Shell) e o csh (C Shell).

Uma Rapidinha nos Principais Sabores de Shell

Bourne Shell (sh)

Desenvolvido por Stephen Bourne da Bell Labs (da AT&T onde também foi desenvolvido o Unix), este foi durante muitos anos o Shell default do sistema operacional Unix. É também chamado de Standard Shell por ter sido durante vários anos o único e até hoje é o mais utilizado até porque ele foi portado para todos os ambientes Unix e distros Linux.

Korn Shell (ksh)

Desenvolvido por David Korn, também da Bell Labs, é um superset do sh, isto é, possui todas as facilidades do sh e a elas agregou muitas outras. A compatibilidade total com o sh vem trazendo muitos usuários e programadores de Shell para este ambiente.

Boune Again Shell (bash)

Este é o Shell mais moderno e cujo número de adeptos mais cresce em todo o mundo, seja por ser o Shell default do Linux, seu sistema operacional hospedeiro, seja por sua grande diversidade de comandos, que incorpora inclusive diversos instruções características do C Shell.

C Shell (csh)

Desenvolvido por Bill Joy da Berkley University é o Shell mais utilizado em ambientes *BSD e Xenix. A estruturação de seus comandos é bem similar à da linguagem C. Seu grande pecado foi ignorar a compatibilidade com o sh, partindo por um caminho próprio.

Além destes Shells existem outros, mas irei falar contigo somente sobre os três primeiros, tratando-os genericamente por Shell e assinalando as especificidades de cada um que porventura hajam.

Explicando o funcionamento do Shell

O Shell é o primeiro programa que você ganha ao se “logar” no Linux. É ele que vai resolver um monte de coisas de forma a não onerar o kernel com tarefas repetitivas, aliviando-o para tratar assuntos mais nobres. Como cada usuário possui o seu próprio Shell interpondo-se entre ele e o Linux, é o Shell quem interpreta os comandos que são teclados e examina as suas sintaxes, passando-os esmiuçados para execução.

– Êpa! Esse negócio de interpreta comando não tem nada a haver com interpretador não, né?

– Tem sim, na verdade o Shell é um interpretador (ou será intérprete) que traz consigo uma poderosa linguagem com comandos de alto nível, que permite construção de loops (laços), de tomadas de decisão e de armazenamento de valores em variáveis, como vou te mostrar.

Vou te explicar as principais tarefas que o Shell cumpre, na sua ordem de execução. Preste atenção nesta ordem porque ela é fundamental para o entendimento do resto do nosso bate papo.

Exame da Linha de Comandos

Neste exame o Shell identifica os caracteres especiais (reservados) que têm significado para interpretação da linha, logo após verifica se a linha passada é uma atribuição ou um comando.

Atribuição

Se o Shell encontra dois campos separados por um sinal de igual (=) sem espaços em branco entre eles, identifica esta seqüência como uma atribuição.

Exemplos

$ ls linux
linux

Neste exemplo o Shell identificou o ls como um programa e o linux como um parâmetro passado para o programa ls.

$ valor=1000

Neste caso, por não haver espaços em branco (já dá para notar que o branco é um dos caracteres reservados) o Shell identificou uma atribuição e colocou 1000 na variável valor.

Pinguim com placa de dica Jamais Faça:

$ valor = 1000
bash: valor: not found

Neste caso, o Bash achou a palavra valor isolada por brancos e julgou que você estivesse mandando executar um programa chamado valor, para o qual estaria passando dois parâmetros: = e 1000.

Comando

Quando uma linha é digitada no prompt do Linux, ela é dividida em pedaços separados por espaço em branco: o primeiro pedaço é o nome do programa que terá sua existência pesquisada; identifica em seguida, nesta ordem, opções/parâmetros, redirecionamentos e variáveis.

Quando o programa identificado existe, o Shell verifica as permissões dos arquivos envolvidos (inclusive o próprio programa), dando um erro caso você não esteja credenciado a executar esta tarefa.

Resolução de Redirecionamentos

Após identificar os componentes da linha que você teclou, o Shell parte para a resolução de redirecionamentos. O Shell tem incorporado ao seu elenco de vantagens o que chamamos de redirecionamento, que pode ser de entrada (stdin), de saída (stdout) ou dos erros (stderr), conforme vou te explicar a seguir.

Substituição de Variáveis

Neste ponto, o Shell verifica se as eventuais variáveis (parâmetros começados por $), encontradas no escopo do comando, estão definidas e as substitui por seus valores atuais.

Substituição de Meta Caracteres

Se algum metacaractere (*, ? ou []) foi encontrado na linha de comando, neste ponto ele será substituído por seus possíveis valores. Supondo que o único arquivo no seu diretório corrente começado pela letra n seja um diretório chamado nomegrandeprachuchu, se você fizer:

$ cd n*

Como até aqui quem esta trabalhando a sua linha é o Shell e o comando (programa) cd ainda não foi executado, o Shell transforma o n* em nomegrandeprachuchu e o comando cd será executado com sucesso.

Passa Linha de Comando para o kernel

Completadas as tarefas anteriores, o Shell monta a linha de comandos, já com todas as substituições feitas, chama o kernel para executá-la em um novo Shell (Shell filho), ganhando um número de processo (PID ou Process IDentification) e permanece inativo, tirando uma soneca, durante a execução do programa. Uma vez encerrado este processo (juntamente com o Shell filho), recebe novamente o controle e, exibindo um prompt, mostra que está pronto para executar outros comandos.

Decifrando a Pedra da Roseta

Para tirar aquela sensação que você tem quando vê um script Shell, que mais parece uma sopa de letrinhas ou um hieróglifo vou lhe mostrar os principais caracteres especiais para que você saia por ai como o Jean-François Champollion decifrando a Pedra da Roseta (dê uma “googlada” para descobrir quem é este cara, acho que vale a pena).

Caracteres para remoção do significado

É isso mesmo, quando não desejamos que o Shell interprete um caractere especial, devemos “escondê-lo” dele. Isso pode ser feito de três formas distintas:

Apóstrofo ou plic (')

Quando o Shell vê uma cadeia de caracteres entre apóstrofos ('), ele tira os apóstrofos da cadeia e não interpreta seu conteúdo.

$ ls linux*
linuxmagazine
$ ls ‘linux*’
bash: linux* no such file or directory

No primeiro caso o Shell “expandiu” o asterisco e descobriu o arquivo linuxmagazine para listar. No segundo, os apóstrofos inibiram a interpretação do Shell e veio a resposta que não existe o arquivo linux*.

Contrabarra ou Barra Invertida ()

Idêntico aos apóstrofos exceto que a barra invertida inibe a interpretação somente do caractere que a segue.

Suponha que você acidentalmente tenha criado um arquivo chamado * (asterisco) – que alguns sabores de Unix permitem – e deseja removê-lo. Se você fizesse:

$ rm *

Você estaria fazendo a maior encrenca, pois o rm removeria todos os arquivos do diretório corrente. A melhor forma de fazer o pretendido é:

$ rm *

Desta forma, o Shell não interpretaria o asterisco, e em conseqüência não faria a sua expansão.

Faça a seguinte experiência científica:

$ cd /etc
$ echo ‘*’
$ echo *
$ echo *

Viu a diferença? Então não precisa explicar mais.

Aspas (")

Exatamente igual ao apóstrofo exceto que, se a cadeia entre aspas contiver um cifrão ($), uma crase (`), ou uma barra invertida (), estes caracteres serão interpretados pelo Shell.

Não precisa se estressar, eu não te dei exemplos do uso das aspas por que você ainda não conhece o cifrão ($) nem a crase (`). Daqui para frente veremos com muita constância o uso destes caracteres especiais, o mais importante é entender o significado de cada um.

Caracteres de redirecionamento

A maioria dos comandos tem uma entrada, uma saída e pode gerar erros. Esta entrada é chamada Entrada Padrão ou stdin e seu default é o teclado do terminal. Analogamente, a saída do comando é chamada Saída Padrão ou stdout e seu default é a tela do terminal. Para a tela também são enviadas por default as mensagens de erro oriundas do comando que neste caso é a chamada Saída de Erro Padrão ou stderr. Veremos agora como alterar este estado de coisas.

Vamos fazer um programa gago. Para isto faça:

$ cat

O cat é uma instrução que lista o conteúdo do arquivo especificado para a Saída Padrão (stdout). Caso a entrada não seja definida, ele espera os dados da stdin. Ora como eu não especifiquei a entrada, ele está esperando-a pelo teclado (Entrada Padrão) e como também não citei a saída, o que eu teclar irá para a tela (Saída Padrão) fazendo desta forma, como eu havia proposto um programa gago. Experimente!

Redirecionamento da Saída Padrão

Para especificarmos a saída de um programa usamos o > (maior que) ou o >> (maior, maior) seguido do nome do arquivo para o qual se deseja mandar a saída.

Vamos transformar o programa gago em um editor de textos (que pretensão heim!). smile

$ cat > Arq

O cat continua sem ter a entrada especificada, portanto está aguardando que os dados sejam teclados, porém a sua saída está sendo desviada para o arquivo Arq. Assim sendo, tudo que esta sendo teclado esta indo para dentro de Arq, de forma que fizemos o editor de textos mais curto e ruim do planeta.

Se eu fizer novamente:

$ cat > Arq

Os dados contidos em Arq serão perdidos, já que antes do redirecionamento o Shell criará um Arq vazio. Para colocar mais informações no final do arquivo eu deveria ter feito:

$ cat >> Arq
Pinguim com placa de atenção Como já havia lhe dito, o Shell resolve a linha e depois manda o comando para a execução. Assim, se você redirecionar a saída de um arquivo para ele próprio, primeiramente o Shell “esvazia” este arquivo e depois manda o comando para execução, desta forma você acabou de perder o conteúdo do seu querido arquivo.

Com isso dá para notar que o >> (maior maior) serve para inserir texto no final do arquivo.

Redirecionamento da Saída de Erro Padrão

Assim como o default do Shell é receber os dados do teclado e mandar as saídas para a tela, os erros também serão enviados para a tela se você não especificar para onde deverão ser enviados. Para redirecionar os erros use 2> SaidaDeErro. Note que entre o número 2 e o sinal de maior (>) não existe espaço em branco.

Pinguim com placa de atenção Preste atenção! Não confunda >> com 2>. O primeiro anexa dados ao final de um arquivo, e o segundo redireciona a Saída de Erro Padrão (stderr) para um arquivo que está sendo designado. Isso é importante!

Suponha que durante a execução de um script você pode, ou não (dependendo do rumo tomado pela execução do programa), ter criado um arquivo chamado /tmp/seraqueexiste$$. Para não ficar sujeira no seu disco, ao final do script você colocaria uma linha:

$ rm /tmp/seraqueexiste$$

Caso o arquivo não existisse seria enviado para a tela uma mensagem de erro. Para que isso não aconteça deve-se fazer:

$ rm /tmp/seraqueexiste$$ 2> /dev/null

Sobre o exemplo que acabamos de ver tenho duas dicas a dar:

Pinguim com placa de dica Dica # 1 O $$ contém o PID, isto é, o número do seu processo. Como o Linux é multiusuário, é bom anexar sempre o $$ ao nome dos dos arquivos que serão usados por várias pessoas para não haver problema de propriedade, isto é, caso você batizasse o seu arquivo simplesmente como seraqueexiste, o primeiro que o usasse (criando-o então) seria o seu dono e todos os outros ganhariam um erro quando tentassem gravar algo nele.

Para que você teste a Saída de Erro Padrão direto no prompt do seu Shell, vou dar mais um exemplo. Faça:

$ ls naoexiste
bash: naoexiste no such file or directory
$ ls naoexiste 2> arquivodeerros
$
$ cat arquivodeerros
bash: naoexiste no such file or directory

Neste exemplo, vimos que quando fizemos um ls em naoexiste, ganhamos uma mensagem de erro. Após, redirecionarmos a Saída de Erro Padrão para arquivodeerros e executarmos o mesmo comando, recebemos somente o prompt na tela. Quando listamos o conteúdo do arquivo para o qual foi redirecionada a Saída de Erro Padrão, vimos que a mensagem de erro tinha sido armazenada nele. Faça este teste ai.

Pinguim com placa de dica Dica # 2 – Quem é esse tal de /dev/null?- Em Unix existe um arquivo fantasma. Chama-se /dev/null. Tudo que é mandado para este arquivo some. Assemelha-se a um Buraco Negro. No caso do exemplo, como não me interessava guardar a possível mensagem de erro oriunda do comando rm, redirecionei-a para este arquivo.

É interessante notar que estes caracteres de redirecionamento são cumulativos, isto é, se no exemplo anterior fizéssemos:

$ ls naoexiste 2>> arquivodeerros

a mensagem de erro oriunda do ls seria anexada ao final de arquivodeerros.

Redirecionamento da Entrada Padrão

Para fazermos o redirecionamento da Entrada Padrão usamos o < (menor que).

– E prá que serve isso? – você vai me perguntar.

– Deixa eu te dar um exemplo que você vai entender rapidinho.

Suponha que você queira mandar um mail para o seu chefe. Para o chefe nós caprichamos, né? então ao invés de sair redigindo o mail direto no prompt da tela de forma a tornar impossível a correção de uma frase anterior onde, sem querer, escreveu um “nós vai”, você edita um arquivo com o conteúdo da mensagem e após umas quinze verificações sem constatar nenhum erro, decide enviá-lo e para tal faz:

$ mail chefe < arquivocommailparaochefe

O teu chefe então receberá o conteúdo do arquivocommailparaochefe.

Here Document

Um outro tipo de redirecionamento muito louco que o Shell te permite é o chamado here document. Ele é representado por << (menor menor) e serve para indicar ao Shell que o escopo de um comando começa na linha seguinte e termina quando encontra uma linha cujo conteúdo seja unicamente o label que segue o sinal <<.

Veja o fragmento de script a seguir, com uma rotina de ftp:

    ftp -ivn hostremoto << fimftp
        user $Usuário $Senha
        binary
        get arquivoremoto
    fimftp

Neste pedacinho de programa temos um monte de detalhes interessantes:

  1. As opções que usei para o ftp (-ivn) servem para ele ir listando tudo que está acontecendo (—v de verbose), para não perguntar se você tem certeza de que deseja transmitir cada arquivo (—i de interactive), e finalmente a opção —n serve para dizer ao ftp para ele não solicitar o usuário e sua senha, pois esses serão informados pela instrução específica (user);
  2. Quando eu usei o << fimftp, estava dizendo o seguinte para o intérprete:
    – Olhe aqui Shell, não se meta em nada a partir daqui até encontrar o label fimftp. Você não entenderia nada, já que são instruções específicas do comando ftp e você não entende nada de ftp.
    Se fosse só isso seria simples, mas pelo próprio exemplo dá para ver que existem duas variáveis ($Usuário e $Senha), que o Shell vai resolver antes do redirecionamento. Mas a grande vantagem desse tipo de construção é que ela permite que comandos também sejam interpretados dentro do escopo do here document, o que também contraria o que acabei de dizer. Logo a seguir explico como esse negócio funciona. Agora ainda não dá, está faltando ferramenta.
  3. O comando user é do repertório de instruções do ftp e serve para passar o usuário e a senha que haviam sido lidos em uma rotina anterior a esse fragmento de código e colocados respectivamente nas duas variáveis: $Usuário e $Senha.
  4. O binary é outra instrução do ftp, que serve para indicar que a transferência de arquivoremoto será feita em modo binário, isto é, o conteúdo do arquivo não será interpretado para saber se está em ASCII, EBCDIC, …
  5. O get arquivoremoto diz ao ftp para pegar esse arquivo em hostremoto e trazê-lo para o nosso host local. Se fosse para mandar o arquivo, usaríamos o comando put.
Pinguim com placa de atenção Um erro muito freqüente no uso de labels (como o fimftp do exemplo anterior) é causado pela presença de espaços em branco antes ou após o mesmo. Fique muito atento quanto a isso, por que este tipo de erro costuma dar uma boa surra no programador, até que seja detectado. Lembre-se: um label que se preze tem que ter uma linha inteira só para ele.

– Está bem, está bem! Eu sei que dei uma viajada e entrei pelos comandos do ftp, fugindo ao nosso assunto que é Shell, mas como é sempre bom aprender e é raro as pessoas estarem disponíveis para ensinar…

Redirecionamento de Comandos

Os redirecionamentos que falamos até aqui sempre se referiam a arquivos, isto é mandavam para arquivo, recebiam de arquivo, simulavam arquivo local, … O que veremos a partir de agora redireciona a saída de um comando para a entrada de outro. É utilíssimo e quebra os maiores galhos. Seu nome é pipe (que em inglês significa tubo, já que ele encana a saída de um comando para a entrada de outro) e sua representação é uma barra vertical (|).

$ ls | wc -l
21

O comando ls passou a lista de arquivos para o comando wc, que quando está com a opção –l conta a quantidade de linha que recebeu. Desta forma, podemos afirmar categoricamente que no meu diretório existiam 21 arquivos.

$ cat /etc/passwd |sort | lp

Esta linha de comandos manda a listagem do arquivo /etc/passwd para a entrada do comando sort. Este a classifica e manda-a para o lp que é o gerenciador do spool de impressão.

Caracteres de Ambiente

Quando quer priorizar uma expressão você coloca-a entre parênteses não é? Pois é, por causa da aritmética é normal pensarmos deste jeito. Mas em Shell o que prioriza mesmo são as crases (`) e não os parênteses. Vou dar exemplos de uso das crases para você entender melhor.

Eu quero saber quantos usuários estão “logados” no computador que eu administro. Eu posso fazer:

$ who | wc -l
8

O comando who passa a lista de usuários conectados para o comando wc –l que conta quantas linhas recebeu e lista a resposta na tela. Pois bem, mas ao invés de ter um oito solto na tela, o que eu quero é que ele esteja no meio de uma frase.

Ora para mandar frases para a tela eu uso o comando echo, então vamos ver como é que fica:

$ echo “Existem who | wc -l usuários conectados”
Existem who | wc -l usuários conectados

Hi! Olha só, não funcionou! É mesmo, não funcionou e não foi por causa das aspas que eu coloquei, mas sim por que eu teria que ter executado o who | wc -l antes do echo. Para resolver este problema, tenho que priorizar esta segunda parte do comando com o uso de crases, fazendo assim:

$ echo “Existem `who | wc -l` usuários conectados”
Existem 8 usuários conectados

Para eliminar esse monte de brancos antes do 8 que o wc -l produziu, basta tirar as aspas. Assim:

$ echo Existem `who | wc -l` usuários conectados
Existem 8 usuários conectados

Como eu disse antes, as aspas protegem tudo que esta dentro dos seus limites, da interpretação do Shell. Como para o Shell basta um espaço em branco como separador, o monte de espaços será trocado por um único após a retirada das aspas.

Antes de falar sobre o uso dos parênteses deixa eu mandar uma rapidinha sobre o uso de ponto-e-vírgula (;). Quando estiver no Shell, você deve sempre dar um comando em cada linha. Para agrupar comandos em uma mesma linha teremos que separá-los por ponto-e-vírgula. Então:

$ pwd ; cd /etc; pwd; cd -; pwd
/home/meudir
/etc/
/home/meudir

Neste exemplo, listei o nome do diretório corrente com o comando pwd, mudei para o diretório /etc, novamente listei o nome do diretório e finalmente voltei para o diretório onde estava anteriormente (cd -), listando seu nome. Repare que coloquei o ponto-e-vírgula (;) de todas as formas possíveis para mostrar que não importa se existe espaços em branco antes ou após este caractere.

Finalmente vamos ver o caso dos parênteses. Veja só o caso a seguir, bem parecido com o exemplo anterior:

$ (pwd ; cd /etc ; pwd;)
/home/meudir
/etc/

$ pwd
/home/meudir

– Quequeiiisso minha gente? Eu estava no /home/meudir, mudei para o /etc, constatei que estava neste diretório com o pwd seguinte e quando o agrupamento de comandos terminou, eu vi que continuava no /home/meudir, como se eu nunca houvesse saído de lá!

– Ih! Será que é tem coisa de mágico aí?

– Tá me estranhando, rapaz? Não é nada disso! O interessante do uso de parênteses é que ele invoca um novo Shell para executar os comandos que estão no seu interior. Desta forma, realmente fomos para o diretório /etc, porém quando todos os comandos dentro dos parênteses foram executados, o novo Shell que estava no diretório /etc morreu e voltamos ao Shell anterior cujo diretório corrente era /home/meudir. Faça outros testes usando cd, e ls para você firmar o conceito.

Agora que já conhecemos estes conceitos veja só este exemplo a seguir:

$ mail suporte << FIM
> Ola suporte, hoje as ‘date “+%H:%M”‘
> ocorreu novamente aquele problema
> que eu havia reportado por
> telefone. Conforme seu pedido
> ai vai uma listagem dos arquivos
> do diretorio:
> ‘ls —l‘
> Abracos a todos.
> FIM

Finalmente agora temos conhecimento para mostrar o que havíamos conversado sobre here document. Os comandos entre crases (`) serão priorizados e portanto o Shell os executará antes da instrução mail. Quando o suporte receber o e-mail, verá que os comandos date e ls foram executados imediatamente antes do comando mail, recebendo então uma fotografia do ambiente no momento em que a correspondência foi enviada.

O prompt primário default do Shell, como vimos, é o cifrão ($), porém o Shell usa o conceito de prompt secundário, ou de continuação de comando, que é enviado para a tela quando há uma quebra de linha e a instrução não terminou. Esse prompt, é representado por um sinal de maior (>), que vemos precedendo a partir da 2ª linha do exemplo.

Para finalizar e bagunçar tudo, devo dizer que existe uma construção mais moderna que vem sendo utilizada como forma de priorização de execução de comandos, tal qual as crases (`). São as construções do tipo $(cmd), onde cmd é um (ou vários) comando que será(ão) executado(s) com prioridade em seu contexto.

Assim sendo, o uso de crases (`) ou construções do tipo $(cmd) servem para o mesmo fim, porém para quem trabalha com sistemas operacionais de diversos fornecedores (multiplataforma), aconselho o uso das crases, já que o $(cmd) não foi portado para todos os sabores de Shell. Aqui dentro do Botequim, usarei ambas as formas, indistintamente.

Vejamos novamente o exemplo dado para as crases sob esta nova ótica:

$ echo Existem $(who | wc -l) usuários conectados
Existem 8 usuários conectados

Veja só este caso:

$ Arqs=ls
$ echo $Arqs
ls

Neste exemplo eu fiz uma atribuição (=) e executei uma instrução. O que eu queria era que a variável $Arqs, recebesse a saída do comando ls. Como as instruções de um script são interpretadas de cima para baixo e da esquerda para a direita, a atribuição foi feita antes da execução do ls. Para fazer o que desejamos é necessário que eu priorize a execução deste comando em detrimento da atribuição e isto pode ser feito de qualquer uma das maneiras a seguir:

$ Arqs=`ls`

ou:

$ Arqs=$(ls)

Para encerrar este assunto vamos ver só mais um exemplo. Digamos que eu queira colocar dentro da variável $Arqs a listagem longa (ls -l) de todos os arquivos começados por arq e seguidos de um único caractere (?). Eu deveria fazer:

$ Arqs=$(ls -l arq?)

ou:

$ Arqs=`ls -l arq?`

Mas veja:

$ echo $Arqs
-rw-r–r–
1 jneves jneves 19 May 24 19:41 arq1 -rw-r–r– 1 jneves jneves 23 May
24 19:43 arq2 -rw-r–r– 1 jneves jneves 1866 Jan 22 2003 arql

– Pô, saiu tudo embolado!

– Pois é cara, como eu já te disse, se você deixar o Shell “ver” os espaços em branco, sempre que houver diversos espaços juntos, eles serão trocados por apenas um. Para que a listagem saia bonitinha, é necessário proteger a variável da interpretação do Shell, assim:

$ echo “$Arqs”
-rw-r–r– 1 jneves jneves 19 May 24 19:41 arq1
-rw-r–r– 1 jneves jneves 23 May 24 19:43 arq2
-rw-r–r– 1 jneves jneves 1866 Jan 22 2003 arql

– Olhe, amigo, vá treinando esses exemplos, porque, quando nos encontrarmos novamente, vou lhe explicar uma série de instruções típicas de programação Shell. Tchau! Ahh! Só mais uma coisinha que eu ia esquecendo de lhe dizer. Em Shell, o “jogo da velha” (#) é usado quando desejamos fazer um comentário.

$ exit # pede a conta ao garcon frown

Vou aproveitar também para mandar o meu jabá: diga para os amigos que quem estiver afim de fazer um curso porreta de programação em Shell que mande um e-mail para a nossa gerencia de treinamento para informar-se.

Qualquer dúvida ou falta de companhia para um chope ou até para falar mal dos políticos é só mandar um e-mail para mim.

Valeu!

Read Full Post »

Papo de Botequim – Parte 2

– Garçom! Traz um “chops” e dois “pastel”. O meu amigo hoje não vai beber por que ele finalmente está sendo apresentado a um verdadeiro sistema operacional e ainda tem muita coisa a aprender!
– E então, amigo, tá entendendo tudo que te expliquei até agora?
– Entendendo eu tô, mas não vi nada prático nisso…
– Calma rapaz, o que te falei até agora, serve como base ao que há de vir daqui pra frente. Vamos usar estas ferramentas que vimos para montar programas estruturados, que o Shell permite. Você verá porque até na TV já teve programa chamado “O Shell é o Limite”.
– Para começar vamos falar dos comandos da família grep.
grep? Não conheço nenhum termo em inglês com este nome…
– É claro, grep é um acrônimo Global Regular Expression Print, que usa expressões regulares para pesquisar a ocorrência de cadeias de caracteres na entrada definida (se bem que há uma lenda sobre como este comando foi nomeado: no editor de textos “ed”, o avô do “vim”, o comando usado para buscas era g/_expressao regular_/p, ou no inglês g/_re_/p.). Por falar em expressões regulares (ou regexp), o Aurélio Marinho Jargas tem todas as dicas em sua página (inclusive tutorias) que abordam o tema. Se você está mesmo a fim de aprender a programar em Shell, Perl, Python, … Acho bom você ler estes artigos para te ajudar no que está para vir.

Eu fico com o grep, você com a gripe

Esse negócio de gripe é brincadeira! É só um pretexto para pedir umas caipirinhas. Mas voltando à vaca fria, eu te falei que o grep procura cadeia de caracteres dentro de uma entrada definida, mas o que vem a ser uma “entrada definida”? Bem, existem várias formas de definir a entrada do comando grep. Vejamos: Pesquisando em um arquivo:

$ grep rafael /etc/passwd

Pesquisando em vários arquivos:

$ grep grep *.sh

Pesquisando na saida de comando:

$ who | grep Pelegrino

No 1º exemplo, o mais simples, procurei a palavra rafael em qualquer lugar do arquivo /etc/passwd. Se quisesse procurá-la como um login name, isto é, somente no início dos registros deste arquivo, eu deveria fazer:

$ grep ‘^rafael’ /etc/passwd

E para que serve este circunflexo e os apóstrofos, você vai me perguntar. O circunflexo (^), se você tivesse lido os artigos anteriores sobre expressões regulares que te falei, saberia que servem para limitar a pesquisa ao início de cada linha, e os apóstrofos (') servem para o Shell não interpretar este circunflexo, deixando-o passar incólume para o comando grep.

Olha que legal! O grep aceita como entrada, a saída de outro comando redirecionado por um pipe (isto é muito comum em Shell e é um tremendo acelerador de execução de comando já que atua como se a saída de um programa fosse guardada em disco e o segundo programa lesse este arquivo gerado), desta forma, no 3º exemplo, o comando who listou as pessoas “logadas” na mesma máquina que você (não se esqueça jamais: o Linux é multiusuário) e o grep foi usado para verificar se o Pelegrino estava trabalhando ou “coçando”.

A família grep

Este comando grep é muito conhecido, pois é usado com muita freqüência, o que muitas pessoas desconhecem é que existem três comandos na família grep, que são:

  • grep
  • egrep
  • fgrep

A principais características diferenciais entre os 3 são:

  • O grep pode ou não usar expressões regulares simples, porém no caso de não usá-las, o fgrep é melhor, por ser mais rápido;
  • O egrep (“e” de extended, extendido) é muito poderoso no uso de expressões regulares. Por ser o mais lento da família, só deve ser usado quando for necessária a elaboração de uma expressão regular não aceita pelo grep;
  • O fgrep (“f” de fast, rápido, ou de “file”, arquivo) como o nome diz é o rapidinho da família, executa o serviço de forma muito veloz (por vezes é cerca de 30% mais veloz que o grep e 50% mais que o egrep), porém não permite o uso de expressões regulares na pesquisa.
Pinguim com placa de atenção Tudo que foi dito acima sobre velocidade, só se aplica à família de comandos grep do Unix. No Linux o grep é sempre mais veloz, já que os outros dois (fgrep e egrep) são scripts em Shell que chamam o primeiro e, já vou adiantando, não gosto nem um pouquinho desta solução.

– Agora que você já conhece as diferenças entre os membros da família, me diga: o que você acha dos três exemplos que eu dei antes das explicações?
– Eu achei que o fgrep resolveria o teu problema de forma mais veloz do que o grep.
– Perfeito! Tô vendo que você está atento! Está entendendo tudo que estou te explicando! Então vamos ver mais exemplos para clarear de vez as diferenças de uso dos membros da família.
Exemplos

Eu sei que em um arquivo existe um texto falando sobre Linux só não tenho certeza se está escrito com L maiúsculo ou l minúsculo. Posso fazer de duas formas:

$ egrep (Linux | linux) arquivo.txt

ou

$ grep [Ll]inux arquivo.txt

No primeiro caso, a expressão regular complexa "(Linux | linux)" usa os parênteses para agrupar as opções e a barra vertical (|) como um “ou” lógico, isto é, estou procurando Linux ou linux.

No segundo, a expressão regular [Ll]inux significa: começado por L ou l seguido de inux. Por esta expressão ser mais simples, o grep consegue resolvê-la, portanto acho melhor usar a segunda forma, já que o egrep tornaria a pesquisa mais lenta.

Outro exemplo. Para listar todos os subdiretórios do diretório corrente, basta:

$ ls -l | grep ‘^d’
drwxr-xr-x 3 root root 4096 Dec 18 2000 doc
drwxr-xr-x 11 root root 4096 Jul 13 18:58 freeciv
drwxr-xr-x 3 root root 4096 Oct 17 2000 gimp
drwxr-xr-x 3 root root 4096 Aug 8 2000 gnome
drwxr-xr-x 2 root root 4096 Aug 8 2000 idl
drwxrwxr-x 14 root root 4096 Jul 13 18:58 locale
drwxrwxr-x 12 root root 4096 Jan 14 2000 lyx
drwxrwxr-x 3 root root 4096 Jan 17 2000 pixmaps
drwxr-xr-x 3 root root 4096 Jul 2 20:30 scribus
drwxrwxr-x 3 root root 4096 Jan 17 2000 sounds
drwxr-xr-x 3 root root 4096 Dec 18 2000 xine

No exemplo que acabamos de ver, o circunflexo (^) serviu para limitar a pesquisa à primeira posição da saída do ls longo. Os apóstrofos foram colocados para o Shell não “ver” o circunflexo (^).

Vamos ver mais um. Sabemos que as quatro primeiras posições possíveis de um ls -l de um arquivo comum (arquivo comum! Não é diretório, nem link, nem …) devem ser:

Posição
Valores Possíveis - r w x
  - - s (suid)
      -

Assim sendo, para descobrir todos os arquivos executáveis em um determinado diretório eu deveria fazer:

$ ls -la | egrep ‘^-..(x|s)’
-rwxr-xr-x 1 root root 2875 Jun 18 19:38 rc
-rwxr-xr-x 1 root root 857 Aug 9 22:03 rc.local
-rwxr-xr-x 1 root root 18453 Jul 6 17:28 rc.sysinit

Onde novamente usamos o circunflexo (^) para limitar a pesquisa ao início de cada linha, então as linhas listadas serão as que começam por um traço (-), seguido de qualquer coisa (o ponto quando usado como uma expressão regular significa qualquer coisa), novamente seguido de qualquer coisa, vindo a seguir um x ou um s.

Obteríamos o mesmo resultado se fizéssemos:

$ ls -la | grep ‘^-..[xs]’

e agilizaríamos a pesquisa.

Vamos Montar uma “cdteca”

Vamos começar a desenvolver programas, acho que a montagem de um banco de dados de músicas é bacana para efeito didático (e útil nesses tempos de downloads de mp3 e “queimadores” de CDs). Não se esqueça que, da mesma forma que vamos desenvolver um monte de programas para organizar os seus CDs de música, com pequenas adaptações, você pode fazer o mesmo com os CDs de software que vêm com a Linux Magazine e outros que você compra ou queima, disponibilizando este banco de software para todos que trabalham com você (o Linux é multiusuário, e como tal deve ser explorado), desta forma ganhando muitos pontos com seu adorado chefe.

– Péra ai! De onde eu vou receber os dados dos CDs?
– Inicialmente, vou lhe mostrar como o seu programa pode receber parâmetros de quem o estiver executando e em breve, ensinarei a ler os dados pela tela ou de um arquivo.

Passando parâmetros

O layout do arquivo musicas será o seguinte:

nome do álbum^intérprete1~nome da música1:..:intérprete~nome da música

isto é, o nome do álbum será separado por um circunflexo (^) do resto do registro, que é formado por diversos grupos compostos pelo intérprete de cada música do CD e a respectiva música interpretada. Estes grupos são separados entre si por dois-pontos (:) e internamente, o intérprete será separado por um til (~) do nome da música.

Eu quero escrever um programa que chamado musinc, que incluirá registros no meu arquivo musicas. Eu passarei o conteúdo de cada álbum como parâmetro na chamada do programa fazendo assim:

$ musinc “álbum^interprete~musica:interprete~musica:…”

Desta forma o programa musinc estará recebendo os dados de cada álbum como se fosse uma variável. A única diferença entre um parâmetro recebido e uma variável é que os primeiros recebem nomes numéricos (nome numérico fica muito esquisito, né? O que quis dizer é que seus nomes são formados por um e somente um algarismo), isto é $1, $2, $3, ..., $9. Vamos, antes de tudo, fazer um teste:

Exemplos

$ cat teste
#!/bin/bash
# Programa para testar passagem de parametros
echo “1o. parm -> $1”
echo “2o. parm -> $2”
echo “3o. parm -> $3”

Vamos executá-lo:

$ teste passando parametros para testar
bash: teste: cannot execute

Ops! Esqueci-me de torná-lo executável. Vou fazê-lo de forma a permitir que todos possam executá-lo e em seguida vou testá-lo:

$ chmod 755 teste
$ teste passando parametros para testar
1o. parm -> passando
2o. parm -> parametros
3o. parm -> para

Repare que a palavra testar, que seria o quarto parâmetro, não foi listada. Isto deu-se justamente porque o programa teste só listava os três primeiros parâmetros. Vamos executá-lo de outra forma:

$ teste “passando parametros” para testar
1o. parm -> passando parametros
2o. parm -> para
3o. parm -> testar

As aspas não deixaram o Shell ver o espaço em branco entre as palavras e considerou-as um único parâmetro.

Macetes paramétricos

Já que estamos falando em passagem de parâmetros deixa eu te dar mais umas dicas:

Significado das Principais Variáveis Referentes aos Parâmetros
Variável Significado
$0 Contém o nome do programa
$# Contém a quantidade de parâmetros passados
$* Contém o conjunto de todos os parâmetros (muito parecido com $@)

Exemplos

Vamos alterar o programa teste para usar as variáveis que acabamos de ver. Vamos fazê-lo assim:

$ cat teste
#!/bin/bash
# Programa para testar passagem de parametros (2a. Versao)
echo O programa $0 recebeu $# parametros
echo “1o. parm -> $1”
echo “2o. parm -> $2”
echo “3o. parm -> $3”
echo Todos de uma só “tacada”: $*

Repare que antes das aspas eu usei uma barra invertida para o escondê-las da interpretação do Shell (se não usasse as contrabarras as aspas não apareceriam). Vamos executá-lo:

$ teste passando parametros para testar
O programa teste recebeu 4 parametros
1o. parm -> passando
2o. parm -> parametros
3o. parm -> para
Todos de uma só “tacada”: passando parametros para testar

Conforme eu disse, os parâmetros recebem números de 1 a 9, mas isso não significa que não posso usar mais de 9 parâmetros significa somente que só posso endereçar 9. Vamos testar isso:

Exemplo:

$ cat teste
#!/bin/bash
# Programa para testar passagem de parametros (3a. Versao)
echo O programa $0 recebeu $# parametros
echo “11o. parm -> $11”
shift
echo “2o. parm -> $1”
shift 2
echo “4o. Parm -> $1”

Vamos executá-lo:

$ teste passando parametros para testar
O programa teste recebeu 4 parametros que são:
11o. parm -> passando1
2o. parm -> parametros
4o. parm -> testar

Duas coisas muito interessantes neste script:

  1. Para mostrar que os nomes dos parâmetros variam de $1 a $9 eu fiz um echo $11 e o que aconteceu? O Shell interpretou como sendo $1 seguido do algarismo 1 e listou passando1;
  2. O comando shift cuja sintaxe é shift n, podendo o n assumir qualquer valor numérico (porém seu default é 1 como no exemplo dado), despreza os n primeiros parâmetros, tornando o parâmetro de ordem n+1, o primeiro ou seja, o $1.

Bem, agora que você já sabe mais sobre passagem de parâmetros do que eu, vamos voltar à nossa “cdteca” para fazer o script de inclusão de CDs no meu banco chamado musicas. O programa é muito simples (como tudo em Shell) e vou listá-lo para você ver:

Exemplos

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 1)
#
echo $1 >> musicas

O script é fácil e funcional, limito-me a anexar ao fim do arquivo musicas o parâmetro recebido. Vamos cadastrar 3 álbuns para ver se funciona (para não ficar “enchendo lingüiça”, vou supor que em cada CD só existem 2 músicas):

$ musinc “album 3^Artista5~Musica5:Artista6~Musica5”
$ musinc “album 1^Artista1~Musica1:Artista2~Musica2”
$ musinc “album 2^Artista3~Musica3:Artista4~Musica4”

Listando o conteúdo de musicas.

$ cat musicas
album 3^Artista5~Musica5:Artista6~Musica6
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4

Não está funcional como achava que deveria ficar… podia ter ficado melhor. Os álbuns estão fora de ordem, dificultando a pesquisa. Vamos alterar nosso script e depois testá-lo novamente:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 2)
#
echo $1 >> musicas
sort musicas -o musicas

Vamos cadastrar mais um:

$ musinc “album 4^Artista7~Musica7:Artista8~Musica8”

Agora vamos ver o que aconteceu com o arquivo musicas:

$ cat musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica5
album 4^Artista7~Musica7:Artista8~Musica8

Simplesmente inseri uma linha que classifica o arquivo musicas dando a saída nele mesmo (para isso serve a opção -o), após cada álbum ser anexado.

Oba! Agora está legal e quase funcional. Mas atenção, não se desespere! Esta não é a versão final. O programa ficará muito melhor e mais amigável, em uma nova versão que desenvolveremos após aprendermos a adquirir os dados da tela e formatar a entrada.

Exemplos

Ficar listando com o comando cat não está com nada, vamos então fazer um programa chamado muslist para listar um álbum cujo nome será passado como parâmetro:

$ cat muslist
#!/bin/bash
# Consulta CDs (versao 1)
#
grep $1 musicas

Vamos executá-lo, procurando pelo album 2. Como já vimos antes, para passar a cadeia album 2 é necessário protegê-la da interpretação do Shell, para que ele não a interprete como dois parâmetros. Vamos fazer assim:

$ muslist “álbum 2”
grep: can’t open 2
musicas: album 1^Artista1~Musica1:Artista2~Musica2
musicas: album 2^Artista3~Musica3:Artista4~Musica4
musicas: album 3^Artista5~Musica5:Artista6~Musica6
musicas: album 4^Artista7~Musica7:Artista8~Musica8

Que lambança! Onde está o erro? Eu tive o cuidado de colocar o parâmetro passado entre aspas, para o Shell não dividi-lo em dois!

É, mas repare como está o grep executado:

grep $1 musicas

Mesmo colocando álbum 2 entre aspas, para que fosse encarado como um único parâmetro, quando o $1 foi passado pelo Shell para o comando grep, transformou-se em dois argumentos. Desta forma o conteúdo final da linha, que o comando grep executou foi o seguinte:

grep album 2 musicas

Como a sintaxe do grep é:

=grep [arq1, arq2, …, arqn]=

o grep entendeu que deveria procurar a cadeia de caracteres album nos arquivos 2 e musicas, Por não existir o arquivo 2 gerou o erro, e por encontrar a palavra album em todos os registros de musicas, listou a todos.

Pinguim com placa de dica Sempre que a cadeia de caracteres a ser passada para o comando grep possuir brancos ou TAB, mesmo que dentro de variáveis, coloque-a sempre entre aspas para evitar que as palavras após o primeiro espaço em branco ou TAB sejam interpretadas como nomes de arquivos.

Por outro lado, é melhor ignorarmos maiúsculas e minúsculas na pesquisa. Resolveríamos os dois problemas se o programa tivesse a seguinte forma:

$ cat muslist
#!/bin/bash
# Consulta CDs (versao 2)
#
grep -i “$1” musicas

Neste caso, usamos a opção -i do grep, que como já vimos, serve para ignorar maiúsculas e minúsculas, e colocamos o $1 entre aspas, para que o grep continuasse a ver a cadeia de caracteres resultante da expansão da linha pelo Shell como um único argumento de pesquisa.

$ muslist “album 2”
album2^Artista3~Musica3:Artista4~Musica4

Agora repare que o grep localiza a cadeia pesquisada em qualquer lugar do registro, então da forma que estamos fazendo, podemos pesquisar por álbum, por música, por intérprete ou até por um pedaço de qualquer um destes. Quando conhecermos os comandos condicionais, montaremos uma nova versão de muslist que permitirá especificar por qual campo pesquisar.

Aí você vai me dizer:

– Poxa, mas é um saco ter que colocar o argumento de pesquisa entre aspas na hora de passar o nome do álbum. Esta forma não é nem um pouco amigável!
– Tem razão, e por isso vou te mostrar uma outra forma de fazer o que você pediu:

$ cat muslist
#!/bin/bash
# Consulta CDs (versao 3)
#
grep -i “$*” musicas
$ muslist album 2
album 2^Artista3~Musica3:Artista4~Musica4

Desta forma, o $*, que significa todos os parâmetros, será substituído pela cadeia album 2 (de acordo com o exemplo anterior, fazendo o que você queria.

Não se esqueça o problema do Shell não é se você pode ou não fazer uma determinada coisa. O problema é decidir qual é a melhor forma de fazê-la, já que para desempenhar qualquer tarefa, a quantidade de opções é enorme.

Ah! Em um dia de verão você foi à praia, esqueceu o CD no carro, aquele “solzinho” de 40 graus empenou o seu CD e agora você precisa de uma ferramenta para removê-lo do banco de dados? Não tem problema, vamos desenvolver um script chamado musexc, para excluir estes CDs.

Antes de desenvolver o “bacalho”, quero te apresentar a uma opção bastante útil da família de comandos grep. É a opção -v, que quando usada lista todos os registros da entrada, exceto o(s) localizado(s) pelo comando. Vejamos:

Exemplos

$ grep -v “album 2” musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 3^Artista5~Musica5:Artista6~Musica6
album 4^Artista7~Musica7:Artista8~Musica8

Conforme eu expliquei antes, o grep do exemplo listou todos os registros de musicas exceto o referente a album 2, porque atendia ao argumento do comando. Estamos então prontos para desenvolver o script para remover aquele CD empenado da sua “CDteca”. Ele tem a seguinte cara:

$ cat musexc
#!/bin/bash
# Exclui CDs (versao 1)
#
grep -v “$1” musicas > /tmp/mus$$
mv -f /tmp/mus$$ musicas

Na primeira linha mandei para /tmp/mus$$ o arquivo musicas, sem os registros que atendessem a consulta feita pelo comando grep. Em seguida, movi (que, no duro, equivale a renomear) /tmp/mus$$ por cima do antigo musicas.

Usei o arquivo /tmp/mus$$ como arquivo de trabalho, porque como já havia citado no artigo anterior, o $$ contém o PID (Process Identification ou identificação do processo) e desta forma cada um que editar o arquivo musicas o fará em um arquivo de trabalho diferente, desta forma evitando colisões no uso.

– Aê cara, estes programas que fizemos até aqui estão muito primários em virtude da falta de ferramentas que ainda temos. Mas é bom, enquanto eu tomo mais um chope, você ir para casa praticar em cima dos exemplos dados porque, eu prometo, chegaremos a desenvolver um sistema bacana para controle dos seus CDs.
– Quando nos encontrarmos da próxima vez, vou te ensinar como funcionam os comandos condicionais e aprimoraremos mais um pouco estes scripts.
– Por hoje chega! Já falei demais e preciso molhar a palavra porque estou de goela seca!
– Garçom! Mais um sem colarinho!

Vou aproveitar também para mandar o meu jabá: diga para os amigos que quem estiver afim de fazer um curso porreta de programação em Shell que mande um e-mail para a nossa gerencia de treinamento para informar-se.

Qualquer dúvida ou falta de companhia para um chope ou até para falar mal dos políticos é só mandar um e-mail para mim.

Valeu!

Read Full Post »

Papo de Botequim – Parte 3

– Garçon, traga dois chopes por favor que hoje eu vou ter que falar muito.

Trabalhando com cadeias

Pelo título acima não pense você que vou lhe ensinar a ser carcereiro! Estou me referindo a cadeia de caracteres!

O Comando cut (que não é a central de trabalhadores)

Primeiro quero te mostrar, de forma eminentemente prática uma instrução simples de usar e muito útil: o comando cut, Esta instrução é usada para cortar um determinado pedaço de um arquivo e tem duas formas distintas de uso

O comando cut com a opção -c

Com esta opção, o comando tem a seguinte sintaxe:
cut -c PosIni-PosFim [arquivo]

Onde:
PosIni = Posição inicial
PosFim = Posição final

$ cat numeros
1234567890
0987654321
1234554321
9876556789

$ cut -c1-5 numeros
12345
09876
12345
98765

$ cut -c-6 numeros
123456
098765
123455
987655

$ cut -c4- numeros
4567890
7654321
4554321
6556789

$ cut -c1,3,5,7,9 numeros
13579
08642
13542
97568

$ cut -c -3,5,8- numeros
1235890
0986321
1235321
9875789

Como dá para ver, no duro mesmo existem quatro sintaxes distintas: na primeira (-c 1-5), eu especifiquei uma faixa, na segunda (-c -6), especifiquei tudo até uma posição, na terceira (-c 4-) de uma determinada posição em diante e na quarta (-c 1,3,5,7,9), determinadas posições. A última (-c -3,5,8-) foi só para mostrar que podemos misturar tudo.

O comando cut com a opção -f

Mas não pense você que acabou por aí! Como você deve ter percebido esta forma de cut é útil para arquivos com campos de tamanho fixo, mas atualmente o que mais existe são arquivos com campos de tamanho variáveis, onde cada campo termina com um delimitador. Vamos dar uma olhada no arquivo musicas que começamos a preparar no nosso papo na última vez que viemos aqui no botequim.

$ cat musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica5
album 4^Artista7~Musica7:Artista8~Musica8

Então, recapitulando, o seu “leiaute” é o seguinte:
nome do album^interprete1~nome da musica1:...:interpreten~nome da musican

isto é, o nome do álbum será separado por um circunflexo (^) do resto do registro, que é formado por diversos grupos compostos pelo intérprete de cada música do CD e a respectiva música interpretada. Estes grupos são separados entre si por dois-pontos (:) e internamente, o nome do intérprete será separado por um til (~) do nome da música.

Então para pegarmos os dados referentes a todas as segundas músicas do arquivo musicas, devemos fazer:

$ cut -f2 -d: musicas
Artista2~Musica2
Artista4~Musica4
Artista6~Musica5
Artista8~Musica8

Ou seja, cortamos o segundo campo (-f de field em inglês) delimitado (-d) por dois-pontos (:). Mas, se quisermos somente os intérpretes, devemos fazer:

$ cut -f2 -d: musicas | cut -f1 -d~
Artista2
Artista4
Artista6
Artista8

Para entender isso, vamos pegar a primeira linha de musicas:

$ head -1 musicas
album 1^Artista1~Musica1:Artista2~Musica2

Então observe o que foi feito:

Delimitador do primeiro cut (:)

album 1^Artista1~Musica1:Artista2~Musica2

Desta forma, no primeiro cut, o primeiro campo do delimitador (-d) dois-pontos (:) é album 1^Artista1~Musica1 e o segundo, que é o que nos interessa, é Artista2~Musica2.

Vamos então ver o que aconteceu no segundo cut:

Novo delimitador (~)

Artista2~Musica2

Agora, primeiro campo do delimitador (-d) til (~), que é o que nos interessa, é Artista2 e o segundo é Musica2.

Se o raciocínio que fizemos para a primeira linha for aplicado no restante do arquivo, chegaremos à resposta anteriormente dada.

Se tem cut tem paste

Como já era de se esperar, o comando paste serve para colar, só que aqui no Shell o que ele cola são arquivos. Só para começar a entendê-lo, vamos fazer assim::

    paste arq1 arq2

Desta forma ele mandará para a saída padrão (stdout) cada um dos registros de arq1 ao lado dos registros de arq2 correspondentes e caso nenhum delimitador seja especificado, ele usará por default o <TAB>.

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 que mostramos acima:

$ paste inteiros pares
1 2
2 4
3 6
4 8
5 10
6
7
8
9
10

Quem está em pé, deita

Agora vamos transformar a coluna do pares em linha:

$ paste -s pares
2 4 6 8 10

Usando separadores

Como já foi dito, o separador default do paste é o <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 -sd’+’ pares | bc
30

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

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

Com o comando paste você também pode montar formatações exóticas como esta a seguir:

$ ls | paste -s -d’ttn’
arq1 arq2 arq3
arq4 arq5 arq6

O que aconteceu foi o seguinte: foi especificado para o comando paste que ele transformaria linhas em colunas (pela opção -s) e que os seus separadores (é…! Ele aceita mais de um, mas somente um após cada coluna criada pelo comando) seriam uma <TAB>, outra <TAB> e um <ENTER>, gerando desta forma a saída tabulada em 3 colunas.

Agora que você já entendeu isto, veja como fazer a mesma coisa, porém de forma mais fácil e menos bizarra e tosca, usando o mesmo comando mas com a seguinte sintaxe:

$ ls | paste – – –
arq1 arq2 arq3
arq4 arq5 arq6

E isto acontece porque se ao invés de especificarmos os arquivos colocarmos o sinal de menos (-), o comando paste os substitui pela saída ou entrada padrão conforme o caso. No exemplo anterior os dados foram mandados para a saída padrão (stdout), porque o pipe (|) estava desviando a saída do ls para a entrada padrão (stdin) do paste, mas veja o exemplo a seguir:

$ cat arq1
predisposição
privilegiado
profissional

$ cat arq2
encher
mário
motor

$ cut -c-3 arq1 | paste -d “” – arq2
preencher
primário
promotor

Neste caso, o cut devolveu as três primeiras letras de cada registro de arq1, o paste foi montado para não ter separador (-d"") e receber a entrada padrão (desviada pelo pipe) no traço (-) gerando a saída juntamente com arq2.

O Comando tr

Outro comando muito interessante é o tr que serve para substituir, comprimir ou remover caracteres. Sua sintaxe segue o seguinte padrão:

    tr [opções] cadeia1 [cadeia2]

O comando tr copia o texto da entrada padrão (stdin), troca as ocorrência dos caracteres de cadeia1 pelo seu correspondente na cadeia2 ou troca múltiplas ocorrências dos caracteres de cadeia1 por somente um caracter, ou ainda remove caracteres da cadeia1.

As principais opções do comando são:

Principais Opções do comando tr
Opção Significado
-s Comprime n ocorrências de cadeia1 em apenas uma
-d Remove os caracteres de cadeia1

Trocando caracteres com tr

Primeiro vou te dar um exemplo bem bobo:

$ echo bobo | tr o a
baba

Isto é, troquei todas as ocorrências da letra o pela letra a.

Suponha que em um determinado ponto do meu script eu peça ao operador para teclar s ou n (sim ou não), e guardo sua resposta na variável $Resp. Ora o conteúdo de $Resp pode estar com letra maiúscula ou minúscula, e desta forma eu teria que fazer diversos testes para saber se a resposta dada foi S, s, N ou n. Então o melhor é fazer:

$ Resp=$(echo $Resp | tr SN sn)

e após este comando eu teria certeza que o conteúdo de $Resp seria um s ou um n.

Se o meu arquivo ArqEnt está todo escrito com letras maiúsculas e desejo passá-las para minúsculas eu faço:

$ tr A-Z a-z < ArqEnt > /tmp/$$
$ mv -f /tmp/$$ ArqEnt

Note que neste caso usei a notação A-Z para não escrever ABCD...YZ. Outro tipo de notação que pode ser usada são as escape sequences (prefiro escrever no bom e velho português, mas nesse caso como eu traduziria? Seqüências de escape? Meio sem sentido, né? Mas vá lá…) que também são reconhecidas por outros comandos e também na linguagem C, e cujo significado você verá a seguir:

Escape Sequences
Seqüência Significado Octal
t Tabulação 11
n Nova linha 12
v Tabulação Vertical 13
f Nova Página 14
r Início da linha <^M> 15
\ Uma barra invertida 134

Removendo caracteres com tr

Então deixa eu te contar um “causo”: um aluno que estava danado comigo, resolveu complicar a minha vida e em um exercício prático valendo nota que passei para ser feito no computador, me entregou o script com todos os comandos separados por ponto-e-vírgula (lembra que eu disse que o ponto-e-vírgula servia para separar diversos comandos em uma mesma linha?).

Vou dar um exemplo simplificado e idiota de uma “tripa” assim:

$ cat confuso
echo leia Programação Shell Linux do Julio Cezar Neves > livro;cat livro;pwd;ls;rm -f lixo 2>/dev/null;cd ~

Eu executava o programa e ele funcionava:

$ confuso
leia Programação Shell Linux do Julio Cezar Neves
/home/jneves/LM
confuso livro musexc musicas musinc muslist numeros

Mas nota de prova é coisa séria (e nota de dólar é mais ainda :)) então, para entender o que o aluno havia feito, o chamei e em sua frente executei o seguinte comando:

$ tr “;” “n” < confuso
echo leia Programação Shell Linux do Julio Cezar Neves
pwd
ls
rm -f lixo 2>/dev/null

cd ~

O cara ficou muito desapontado, porque em 2 ou 3 segundos eu desfiz a gozação que ele perdera horas para fazer.
Mas preste atenção! Se eu estivesse em uma máquina com Unix, eu teria feito:

$ tr “;” “12” < confuso

Xpremendo com tr

Agora veja a diferença entre os dois comandos date: o que fiz hoje e outro que foi executado há duas semanas:

$ date # Hoje
Sun Sep 19 14:59:54 2004

$ date # Há duas semanas
Sun Sep 5 10:12:33 2004

Para pegar a hora eu deveria fazer:

$ date | cut -f 4 -d ‘ ‘
14:59:54

Mas duas semanas antes ocorreria o seguinte:

$ date | cut -f 4 -d ‘ ‘
5

Mas observe porque:

$ date # Há duas semanas
Sun Sep 5 10:12:33 2004

Como você pode notar, existem 2 caracteres em branco antes do 5 (dia), o que estraga tudo porque o terceiro pedaço está vazio e o quarto é o dia (5). Então o ideal seria comprimir os espaços em brancos sucessivos em somente um espaço para poder tratar as duas cadeias resultantes do comando date da mesma forma, e isso se faz assim:

$ date | tr -s ” “a
Sun Sep 5 10:12:33 2004

Como você pode ver não existem mais os dois espaços, então agora eu poderia cortar:

$ date | tr -s ” ” | cut -f 4 -d ” “
10:12:33

Olha só como o Shell já está quebrando o galho. Veja este arquivo que foi baixado de uma máquina com aquele sistema operacional que pega vírus:

$ cat -ve ArqDoDOS.txt
Este arquivo^M$
foi gerado pelo^M$
DOS/Rwin e foi^M$
baixado por um^M$
ftp mal feito.^M$

E agora eu quero te dar duas dicas:

Pinguim com placa de dica Dica #1 – A opção -v do cat mostra os caracteres de controle invisíveis, com a notação ^L, onde ^ é a tecla control e L é a respectiva letra. A opção -e mostra o final da linha como um cifrão ($).
Pinguim com placa de dica Dica #2 – Isto ocorre porque no formato DOS (ou rwin), o fim dos registros é formado por um carriage-return (r) e um line-feed (n). No Linux porém o final do registro tem somente o line-feed.

Vamos então limpar este arquivo.

$ tr -d ‘r’ < ArqDoDOS.txt > /tmp/$$
$ mv -f /tmp/$$ ArqDoDOS.txt

Agora vamos ver o que aconteceu:

$ cat -ve ArqDoDOS.txt
Este arquivo$
foi gerado pelo$
DOS/Rwin e foi$
baixado por um$
ftp mal feito.$

Bem a opção -d do tr remove o caractere especificado de todo o arquivo. Desta forma eu removi os caracteres indesejados salvando em um arquivo de trabalho e posteriormente renomeei-o para a sua designação original.
Obs: No Unix eu deveria fazer:

$ tr -d ’15’ < ArqDoDOS.txt > /tmp/$$
Pinguim com placa de atenção Isto aconteceu porque o ftp foi feito do modo binário (ou image), isto é, sem a interpretação do texto. Se antes da transmissão do arquivo tivesse sido estipulada a opção ascii do ftp, isto não teria ocorrido.

– Olha, depois desta dica tô começando a gostar deste tal de Shell, mas ainda tem muita coisa que não consigo fazer.

– Pois é, ainda não te falei quase nada sobre programação em Shell, ainda tem muita coisa para aprender, mas com o que aprendeu, já dá para resolver muitos problemas, desde que você adquira o “modo Shell de pensar”. Você seria capaz de fazer um script para me dizer quais são as pessoas que estão “logadas” há mais de um dia no seu servidor?

– Claro que não! Para isso seria necessário eu conhecer os comandos condicionais que você ainda não me explicou como funcionam.

– Deixa eu tentar mudar um pouco a sua lógica e trazê-la para o “modo Shell de pensar”, mas antes é melhor tomarmos um chope… Ô Chico, traz mais dois…

– Agora que já molhei a palavra, vamos resolver o problema que te propus. Repare como funciona o comando who:

$ who
jneves pts/1 Sep 18 13:40
rtorres pts/0 Sep 20 07:01
rlegaria pts/1 Sep 20 08:19
lcarlos pts/3 Sep 20 10:01

E veja também o date:

$ date
Mon Sep 20 10:47:19 BRT 2004

Repare que o mês e o dia estão no mesmo formato em ambos os comandos.

Pinguim com placa de dica Algumas vezes um comando tem a saída em português e o outro em inglês. Quando isso ocorrer, você pode usar o seguinte artifício:

$ date
Mon Sep 20 10:47:19 BRT 2004

$ LANG=pt_BR date
Seg Set 20 10:47:19 BRT 2004

Desta forma passando a saída do comando date para português.

Ora, se em algum registro do who eu não encontrar a data de hoje, é sinal que o cara está “logado” há mais de um dia, já que ele não pode ter se “logado” amanhã… Então vamos guardar o pedaço que importa da data de hoje para procurá-la na saída do who:

$ Data=$(date | cut -c 5-10)

Eu usei a construção $(...), para priorizar a execução dos comandos antes de atribuir a sua saída à variável $Data. Vamos ver se funcionou:

$ echo $Data
Sep 20

Beleza! Agora, o que temos que fazer é procurar no comando who os registros que não possuem esta data.

– Ah! Eu acho que estou entendendo! Você falou em procurar e me ocorreu o comando grep, estou certo?

– Certíssimo! Só que eu tenho que usar o grep com aquela opção que ele só lista os registros nos quais ele não encontrou a cadeia. Você se lembra que opção é essa?

– Claro, é a opção -v

– Isso! Tá ficando bão! Então vamos ver:

$ who | grep -v “$Data”
jneves pts/1 Sep 18 13:40

– E se eu quisesse mais um pouco de perfumaria eu faria assim:

$ who | grep -v “$Data” | cut -f1 -d ‘ ‘
jneves

– Viu? Não foi necessário usar nenhum comando condicional, até porque o nosso mais usado comando condicional, o famoso if, não testa condição, mas sim instruções, como veremos agora.

Comandos Condicionais

Veja as linhas de comando a seguir:

$ ls musicas
musicas

$ echo $?
0

$ ls ArqInexistente
ls: ArqInexistente: No such file or directory

$ echo $?
1

$ who | grep jneves
jneves pts/1 Sep 18 13:40 (10.2.4.144)

$ echo $?
0

$ who | grep juliana
$ echo $?

1

– O que é esse $? faz aí? Começando por cifrão ($) parece ser uma variável, certo?

– Sim é uma variável que contém o código de retorno da última instrução executada. Posso te garantir que se esta instrução foi bem sucedida, $? terá o valor zero, caso contrário seu valor será diferente de zero.

O Comando if

O que o nosso comando condicional if faz é testar a variável $?. Então vamos ver a sua sintaxe:

    if cmd
    then
        cmd1
        cmd2
        cmdn
    else
        cmd3
        cmd4
        cmdm
    fi

ou seja: caso comando cmd tenha sido executado com sucesso, os comandos do bloco do then (cmd1, cmd2 e cmdn) serão executados, caso contrário, os comandos executados serão os do bloco opcional do else (cmd3, cmd4 e cmdm), terminando com um fi.

Vamos ver na prática como isso funciona usando um scriptizinho que serve para incluir usuários no /etc/passwd:

$ cat incusu
#!/bin/bash
# Versão 1
if grep ^$1 /etc/passwd
then
echo Usuario ‘$1’ já existe
else
if useradd $1
then
echo Usuário ‘$1’ incluído em /etc/passwd
else
echo “Problemas no cadastramento. Você é root?”
fi
fi

Repare que o if está testando direto o comando grep e esta é a sua finalidade. Caso o if seja bem sucedido, ou seja, o usuário (cujo nome está em $1) foi encontrado em /etc/passwd, os comandos do bloco do then serão executados (neste exemplo é somente o echo) e caso contrário, as instruções do bloco do else é que serão executadas, quando um novo if testa se o comando useradd foi executado a contento, criando o registro do usuário em /etc/passwd, ou não quando dará a mensagem de erro.
Vejamos sua execução, primeiramente passando um usuário já cadastrado:

$ incusu jneves
jneves:x:54002:1001:Julio Neves:/home/jneves:/bin/bash
Usuario ‘jneves’ ja existe

Como já vimos diversas vezes, mas é sempre bom insistir no tema para que você já fique precavido, no exemplo dado surgiu uma linha indesejada, ela é a saída do comando grep. Para evitar que isso aconteça, devemos desviar a saída desta instrução para /dev/null, ficando assim:

$ cat incusu
#!/bin/bash
# Versão 2
if grep ^$1 /etc/passwd > /dev/null # ou: if grep -q ^$1 /etc/passwd
then
echo Usuario ‘$1’ já existe
else
if useradd $1
then
echo Usuário ‘$1’ incluído em /etc/passwd
else
echo “Problemas no cadastramento. Você é root?”
fi
fi

Agora vamos testá-lo como usuário normal (não root):

$ incusu ZeNinguem
./incusu[6]: useradd: not found
Problemas no cadastramento. Você é root?

Epa, aquele erro não era para acontecer! Para evitar que isso aconteça devemos mandar também a saída de erro (strerr, lembra?) do useradd para /dev/null, ficando na versão final assim:

$ cat incusu
#!/bin/bash
# Versão 3
if grep ^$1 /etc/passwd > /dev/null
then
echo Usuario ‘$1’ já existe
else
if useradd $1 2> /dev/null
then
echo Usuário ‘$1’ incluído em /etc/passwd
else
echo “Problemas no cadastramento. Você é root?”
fi
fi

Depois destas alterações e de fazer um su – (me tornar root) vejamos o seu comportamento:

$ incusu botelho
Usuário ‘botelho’ incluido em /etc/passwd

E novamente:

$ incusu botelho
Usuário ‘botelho’ já existe

Lembra que eu falei que ao longo dos nossos papos e chopes os nossos programas iriam se aprimorando? Então vejamos agora como poderíamos melhorar o nosso programa para incluir músicas:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 3)
#
if grep “^$1$” musicas > /dev/null
then
echo Este álbum já está cadastrado
else
echo $1 >> musicas
sort musicas -o musicas
fi

Como você viu, é uma pequena evolução da versão anterior, assim, antes de incluir um registro (que pela versão anterior poderia ser duplicado), testamos se o registro começava (^) e terminava ($) igual ao parâmetro passado ($1). O uso do circunflexo (^) no início da cadeia e cifrão ($) no fim, são para testar se o parâmetro passado (o álbum e seus dados) são exatamente iguais a algum registro anteriormente cadastrado e não somente igual a um pedaço de algum dos registros.
Vamos executá-lo passando um álbum já cadastrado:

$ musinc “album 4^Artista7~Musica7:Artista8~Musica8”
Este álbum já está cadastrado

E agora um não cadastrado:

$ musinc “album 5^Artista9~Musica9:Artista10~Musica10”
$ cat musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica5
album 4^Artista7~Musica7:Artista8~Musica8
album 5^Artista9~Musica9:Artista10~Musica10

– Como você viu, o programa melhorou um pouquinho, mas ainda não está pronto. À medida que eu for te ensinando a programar em shell, nossa CDteca irá ficando cada vez melhor.

– Entendi tudo que você me explicou, mas ainda não sei como fazer um if para testar condições, ou seja o uso normal do comando.

– Cara, para isso existe o comando test, ele é que testa condições. O comando if testa o comando test. Mas isso está meio confuso e como já falei muito, estou precisando de uns chopes para molhar a palavra. Vamos parando por aqui e na próxima vez te explico direitinho o uso do test e de diversas outras sintaxes do if.

– Falou! Acho bom mesmo porque eu também já tô ficando zonzo e assim tenho tempo para praticar esse monte de coisas que você me falou hoje.

– Para fixar o que você aprendeu, tente fazer um scriptizinho para informar se um determinado usuário, que será passado como parâmetro esta logado (arghh!) ou não.

– Aê Chico, mais dois chopes por favor…

Vou aproveitar também para mandar o meu jabá: diga para os amigos que quem estiver afim de fazer um curso porreta de programação em Shell que mande um e-mail para a nossa gerencia de treinamento para informar-se.

Qualquer dúvida ou falta de companhia para um chope ou até para falar mal dos políticos é só mandar um e-mail para mim.

Valeu!

Read Full Post »

Papo de Botequim – Parte 5

– Fala cara! E as idéias estão em ordem? Já fundiu a cuca ou você ainda aguenta mais Shell?

– Guento! Tô gostando muito! Gostei tanto que até caprichei no exercício que você passou. Lembra que você me pediu para fazer um programa que receberia como parâmetro o nome de um arquivo e que quando executado salvaria este arquivo com o nome original seguido de um til (~) e colocaria este arquivo dentro do vi?

– Claro que lembro, me mostre e explique como você fez.

$ cat vira
#!/bin/bash
#
# vira – vi resguardando arquivo anterior
# == = =

# Verificando se foi passado 1 parametro
if [ “$#” -ne 1 ]
then
echo “Erro -> Uso: $0 ”
exit 1
fi

Arq=$1
# Caso o arquivo não exista, nao ha copia para ser salva
if [ ! -f “$Arq” ]
then
vi $Arq
exit 0
fi

# Se nao puder alterar o arquivo vou usar o vi para que?
if [ ! -w “$Arq” ]
then
echo “Voce nao tem direito de gravacao em $Arq”
exit 2
fi

# Ja que esta tudo OK, vou salvar a copia e chamar o vi
cp -f $Arq $Arq~
vi $Arq
exit 0

– É, beleza! Mas me diz uma coisa: porque você terminou o programa com um exit 0?

– Ahhh! Eu descobri que o número após o exit resultará no código de retorno do programa (o $?, lembra?), e desta forma, como foi tudo bem sucedido, ele encerraria com o $? = 0. Porém se você observar, verá que caso o programa não tenha recebido o nome do arquivo ou caso o operador não tivesse direito de gravação sobre este arquivo, o código de retorno ($?) seria diferente do zero.

– Grande garoto, aprendeu legal, mas é bom deixar claro que exit 0, simplesmente exit ou não colocar exit, produzem igualmente um código de retorno ($?) igual a zero. Agora vamos falar sobre as instruções de loop ou laço, mas antes vou passar o conceito de bloco de programa.

Até agora já vimos alguns blocos de programa. Quando te mostrei um exemplo para fazer um cd para dentro de um diretório que era assim:

cd lmb 2> /dev/null ||
    {
    mkdir lmb
    cd lmb
    }

O fragmento contido entre as duas chaves ({}), forma um bloco de comandos. Também neste exercício que acabamos de ver, em que salvamos o arquivo antes de editá-lo, existem vários blocos de comandos compreendidos entre os then e os fi do if.

Um bloco de comandos também pode estar dentro de um case, ou entre um do e um done.

– Peraí Julio, que do e done é esse, não me lembro de você ter falado nisso e olha estou prestando muita atenção…

– Pois é, ainda não havia falado porque não havia chegado o momento propício. Todas as instruções de loop ou laço, executam os comandos do bloco compreendido entre o do e o done.

Comandos de Loop (ou laço)

As instruções de loop ou laço são o for, o while e o until que passarei a te explicar uma-a-uma a partir de agora.

O comando for

Se você está habituado a programar, certamente já conhece o comando for, mas o que você não sabe é que o for, que é uma instrução intrinseca do Shell (isto significa que o código fonte do comando faz parte do código fonte do Shell, ou seja em bom programês é um built-in), é muito mais poderoso que os seus correlatos das outras linguagens.

Vamos entender a sua sintaxe, primeiramente em português e depois como funciona no duro.

    para var em val1 val2 ... valn
    faça
        cmd1
        cmd2
        cmdn
    feito

Onde a variável var assume cada um dos valores da lista val1 val2 ... valn e para cada um desses valores executa o bloco de comandos formado por cmd1, cmd2 e cmdn

Agora que já vimos o significado da instrução em português, vejamos a sintaxe correta:

Primeira sintaxe do comando for

    for var in val1 val2 ... valn
    do
        cmd1
        cmd2
        cmdn
    done

Vamos direto para os exemplos, para entender direito o funcionamento deste comando. Vamos escrever um script para listar todos os arquivos do nosso diretório separados por dois-pontos, mas primeiro veja:

$ echo *
ArqDoDOS.txt1 confuso incusu logado musexc musicas musinc muslist

Isto é, o Shell viu o asterisco (*) expandindo-o com o nome de todos os arquivos do diretório e o comando echo jogou-os para a tela separados por espaços em branco. Visto isso vamos ver como resolver o problema a que nos propuzemos:

$ cat testefor1
#!/bin/bash
# 1o. Prog didático para entender o for

for Arq in *
do
echo -n $Arq: # A opcao -n eh para nao saltar linha
done

Então vamos executá-lo:

$ testefor1
ArqDoDOS.txt1:confuso:incusu:logado:musexc:musicas:musinc:muslist:
$

Como você viu o Shell transformou o asterísco (que odeia ser chamado de asterístico) em uma lista de arquivos separados por espaços em branco. quando o for viu aquela lista, ele disse: “Opa, lista separadas por espaços é comigo mesmo!”

O bloco de comandos a ser executado era somente o echo, que com a opção -n listou a variável $Arq seguida de dois-pontos (:), sem saltar a linha. O cifrão ($) do final da linha da execução é o prompt. que permaneceu na mesma linha também em função da opção -n. Outro exemplo simples (por enquanto):

$ cat testefor2
#!/bin/bash
# 2o. Prog didático para entender o for

for Palavra in Papo de Botequim
do
echo $Palavra
done

E executando vem:

$ testefor2
Papo
de
Botequim

Como você viu, este exemplo é tão bobo e simples como o anterior, mas serve para mostrar o comportamento básico do for.

Veja só a força do for: ainda estamos na primeira sintaxe do comando e já estou mostrando novas formas de usá-lo. Lá atrás eu havia falado que o for usava listas separadas por espaços em branco, mas isso é uma meia verdade, era só para facilitar a compreensão.

No duro, as listas não são obrigatóriamente separadas por espaços mas antes de prosseguir, deixa eu te mostrar como se comporta uma variável do sistema chamada de $IFS. Repare seu conteúdo:

$ echo “$IFS” | od -h
0000000 0920 0a0a
0000004

Isto é, mandei a variável (protegida da interpretação do Shell pelas aspas) para um dump hexadecimal (od -h) e resultou:

Conteúdo da Variável $IFS
Hexadecimal Significado
09 <TAB>
20 <ESPAÇO>
0a <ENTER>

Onde o último 0a foi proveniente do <ENTER> dado ao final do comando. Para melhorar a explicação, vamos ver isso de outra forma:

$ echo “:$IFS:” | cat -vet
: ^I$
:$

Preste atenção na dica a seguir para entender a construção deste comando cat:

Pinguim com placa de dica No comando cat, a opção -e representa o <ENTER> como um cifrão ($) e a opção -t representa o <TAB> como um ^I. Usei os dois-pontos (:) para mostrar o início e o fim do echo. E desta forma, mais uma vez pudemos notar que os três caracteres estão presentes naquela variável.

Agora veja você, IFS significa Inter Field Separator ou, traduzindo, separador entre campos. Uma vez entendido isso, eu posso afirmar (porque vou provar) que o comando for não usa listas separadas por espaços em branco, mas sim pelo conteúdo da variável $IFS, cujo valor padrão (default) são esses caracteres que acabamos de ver. Para comprovarmos isso, vamos mostrar um script que recebe o nome do artista como parâmetro e lista as músicas que ele executa, mas primeiramente vamos ver como está o nosso arquivo musicas:

$ cat musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica6
album 4^Artista7~Musica7:Artista1~Musica3
album 5^Artista9~Musica9:Artista10~Musica10

Em cima deste “leiaute” foi desenvolvido o script a seguir:

$ cat listartista
#!/bin/bash
# Dado um artista, mostra as suas musicas

if [ $# -ne 1 ]
then
echo Voce deveria ter passado um parametro
exit 1
fi

IFS=”
:”

for ArtMus in $(cut -f2 -d^ musicas)
do
echo “$ArtMus” | grep $1 && echo $ArtMus | cut -f2 -d~
done

O script, como sempre, começa testando se os parâmetros foram passados corretamente, em seguida o IFS foi setado para <ENTER> e dois-pontos (:) (como demonstram as aspas em linha diferentes), porque é ele que separa os blocos Artistan~Musicam. Desta forma, a variável $ArtMus irá receber cada um destes blocos do arquivo (repare que o for já recebe os registros sem o álbum em virtude do cut na sua linha). Caso encontre o parâmetro ($1) no bloco, o segundo cut listará somente o nome da música. Vamos executá-lo:

$ listartista Artista1
Artista1~Musica1
Musica1
Artista1~Musica3
Musica3
Artista10~Musica10
Musica10

Êpa! Aconteceram duas coisas indesejáveis: os blocos também foram listados e a Musica10 idem. Além do mais, o nosso arquivo de músicas está muito simples, na vida real, tanto a música quanto o artista têm mais de um nome. Suponha que o artista fosse uma dupla sertaneja chamada Perereca & Peteleca (não gosto nem de dar a idéia com receio que isso se torne realidade:). Neste caso o $1 seria Perereca e o resto deste lindo nome seria ignorado na pesquisa.

Para que isso não ocorresse, eu deveia passar o nome do artista entre aspas (") ou alterar $1 por $@ (que significa todos os parâmetros passados), que é a melhor solução, mas neste caso eu teria que modificar a crítica dos parâmetros e o grep. A nova crítica não seria se eu passei um parâmetro, mas pelo menos um parâmetro e quanto ao grep, veja só o que resultaria após a substituição do $* (que entraria no lugar do $1) pelos parâmetros:

    echo "$ArtMus" | grep perereca & peteleca

O que resultaria em erro. O correto seria:

    echo "$ArtMus" | grep -i "perereca & peteleca"

Onde foi colocado a opção -i para que a pesquisa ignorasse maiúsculas e minúsculas e as aspas também foram inseridas para que o nome do artista fosse visto como uma só cadeia monolítica.

Ainda falta consertar o erro dele ter listado o Artista10. Para isso o melhor é dizer ao grep que a cadeia está no início de $ArtMus (a expressão regular para dizer que está no início é ^) e logo após vem um til (~). É necessário também que se redirecione a saída do grep para /dev/null para que os blocos não sejam mais listados. Veja então a nova (e definitiva) cara do programa:

$ cat listartista
#!/bin/bash
# Dado um artista, mostra as suas musicas
# versao 2

if [ $# -eq 0 ]
then
echo Voce deveria ter passado pelo menos um parametro
exit 1
fi

IFS=”
:”

for ArtMus in $(cut -f2 -d^ musicas)
do
echo “$ArtMus” | grep -i “^$@~” > /dev/null && echo $ArtMus | cut -f2 -d~
done

Que executando vem:

$ listartista Artista1
Musica1
Musica3

Segunda sintaxe do comando for

    for var
    do
        cmd1
        cmd2
        cmdn
    done

– Ué, sem o in como ele vai saber que valor assumir?

– Pois é, né? Esta construção a primeira vista parece xquisita mas é bastante simples. Neste caso, var assumirá um-a-um cada um dos parâmetros passados para o progama.

Vamos logo aos exemplos para entender melhor. Vamos fazer um script que receba como parâmetro um monte de músicas e liste seus autores:

$ cat listamusica
#!/bin/bash
# Recebe parte dos nomes de musicas como parametro e
# lista os interpretes. Se o nome for composto, deve
# ser passado entre aspas.
# ex. “Eu nao sou cachorro nao” “Churrasquinho de Mae”
#
if [ $# -eq 0 ]
then
echo Uso: $0 musica1 [musica2] … [musican]
exit 1
fi
IFS=”
:”
for Musica
do
echo $Musica
Str=$(grep -i “$Musica” musicas) ||
{
echo ” Não encontrada”
continue
}
for ArtMus in $(echo “$Str” | cut -f2 -d^)
do
echo ” $ArtMus” | grep -i “$Musica” | cut -f1 -d~
done
done

Da mesma forma que os outros, começamos o exercício com uma crítica sobre os parâmetros recebidos, em seguida fizemos um for em que a variável $Musica receberá cada um dos parâmetros passados, colocando em $Str todos os álbuns que contém as músicas passadas. Em seguida, o outro for pega cada bloco Artista~Musica nos registros que estão em $Str e lista cada artista que execute aquela música.

Como sempre vamos executá-lo para ver se funciona mesmo:

$ listamusica musica3 Musica4 “Eguinha Pocotó”
musica3
Artista3
Artista1
Musica4
Artista4
Eguinha Pocotó
Não encontrada

A listagem ficou feinha porque ainda não sabemos formatar a saída, mas qualquer dia desses, quando você souber posicionar o cursor, fazer negrito, trabalhar com cores e etc, faremos esta listagem novamente usando todas estas perfumarias e ela ficará muito fashion.

A esta altura dos acontecimentos você deve estar se perguntando: “E aquele for tradicional das outras linguagens em que ele sai contando a partir de um número, com um determinado incremento até alcançar uma condição?”

E é aí que eu te respondo: “Eu não te disse que o nosso for é mais porreta que os outros?” Para fazer isso existem duas formas:

1 – Com a primeira sintaxe que vimos, como nos exemplos a seguir direto no prompt:

$ for i in $(seq 9)
> do
> echo -n “$i ”
> done
1 2 3 4 5 6 7 8 9

Neste a variável i assumiu os inteiros de 1 a 9 gerados pelo comando seq e a opção -n do echo foi usada para não saltar linha a cada número listado (sinto-me ecologicamente correto por não gastar um monte de papel da revista quando isso pode ser evitado). Ainda usando o for com seq:

$ for i in $(seq 3 9)
> do
> echo -n “$i ”
> done
4 5 6 7 8 9

Ou ainda na forma mais completa do seq:

$ for i in $(seq 0 3 9)
> do
> echo -n “$i ”
> done
0 3 6 9

2 – A outra forma de fazer o desejado é com uma sintaxe muito semelhante ao for da linguagem C, como veremos a seguir.

Terceira sintaxe do comando for

    for ((var=ini; cond; incr))
    do
        cmd1
        cmd2
        cmdn
    done

Onde:

var=ini – Significa que a variável var começará de um valor inicial ini;
cond – Siginifica que o loop ou laço do for será executado enquanto var não atingir a condição cond;
incr – Significa o incremento que a variável var sofrerá em cada passada do loop.

Como sempre vamos aos exemplos que a coisa fica mais fácil:

$ for ((i=1; i<=9; i++))
> do
> echo -n “$i ”
> done
1 2 3 4 5 6 7 8 9

Neste caso a variável i partiu do valor inicial 1, o bloco de comando (neste caso somente o echo) será executado enquanto i menor ou igual (<=) a 9 e o incremento de i será de 1 a cada passada do loop.

Repare que no for propriamente dito (e não no bloco de comandos) não coloquei um cifrão ($) antes do i, e a notação para incrementar (i++) é diferente do que vimos até agora. Isto é porque o uso de parênteses duplos (assim como o comando let) chama o interpretador aritmético do Shell, que é mais tolerante.

Como me referi ao comando let, só para mostrar como ele funciona e a versatilidade do for, vamos fazer a mesma coisa, porém omitindo a última parte do escopo do for, passando-a para o bloco de comandos.

$ for ((; i<=9;))
> do
> let i++
> echo -n “$i ”
> done
1 2 3 4 5 6 7 8 9

Repare que o incremento saiu do corpo do for e passou para o bloco de comandos, repare também que quando usei o let, não foi necessário sequer inicializar a variável $i. Veja só os comandos a seguir dados diretamente no prompt para mostrar o que acabo de falar:

$ echo $j$ let j++
$ echo $j
1

Ou seja, a variável $j sequer existia e no primeiro let assumiu o valor 0 (zero) para, após o incremento, ter o valor 1.

Veja só como as coisas ficam simples:

$ for arq in *
> do
> let i++
> echo “$i -> $Arq”
> done
1 -> ArqDoDOS.txt1
2 -> confuso
3 -> incusu
4 -> listamusica
5 -> listartista
6 -> logado
7 -> musexc
8 -> musicas
9 -> musinc
10 -> muslist
11 -> testefor1
12 -> testefor2

– Pois é amigo, tenho certeza que você já tomou um xarope do comando for. Por hoje chega, na próxima vez que nos encontrarmos falaremos sobre outras instruções de loop, mas eu gostaria que até lá você fizesse um pequeno script para contar a quantidade de palavras de um arquivo texto, cujo nome seria recebido por parâmetro.

OBS: Essa contagem tem de ser feita usando o comando for para se habituar ao seu uso. Não vale usar o wc -w.

– Aê Chico! Traz a saideira.

Vou aproveitar também para mandar o meu jabá: diga para os amigos que quem estiver afim de fazer um curso porreta de programação em Shell que mande um e-mail para a nossa gerencia de treinamento para informar-se.

Qualquer dúvida ou falta de companhia para um chope ou até para falar mal dos políticos é só mandar um e-mail para mim.

Valeu!

Read Full Post »

Papo de Botequim – Parte 8


– E aê cara tudo bem? – Legal!, eu queria te mostrar o que fiz mas já sei que você vai querer molhar o bico primeiro né?- Só pra contrariar, hoje vou deixar você mostrar logo o seu “bacalho”. Vai mostra aí o que você fez.- Poxa o exercício que você passou é muito grande. Veja só como eu resolvi:

$ cat musinc5
#!/bin/bash
# Cadastra CDs (versao 5)
#
clear
LinhaMesg=$((`tput lines` – 3)) # Linha que msgs serão dadas para operador
TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs
echo ”
Inclusao de Músicas
======== == =======
Título do Álbum:
| Este campo foi
Faixa: < criado somente para
| orientar o preenchimento
Nome da Música:
Intérprete:” # Tela montada com um único echo
while true
do
tput cup 5 38; tput el # Posiciona e limpa linha
read Album
[ ! “$Album” ] && # Operador deu <ENTER>
{
Msg=”Deseja Terminar? (S/n)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1))
read -n1 SN
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
[ $SN = “N” -o $SN = “n” ] && continue # $SN é igual a N ou (-o) n?
clear; exit # Fim da execução
}
grep “^$Album^” musicas > /dev/null &&
{
Msg=”Este álbum já está cadastrado”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
read -n1
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
continue # Volta para ler outro álbum
}
Reg=”$Album^” # $Reg receberá os dados para gravação
oArtista= # Variavel que guarda artista anterior
while true
do
((Faixa++))
tput cup 7 38
echo $Faixa
tput cup 9 38 # Posiciona para ler musica
read Musica
[ “$Musica” ] || # Se o operador tiver dado …
{
Msg=”Fim de Álbum? (S/n)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1)
read -n1 SN
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
[ “$SN” = N -o “$SN” = n ]&&continue # $SN é igual a N ou (-o) n?
break # Sai do loop para gravar
}
tput cup 11 38 # Posiciona para ler Artista
[ “$oArtista” ]&& echo -n “($oArtista) ” # Artista anterior é default
read Artista
[ “$Artista” ] && oArtista=”$Artista”
Reg=”$Reg$oArtista~$Musica:” # Montando registro
tput cup 9 38; tput el # Apaga Musica da tela
tput cup 11 38; tput el # Apaga Artista da tela
done
echo “$Reg” >> musicas # Grava registro no fim do arquivo
sort musicas -0 musicas # Classifica o arquivo
done

– É o programa tá legal, tá todo estruturadinho, mas gostaria de alguns poucos comentários sobre o que você fez:

  • Só para relembrar, as seguintes construções: [ ! $Album ] && e [ $Musica ] || representam a mesma coisa, isto é, no caso da primeira, o testamos se a variável $Album não (!) tem nada dentro, então (&&) … Na segunda, testamos se $Musica tem dado, senão (||) …
  • Se você reclamou do tamanho dele, é porque ainda não dei algumas dicas. Repare que a maior parte do script é para dar mensagens centradas na penúltima linha da tela. Repare ainda que algumas mensagens pedem um S ou um N e outras são só de advertência. Seria o caso típico do uso de funções, que seriam escritas somente uma vez e chamada a execução de diversos pontos do script. Vou montar duas funções para resolver estes casos e vamos incorporá-las ao seu programa para ver o resultado final.

Funções

– Chico! Agora traz dois chopes, sendo um sem colarinho, para me dar inspiração.

    Pergunta ()
        {
        #  A função recebe 3 parâmetros na seguinte ordem:
        #  $1 - Mensagem a ser dada na tela
        #  $2 - Valor a ser aceito com resposta default
        #  $3 - O outro valor aceito
        #  Supondo que $1=Aceita?, $2=s e $3=n, a linha a
        #  seguir colocaria em Msg o valor "Aceita? (S/n)"
        local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)"
        local TamMsg=${#Msg}
        local Col=$(((TotCols - TamMsg) / 2))  # Centra msg na linha
        tput cup $LinhaMesg $Col
        echo "$Msg"
        tput cup $LinhaMesg $((Col + TamMsg + 1))
        read -n1 SN
        [ ! $SN ] && SN=$2                     # Se vazia coloca default em SN
        echo $SN | tr A-Z a-z                  # A saída de SN será em minúscula
        tput cup $LinhaMesg $Col; tput el      # Apaga msg da tela
        return                                 # Sai da função
        }

Como podemos ver, uma função é definida quando fazemos nome_da_função () e todo o seu corpo está entre chaves ({}). Assim como conversamos aqui no Boteco sobre passagem de parâmetros, as funções os recebem da mesma forma, isto é, são parâmetros posicionais ($1, $2, ..., $n) e todas as regras que se aplicam a passagem de parâmetros para programas, também valem para funções, mas é muito importante realçar que os parâmetros passados para um programa não se confundem com aqueles que este passou para suas funções. Isso significa, por exemplo, que o $1 de um script é diferente do $1 de uma de suas funções

Repare que as variáveis $Msg, $TamMsg e $Col são de uso restrito desta rotina, e por isso foram criadas como local. A finalidade disso é simplesmente para economizar memória, já que ao sair da rotina, elas serão devidamente detonadas da partição e caso não tivesse usado este artifício, permaneceriam residentes.

A linha de código que cria local Msg, concatena ao texto recebido ($1) um abre parênteses, a resposta default ($2) em caixa alta, uma barra, a outra resposta ($3) em caixa baixa e finaliza fechando o parênteses. Uso esta convenção para, ao mesmo tempo, mostrar as opções disponíveis e realçar a resposta oferecida como default.

Quase ao fim da rotina, a resposta recebida ($SN) é passada para caixa baixa de forma que no corpo do programa não se precise fazer este teste.

Veja agora como ficaria a função para dar uma mensagem na tela:

    function MandaMsg
        {
        # A função recebe somente um parâmetro
        # com a mensagem que se deseja exibir,
        # para não obrigar ao programador passar
        # a msq entre aspas, usaremos $* (todos
        # os parâmetro, lembra?) e não $1.
        local Msg="$*"
        local TamMsg=${#Msg}
        local Col=$(((TotCols - TamMsg) / 2)) # Centra msg na linha
        tput cup $LinhaMesg $Col
        echo "$Msg"
        read -n1
        tput cup $LinhaMesg $Col; tput el     # Apaga msg da tela
        return                                # Sai da função
        }

Esta é uma outra forma de definir uma função: não a chamamos como no exemplo anterior usando uma construção com a sintaxe nome_da_função (), mas sim como function nome_da_função. Quanto ao mais, nada difere da anterior, exceto que, como consta dos comentários, usamos a variável $* que como já sabemos é o conjunto de todos os parâmetros passados, para que o programador não precise usar aspas envolvendo a mensagem que deseja passar para a função.

Para terminar com este blá-blá-blá vamos ver então as alterações que o programa necessita quando usamos o conceito de funções:

$ cat musinc6
#!/bin/bash
# Cadastra CDs (versao 6)
#

# Área de variáveis globais
LinhaMesg=$((`tput lines` – 3)) # Linha que msgs serão dadas para operador
TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs

# Área de funções
Pergunta ()
{
# A função recebe 3 parâmetros na seguinte ordem:
# $1 – Mensagem a ser dada na tela
# $2 – Valor a ser aceito com resposta default
# $3 – O outro valor aceito
# Supondo que $1=Aceita?, $2=s e $3=n, a linha
# abaixo colocaria em Msg o valor “Aceita? (S/n)”
local Msg=”$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)”
local TamMsg=${#Msg}
local Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1))
read -n1 SN
[ ! $SN ] && SN=$2 # Se vazia coloca default em SN
echo $SN | tr A-Z a-z # A saída de SN será em minúscula
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
return # Sai da função
}
function MandaMsg
{
# A função recebe somente um parâmetro
# com a mensagem que se deseja exibir,
# para não obrigar ao programador passar
# a msq entre aspas, usaremos $* (todos
# os parâmetro, lembra?) e não $1.
local Msg=”$*”
local TamMsg=${#Msg}
local Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
read -n1
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
return # Sai da função
}

# O corpo do programa propriamente dito começa aqui
clear
echo ”
Inclusao de Músicas
======== == =======
Título do Álbum:
| Este campo foi
Faixa: < criado somente para
| orientar o preenchimento
Nome da Música:
Intérprete:” # Tela montada com um único echo
while true
do
tput cup 5 38; tput el # Posiciona e limpa linha
read Album
[ ! “$Album” ] && # Operador deu
{
Pergunta “Deseja Terminar” s n
[ $SN = “n” ] && continue # Agora só testo a caixa baixa
clear; exit # Fim da execução
}
grep -iq “^$Album^” musicas 2> /dev/null &&
{
MandaMsg Este álbum já está cadastrado
continue # Volta para ler outro álbum
}
Reg=”$Album^” # $Reg receberá os dados de gravação
oArtista= # Guardará artista anterior
while true
do
((Faixa++))
tput cup 7 38
echo $Faixa
tput cup 9 38 # Posiciona para ler musica
read Musica
[ “$Musica” ] || # Se o operador tiver dado …
{
Pergunta “Fim de Álbum?” s n
[ “$SN” = n ] && continue # Agora só testo a caixa baixa
break # Sai do loop para gravar dados
}
tput cup 11 38 # Posiciona para ler Artista
[ “$oArtista” ]&& echo -n “($oArtista) ” # Artista anterior é default
read Artista
[ “$Artista” ] && oArtista=”$Artista”
Reg=”$Reg$oArtista~$Musica:” # Montando registro
tput cup 9 38; tput el # Apaga Musica da tela
tput cup 11 38; tput el # Apaga Artista da tela
done
echo “$Reg” >> musicas # Grava registro no fim do arquivo
sort musicas -o musicas # Classifica o arquivo
done

Repare que a estruturação do script está conforme o gráfico a seguir:

Variáveis Globais
Funções
Corpo do Programa

Esta estruturação é devido ao Shell ser uma linguagem interpretada e desta forma o programa é lido da esquerda para a direita e de cima para baixo e uma variável para ser vista simultaneamente pelo script e suas funções deve ser declarada (ou inicializada) antes de qualquer coisa. As funções por sua vez devem ser declaradas antes do corpo do programa propriamente dito porque no ponto em que o programador mencionou seu nome, o interpretador Shell já o havia antes localizado e registrado que era uma função.

Uma coisa bacana no uso de funções é fazê-las o mais genérico possível de forma que elas sirvam para outras aplicações, sem necessidade de serem reescritas. Essas duas que acabamos de ver têm uso generalizado, pois é difícil um script que tenha uma entrada de dados pelo teclado que não use uma rotina do tipo da MandaMsg ou não interage com o operador por algo semelhante à Pergunta.

Conselho de amigo: crie um arquivo e cada função nova que você criar, anexe-a a este arquivo. Ao final de um tempo você terá uma bela biblioteca de funções que lhe poupará muito tempo de programação.

O comando source

Vê se você nota algo de diferente na saída do ls a seguir:

$ ls -la .bash_profile
-rw-r–r– 1 Julio unknown 4511 Mar 18 17:45 .bash_profile

Não olhe a resposta não, volte a prestar atenção! Bem, já que você está mesmo sem saco de pensar e prefere ler a resposta, vou te dar uma dica: acho que você sabe que o .bash_profile é um dos programas que são automaticamente “executados” quando você se loga (ARRGGHH! Odeio este termo). Agora que te dei esta dica olhe novamente para a saída do ls e me diga o que há de diferente nela.

Como eu disse o .bash_profile é “executado” em tempo de logon e repare que não tem nenhum direito de execução. Isso se dá porque o se você o executasse como qualquer outro script careta, quando terminasse sua execução todo o ambiente por ele gerado morreria junto com o Shell sob o qual ele foi executado (você se lembra que todos os scripts são executados em subshells, né?).

Pois é. É para coisas assim que existe o comando source, também conhecido por . (ponto). Este comando faz com que não seja criado um novo Shell (um subshell) para executar o programa que que lhe é passado como parâmetro.

Melhor um exemplo que 453 palavras. Veja este scriptizinho a seguir:

$ cat script_bobo
cd ..
ls

Ele simplesmente deveria ir para o diretório acima do diretório atual. Vamos executar uns comandos envolvendo o script_bobo e vamos analisar os resultados:

$ pwd
/home/jneves

$ script_bobo
jneves juliana paula silvie

$ pwd
/home/jneves

Se eu mandei ele subir um diretório, porque não subiu? Subiu sim! O subshell que foi criado para executar o script tanto subiu que listou os diretórios dos quatro usuários abaixo do /home, só que assim que o script acabou, o subshell foi para o beleleu e com ele todo o ambiente criado. Olha agora como a coisa muda:

$ source script_bobo
jneves juliana paula silvie

$ pwd
/home

$ cd –
/home/jneves

$ . script_bobo
jneves juliana paula silvie

$ pwd
/home

Ahh! Agora sim! Sendo passado como parâmetro do comando source ou . (ponto), o script foi executado no Shell corrente deixando neste, todo o ambiente criado. Agora damos um rewind para o início da explicação sobre este comando. Lá falamos do .bash_profile, e a esta altura você já deve saber que a sua incumbência é, logo após o login, deixar o ambiente de trabalho preparado para o usuário, e agora entendemos que é por isso mesmo que ele é executado usando este artifício.

E agora você deve estar se perguntando se é só para isso que este comando serve, e eu lhe digo que sim, mas isso nos traz um monte de vantagens e uma das mais usadas é tratar funções como rotinas externas. Veja uma outra forma de fazer o nosso programa para incluir CDs no arquivo musicas:

$ cat musinc7
#!/bin/bash
# Cadastra CDs (versao 7)
#

# Área de variáveis globais
LinhaMesg=$((`tput lines` – 3)) # Linha que msgs serão dadas para operador
TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs

# O corpo do programa propriamente dito começa aqui
clear
echo ”
Inclusao de Músicas
======== == =======
Título do Álbum:
| Este campo foi
Faixa: < criado somente para
| orientar o preenchimento
Nome da Música:
Intérprete:” # Tela montada com um único echo
while true
do
tput cup 5 38; tput el # Posiciona e limpa linha
read Album
[ ! “$Album” ] && # Operador deu
{
source pergunta.func “Deseja Terminar” s n
[ $SN = “n” ] && continue # Agora só testo a caixa baixa
clear; exit # Fim da execução
}
grep -iq “^$Album^” musicas 2> /dev/null &&
{
. mandamsg.func Este álbum já está cadastrado
continue # Volta para ler outro álbum
}
Reg=”$Album^” # $Reg receberá os dados de gravação
oArtista= # Guardará artista anterior
while true
do
((Faixa++))
tput cup 7 38
echo $Faixa
tput cup 9 38 # Posiciona para ler musica
read Musica
[ “$Musica” ] || # Se o operador tiver dado …
{
. pergunta.func “Fim de Álbum?” s n
[ “$SN” = n ] && continue # Agora só testo a caixa baixa
break # Sai do loop para gravar dados
}
tput cup 11 38 # Posiciona para ler Artista
[ “$oArtista” ] && echo -n “($oArtista) ” # Artista anter. é default
read Artista
[ “$Artista” ] && oArtista=”$Artista”
Reg=”$Reg$oArtista~$Musica:” # Montando registro
tput cup 9 38; tput el # Apaga Musica da tela
tput cup 11 38; tput el # Apaga Artista da tela
done
echo “$Reg” >> musicas # Grava registro no fim do arquivo
sort musicas -o musicas # Classifica o arquivo
done

Agora o programa deu uma boa encolhida e as chamadas de função foram trocadas por arquivos externos chamados pergunta.func e mandamsg.func, que assim podem ser chamados por qualquer outro programa, desta forma reutilizando o seu código.

Por motivos meramente didáticos as execuções de pergunta.func e mandamsg.func estão sendo comandadas por source e por . (ponto) indiscriminadamente, embora prefira o source por ser mais visível desta forma dando maior legibilidade ao código e facilitando sua posterior manutenção.

Veja agora como ficaram estes dois arquivos:

$ cat pergunta.func
# A função recebe 3 parâmetros na seguinte ordem:
# $1 – Mensagem a ser dada na tela
# $2 – Valor a ser aceito com resposta default
# $3 – O outro valor aceito
# Supondo que $1=Aceita?, $2=s e $3=n, a linha
# abaixo colocaria em Msg o valor “Aceita? (S/n)”
Msg=”$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1))
read -n1 SN
[ ! $SN ] && SN=$2 # Se vazia coloca default em SN
echo $SN | tr A-Z a-z # A saída de SN será em minúscula
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela

$ cat mandamsg.func
# A função recebe somente um parâmetro
# com a mensagem que se deseja exibir,
# para não obrigar ao programador passar
# a msq entre aspas, usaremos $* (todos
# os parâmetro, lembra?) e não $1.
Msg=”$*”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
read -n1
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela

Em ambos os arquivos, fiz somente duas mudanças que veremos nas observações a seguir, porém tenho mais três a fazer:

  1. As variáveis não estão sendo mais declaradas como local, porque está é uma diretiva que só pode ser usada no corpo de funções e portanto estas variáveis permanecem no ambiente do Shell, poluindo-o;
  2. O comando return não está mais presente mas poderia estar sem alterar em nada a lógica, uma vez que ele só serviria para indicar um eventual erro via um código de retorno previamente estabelecido (por exemplo return 1, return 2, …), sendo que o return e return 0 são idênticos e significam rotina executada sem erros;
  3. O comando que estamos acostumados a usar para gerar código de retorno é o exit, mas a saída de uma rotina externa não pode ser feita desta forma, porque por estar sendo executada no mesmo Shell que o script chamador, o exit simplesmente encerraria este Shell, terminando a execução de todo o script;
  4. De onde surgiu a variável LinhaMesg? Ela veio do musinc7, porque ela havia sido declarada antes da chamada das rotinas (nunca esquecendo que o Shell que está interpretando o script e estas rotinas é o mesmo);
  5. Se você decidir usar rotinas externas, não se avexe, abunde os comentários (principalmente sobre a passagem dos parâmetros) para facilitar a manutenção e seu uso por outros programas no futuro.

– Bem, agora você já tem mais um monte de novidade para melhorar os scripts que fizemos você se lembra do programa listartista no qual você passava o nome de um artista como parâmetro e ele devolvia as suas músicas? Ele era assim:

$ cat listartista
#!/bin/bash
# Dado um artista, mostra as suas musicas
# versao 2

if [ $# -eq 0 ]
then
echo Voce deveria ter passado pelo menos um parametro
exit 1
fi

IFS=”
:”
for ArtMus in $(cut -f2 -d^ musicas)
do
echo “$ArtMus” | grep -i “^$*~” > /dev/null && echo $ArtMus | cut -f2 -d~
done

– Claro que me lembro!…

– Então para firmar os conceitos que te passei, faça ele com a tela formatada, em loop, de forma que ele só termine quando receber um <ENTER> puro no nome do artista. Ahhh! Quando a listagem atingir a antepenúltima linha da tela, o programa deverá dar uma parada para que o operador possa lê-las, isto é, suponha que a tela tenha 25 linhas. A cada 22 músicas listadas (quantidade de linhas menos 3) o programa aguardará que o operador tecle algo para então prosseguir. Eventuais mensagens de erro devem ser passadas usando a rotina mandamsg.func que acabamos de desenvolver.

– Chico, manda mais dois, o meu é com pouca pressão…

Vou aproveitar também para mandar o meu jabá: diga para os amigos que quem estiver afim de fazer um curso porreta de programação em Shell que mande um e-mail para a nossa gerencia de treinamento para informar-se.

Qualquer dúvida ou falta de companhia para um chope ou até para falar mal dos políticos é só mandar um e-mail para mim.

Valeu!

Read Full Post »