Feeds:
Posts
Comentários

Posts Tagged ‘PID’

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 6

Comandos de Loop ou Laço (Continuação)

– Fala cara! E aí, já tá sabendo tudo do comando for? Eu te deixei um exercício para treinar, se não me engano era para contar a quantidade de palavras de um arquivo… Você fez? – Claro! Tô empolgadão com essa linguagem, eu fiz da forma que você pediu, isto é sem usar o comando wc porque senão era mais mole ainda. Olha só como eu fi…

– Êpa! Perai! Você realmente está fissurado na linguagem, mas eu tô sequinho pra tomar um chope. Aê Chico, traz dois por favor. Um sem colarinho!

– Como eu ia dizendo olha a forma que eu fiz. É muito fácil…

$ cat contpal.sh
#!/bin/bash
# Script meramente pedagógico cuja
# função é contar a qtd de palavras
# de um arquivo. Supõe-se que as
# palavras estão separadas entre si
# por espaço, ou .

if [ $# -ne 1 ]
then
echo uso: $0 /caminho/do/arquivo
exit 2
fi
Cont=0
for Palavra in $(cat $1)
do
Cont=$((Cont+1))
done
echo O arquivo $1 tem $Cont palavras.

Ou seja, o programa começa como sempre verificando se a passagem de parâmetros foi correta, em seguida o comando for se incumbe de pegar cada uma das palavras (lembre-se que o $IFS padrão (default) é branco, <TAB> e <ENTER>, que é exatamente o que desejamos para separar as palavras), incrementando a variável $Cont.

Vamos relembrar como é o arquivo ArqDoDOS.txt.

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

Agora vamos testar o programa passando este arquivo como parâmetro:

$ contpal.sh ArqDoDOS.txt
O arquivo ArqDoDOS.txt tem 14 palavras

– Beleza, funcionou legal!

Um Pouco Mais de for e Matemática

Voltando à vaca fria, na última vez que aqui estivemos, terminamos o nosso papo mostrando o loop de for a seguir:

    for ((; i<=9;))
    do
        let i++
        echo -n "$i "
    done

Uma vez que chegamos neste ponto, creio ser bastante interessante citar que o Shell trabalha com o conceito de “Expansão Aritmética” (Arithmetic Expansion), da qual vou falar rapidamente porque na seção Tira Gosto isso está muito bem mastigado.

A expansão aritmética é acionada por uma construção da forma:

$((expressão))

ou

let expressão

No último for citado usei a expansão das duas formas, mas não poderíamos seguir adiante sem saber que a expressão pode ser de uma das listadas a seguir:

Expansão Aritmética
Expressão Resultado
id++ id-- pós-incremento e pós-decremento de variáveis
++id -–id pré-incremento e pré-decremento de variáveis
** exponenciação
* / % multiplicação, divisão, resto da divisão
+ - adição, subtração
<= >= < > comparação
== != igualdade, desigualdade
&& E lógico
|| OU lógico

– Mas você pensa que o papo de loop (ou laço) se encerra no comando for? Ledo engano amigo, vamos a partir de agora ver mais dois.

O comando while

Todos os programadores conhecem este comando, porque ele é comum a todas as linguagens e nelas, o que normalmente ocorre é que um bloco de comandos é executado, enquanto (enquanto em ingles é while) uma determinada condição for verdadeira. Pois bem, isto é o que ocorre nas linguagens caretas! Em programação Shell, o bloco de comandos é executado enquanto um comando for verdadeiro. E é claro, se quiser testar uma condição use o comando while junto com o comando test, exatamente como você aprendeu a fazer no if, lembra?

Então a sintaxe do comando fica assim:

    while comando
    do
        cmd1
        cmd2
        ...
        cmdn
    done

e desta forma o bloco de comandos formado pelas instruções cmd1, cmd2,… e cmdn é executado enquanto a execução da instrução comando for bem sucedida.

Suponha a seguinte cena: tem uma tremenda gata me esperando e eu preso no trabalho sem poder sair porque o meu chefe, que é um pé no saco (aliás chefe-chato é uma redundância, né?:), ainda estava na sua sala, que fica bem na minha passagem para a rua.

Ele começou a ficar com as antenas (provavelmente instaladas na cabeça dele pela esposa) ligadas depois da quinta vez que passei pela sua porta e olhei para ver se já havia ido embora. Então voltei para a minha mesa e fiz, no servidor, um script assim:

$ cat logaute.sh
#!/bin/bash

# Espero que a Xuxa não tenha
# copyright de xefe e xato 🙂

while who | grep xefe
do
sleep 30
done
echo O xato se mandou, não hesite, dê exit e vá a luta

Neste scriptizinho, o comando while testa o pipeline composto pelo who e pelo grep e que será verdadeiro enquanto o grep localizar a palavra xefe na saída do who. Desta forma, o script dormirá por 30 segundos enquanto o chefe estiver logado (Argh!). Assim que ele se desconectar do servidor, o fluxo do script sairá do loop e dará a tão ansiada mensagem de liberdade.

Quando o executei adivinha o que aconteceu?

$ logaute.sh
xefe pts/0 Jan 4 08:46 (10.2.4.144)
xefe pts/0 Jan 4 08:47 (10.2.4.144)

xefe pts/0 Jan 4 08:52 (10.2.4.144)

Isto é a cada 30 segundos seria enviado para a tela a saída do grep, o que não seria legal já que poluiria a tela do meu micro e a mensagem esperada poderia passar desapercebida. Para evitar isso já sabemos que a saída do pipeline tem que ser redirecionada para /dev/null.

$ cat logaute.sh
#!/bin/bash

# Espero que a Xuxa não tenha
# copyright de xefe e xato 🙂

while who | grep xefe > /dev/null
do
sleep 30
done
echo O xato se mandou, não hesite, dê exit e vá a luta

Agora quero montar um script que receba o nome (e eventuais parâmetros) de um programa que será executado em background e que me informe do seu término. Mas, para você entender este exemplo, primeiro tenho de mostar uma nova variável do sistema. Veja estes comandos diretos no prompt:

$ sleep 10&
[1] 16317

$ echo $!
16317
[1]+ Done sleep 10

$ echo $!
16317

Isto é, criei um processo em background para dormir por 10 segundos, somente para mostrar que a variável $! guarda o PID (Process IDentification) do último processo em background, mas repare após a linha do done, que a variável reteve o valor mesmo após o término deste processo.

Bem sabendo isso já fica mais fácil de monitorar qualquer processo em background. Veja só como:

$ cat monbg.sh
#!/bin/bash

# Executa e monitora um
# processo em background

$1 & # Coloca em backgroud
while ps | grep -q $!
do
sleep 5
done
echo Fim do Processo $1

Este script é bastante similar ao anterior, mas tem uns macetes a mais, veja só: ele tem que ser executado em background para não prender o prompt mas o $! será o do programa passado como parâmetro já que ele foi colocado em background após o monbg.sh propriamente dito. Repare também a opção -q (quiet) do grep, ela serve para tranformá-lo num comando mineiro, isto é, para o grep “trabalhar em silêncio”. O mesmo resultado poderia ser obtido se a linha fosse while ps | grep $! > /dev/null, como nos exemplos que vimos até agora.

Pinguim com placa de dica Não esqueça: o Bash disponibiliza a variável $! que possui o PID (Process IDentification) do último processo executado em background.

Vamos melhorar o musinc, que é o nosso programa para incluir registros no arquivo musicas, mas antes preciso te ensinar a pegar um dado da tela, e já vou avisando: só vou dar uma pequena dica do comando read (que é quem pega o dado da tela) que seja o suficiente para resolver este nosso problema. Em uma outra rodada de chope vou te ensinar tudo sobre o assunto, inclusive como formatar tela, mas hoje estamos falando sobre loops.

A sintaxe do comando read que nos interessa por hoje é a seguinte:

$ read -p “prompt de leitura” var

Onde prompt de leitura é o texto que você quer que apareça escrito na tela, e quando o operador teclar o dado, ele irá para a variável var. Por exemplo:

$ read -p “Título do Álbum: ” Tit

Bem, uma vez entendido isso, vamos à especificação do nosso problema: faremos um programa que inicialmente lerá o nome do álbum e em seguida fara um loop de leitura, pegando a música e o artista. Este loop termina quando for informada uma música vazia, isto é, ao ser solicitada a digitação da música, o operador dá um simples <ENTER>. Para facilitar a vida do operador, vamos oferecer como default o mesmo nome do artista da música anterior (já que é normal que o álbum seja todo do mesmo artista) até que ele deseje alterá-lo. Vamos ver como ficou:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 4)
#
clear
read -p “Título do Álbum: ” Tit
[ “$Tit” ] || exit 1 # Fim da execução se título vazio
if grep “^$Tit^” musicas > /dev/null
then
echo Este álbum já está cadastrado
exit 1
fi
Reg=”$Tit^”
Cont=1
oArt=
while true
do
echo Dados da trilha $Cont:
read -p “Música: ” Mus
[ “$Mus” ] || break # Sai se vazio
read -p “Artista: $oArt // ” Art
[ “$Art” ] && oArt=”$Art” # Se vazio Art anterior
Reg=”$Reg$oArt~$Mus:” # Montando registro
Cont=$((Cont + 1))
# A linha anterior tb poderia ser ((Cont++))
done
echo “$Reg” >> musicas
sort musicas -o musicas

Este exemplo, começa com a leitura do título do álbum, que se não for informado, terminará a execução do programa. Em seguida um grep procura no início (^) de cada registro de musicas, o título informado seguido do separador (^) (que está precedido de uma contrabarra () para protegê-lo da interpretação do Shell).

Para ler os nomes dos artistas e as músicas do álbum, foi montado um loop de while simples, cujo único destaque é o fato de estar armazenando o artista da música anterior na variável $oArt que só terá o seu conteúdo alterado, quando algum dados for informado para a variável $Art, isto é, quando não teclou-se um simples <ENTER> para manter o artista anterior.

O que foi visto até agora sobre o while foi muito pouco. Este comando é muito utilizado, principalmente para leitura de arquivos, porém nos falta bagagem para prosseguir. Depois que aprendermos a ler, veremos esta instrução mais a fundo.

Pinguim com placa de dica Leitura de arquivo significa ler um-a-um todos os registros, o que é sempre uma operação lenta. Fique atento para não usar o while quando seu uso for desnecessário. O Shell tem ferramentas como o sed e a família grep que vasculham arquivos de forma otimizada sem ser necessário o uso de comandos de loop para fazê-lo registro a registro (ou até palavra a palavra).

O comando until

O comando until funciona exatamente igual ao while, porém ao contrário. Disse tudo mas não disse nada, né? É o seguinte: ambos testam comandos; ambos possuem a mesma sintaxe e ambos atuam em loop, porém enquanto o while executa o bloco de intruções do loop enquanto um comando for bem sucedido, o until executa o bloco do loop até que o comando seja bem sucedido. Parece pouca coisa mas a diferença é fundamental.

A sintaxe do comando é praticamente a mesma do while. Veja:

    until comando
    do
        cmd1
        cmd2
        ...
        cmdn
    done

E desta forma o bloco de comandos formado pelas instruções cmd1, cmd2,… e cmdn é executado até que a execução da instrução comando seja bem sucedida.

Como eu te disse, o while e until funcionam de forma antagônica e isso é muito fácil de demonstrar: em uma guerra sempre que se inventa uma arma, o inimigo busca uma solução para neutralizá-la. Baseado neste principio belicoso que o meu chefe, desenvolveu, no mesmo servidor que eu executava o logaute.sh um script para controlar o meu horário de chegada.

Um dia deu um problema da rede, ele me pediu para dar uma olhada no micro dele e me deixou sozinho em sua sala. Imediatamente comecei a bisbilhotar seus arquivos – porque guerra é guerra – e veja só o que descobri:

$cat chegada.sh
#!/bin/bash

until who | grep julio
do
sleep 30
done
echo $(date “+ Em %d/%m às %H:%Mh”) >> relapso.log

Olha que safado! O cara estava montando um log com os horários que eu chegava, e ainda por cima chamou o arquivo que me monitorava de relapso.log! O que será que ele quis dizer com isso?

Neste script, o pipeline who | grep julio, será bem sucedido somente quando julio for encontrado no comando who, isto é, quando eu me “logar” no servidor. Até que isso aconteça, o comando sleep, que forma o bloco de instruções do until, porá o programa em espera por 30 segundos. Quando este loop encerrar-se, será dada uma mensagem para o relapso.log (ARGHH!). Supondo que no dia 20/01 eu me loguei às 11:23 horas, a mensagem seria a seguinte:

Em 20/01 às 11:23h

Quando vamos cadastrar músicas, o ideal seria que pudéssemos cadastrar diversos CDs, e na última versão que fizemos do musinc, isso não ocorre, a cada CD que cadastramos o programa termina. Vejamos como melhorá-lo:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 5)
#
Para=
until [ “$Para” ]
do
clear
read -p “Título do Álbum: ” Tit
if [ ! “$Tit” ] # Se titulo vazio…
then
Para=1 # Liguei flag de saída
else
if grep “^$Tit^” musicas > /dev/null
then
echo Este álbum já está cadastrado
exit 1
fi
Reg=”$Tit^”
Cont=1
oArt=
while [ “$Tit” ]
do
echo Dados da trilha $Cont:
read -p “Música: ” Mus
[ “$Mus” ] || break # Sai se vazio
read -p “Artista: $oArt // ” Art
[ “$Art” ] && oArt=”$Art” # Se vazio Art anterior
Reg=”$Reg$oArt~$Mus:” # Montando registro
Cont=$((Cont + 1))
# A linha anterior tb poderia ser ((Cont++))
done
echo “$Reg” >> musicas
sort musicas -o musicas
fi
done

Nesta versão, um loop maior foi adicionado antes da leitura do título, que só terminará quando a variável $Para deixar de ser vazia. Caso o título do álbum não seja informado, a variável $Para receberá valor (no caso coloquei 1 mas poderia ter colocado qualquer coisa. O importante é que não seja vazia) para sair deste loop, terminando desta forma o programa. No resto, o script é idêntico à sua versão anterior.

Atalhos no loop

Nem sempre um ciclo de programa, compreendido entre um do e um done, sai pela porta da frente. Em algumas oportunidades, temos que colocar um comando que aborte de forma controlada este loop. De maneira inversa, algumas vezes desejamos que o fluxo de execução do programa volte antes de chegar ao done. Para isto, temos respectivamente, os comandos break (que já vimos rapidamente nos exemplos do comado while) e continue, que funcionam da seguinte forma:

O que eu não havia dito anteriormente é que nas suas sintaxes genéricas eles aparecem da seguinte forma:

break [qtd loop]

e

continue [qtd loop]

Onde qtd loop representa a quantidade dos loops mais internos sobre os quais os comandos irão atuar. Seu valor default é 1.

Fluxograma Duvido que você nunca tenha deletado um arquivo e logo após deu um tabefe na testa se xingando porque não devia tê-lo removido. Pois é, na décima vez que fiz esta besteira, criei um script para simular uma lixeira, isto é, quando mando remover um (ou vários) arquivo(s), o programa “finge” que removeu-o, mas no duro o que fez foi mandá-lo(s) para o diretório /tmp/LoginName_do_usuario. Chamei este programa de erreeme e no /etc/profile coloquei a seguinte linha:

alias rm=erreeme

O programa era assim:

$ cat erreeme
#/bin/bash
#
# Salvando Copia de Arquivo Antes de Remove-lo
#

if [ $# -eq 0 ] # Tem de ter um ou mais arquivos para remover
then
echo “Erro -> Uso: erreeme arq [arq] … [arq]”
echo ” O uso de metacaracteres e’ permitido. Ex. erreeme arq*”
exit 1
fi

MeuDir=”/tmp/$LOGNAME” # Variavel do sist. Contém o nome do usuário.
if [ ! -d $MeuDir ] # Se não existir o meu diretório sob o /tmp…
then
mkdir $MeuDir # Vou cria-lo
fi

if [ ! -w $MeuDir ] # Se não posso gravar no diretório…
then
echo Impossivel salvar arquivos em $MeuDir. Mude permissao…
exit 2
fi

Erro=0 # Variavel para indicar o cod. de retorno do prg
for Arq # For sem o “in” recebe os parametros passados
do
if [ ! -f $Arq ] # Se este arquivo não existir…
then
echo $Arq nao existe.
Erro=3
continue # Volta para o comando for
fi

DirOrig=`dirname $Arq` # Cmd. dirname informa nome do dir de $Arq
if [ ! -w $DirOrig ] # Verifica permissão de gravacaoo no diretório
then
echo Sem permissao de remover no diretorio de $Arq
Erro=4
continue # Volta para o comando for
fi

if [ “$DirOrig” = “$MeuDir” ] # Se estou “esvaziando a lixeira”…
then
echo $Arq ficara sem copia de seguranca
rm -i $Arq # Pergunta antes de remover
[ -f $Arq ] || echo $Arq removido # Será que o usuario removeu?
continue
fi

cd $DirOrig # Guardo no fim do arquivo o seu diretorio
pwd >> $Arq # original para usa-lo em um script de undelete
mv $Arq $MeuDir # Salvo e removo
echo $Arq removido
done
exit $Erro # Passo eventual numero do erro para o codigo de retorno

Como você pode ver, a maior parte do script é formada por pequenas criticas aos parâmetros informados, mas como o script pode ter recebido diversos arquivos para remover, a cada arquivo que não se encaixa dentro do especificado, há um continue, para que a sequência volte para o loop do for de forma a receber outros arquivos.

Quando você está no Windows (com perdão da má palavra) e tenta remover aquele monte de lixo com nomes esquisitos como HD04TG.TMP, se der erro em um deles, os outros não são removidos, não é? Então, o continue foi usado para evitar que um impropério desses ocorra, isto é, mesmo que dê erro na remoção de um arquivo, o programa continuará removendo os outros que foram passados.

– Eu acho que a esta altura você deve estar curioso para ver o programa que restaura o arquivo removido, não é? Pois então aí vai vai um desafio: faça-o em casa e me traga para discutirmos no nosso próximo encontro aqui no boteco.

– Poxa, mas nesse eu acho que vou dançar, pois não sei nem como começar…

– Cara, este programa é como tudo que se faz em Shell, extremamente fácil, é para ser feito em, no máximo 10 linhas. Não se esqueça que o arquivo está salvo em /tmp/$LOGNAME e que a sua última linha é o diretório em que ele residia antes de ser “removido”. Também não se esqueça de criticar se foi passado o nome do arquivo a ser removido.

– É eu vou tentar, mas sei não…

– Tenha fé irmão, eu tô te falando que é mole! Qualquer dúvida é só me passar um e-mail para julio.neves@gmail.com. Agora chega de papo que eu já estou de goela seca de tanto falar. Me acompanha no próximo chope ou já vai sair correndo para fazer o script que passei?

– Deixa eu pensar um pouco…

– Chico, traz mais um chope enquanto ele pensa!

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 »