Antes de explorar os shell scripts, o que faremos em aulas posteriores, nesta aula vamos simplesmente usar as estruturas de controle da shell diretamente na linha de comando, para efetuar de forma simples e fácil operações que, de outra forma, seriam laboriosas e tediosas. Por simplicidade de formulação, em sua forma atual estas tarefas devem ser realizadas em uma sessão interativa no sistema socrates.
Existem dois aspectos desta atividade, sendo um deles simplesmente conhecer as estruturas de controle, as estruturas de dados e os comandos básicos que estão disponíveis. O outro aspecto é a atividade de programação propriamente dita: a análise dos problemas que se tem de resolver seguida da invenção de algoritmos contendo estruturas e sistemas de operações que possam resolvê-los e, finalmente, a escrita dos correspondentes programas.
Nas tarefas abaixo tentaremos tocar em ambos estes aspectos, mas a habilidade de programação propriamente dita não é algo que possa ser desenvolvida em uma única aula. Trata-se de um treinamento que deve ser desenvolvido devagar e a longo prazo. Todo usuário de um sistema moderno, aberto e poderoso como o Linux deve se habituar a pensar pelo menos um pouco como um programador. Comecemos o quanto antes!
Estaremos examinando aqui a linguagem de programação da shell que usamos como shell default do sistema, a tcsh. Tudo que vamos fazer aqui também pode ser feito na bash através de operações semelhantes, mas com detalhes de sintaxe um pouco diferentes. Você deve abrir uma sessão no socrates e tentar executar nesta sua sessão todos os exemplos e alternativas que são descritos abaixo.
grep $user /etc/passwd | cut -d : -f 5
onde estamos usando o seu próprio username como exemplo, que é obtido como o valor da variável user. Trata-se de um processo em duas etapas, primeiro obtemos a linha correspondente ao usuário $user na base de dados /etc/passwd, com
grep $user /etc/passwd
e, em seguida, cortamos da linha resultante a informação que queremos, com o comando cut, usando o caracter ``:'' como separador de campos e selecionando o quinto campo. Tudo o que estamos fazendo aqui é usar os comandos grep e cut do sistema, além da pipeline, ou seja, a capacidade da shell de concatenar vários programas através de seus inputs (stdin) e outputs (stdout).
Nas tarefas e exemplos desta aula que envolvem pipelines de comandos você deve construir a estrutura completa aos poucos, experimentando cada parte dela na linha de comando e acrescentando uma nova operação de cada vez. Neste primeiro exemplo temos apenas dois passos neste processo,
grep $user /etc/passwd
e
grep $user /etc/passwd | cut -d : -f 5
Sempre que você for criar uma pipeline de comandos para realizar uma determinada tarefa você deve proceder desta forma, trata-se de um tipo de programação cujo desenvolvimento envolve extensivamente operações de tentativa e erro.
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd | more
O alvo de procura do comando grep é, neste caso, um pouco mais complexo, utilizando uma das estruturas da classe de estruturas que denominamos de ``expressões regulares'' (``regular expressions'' ou, abreviadamente, regexps)1. A expressão [0-9] significa qualquer dígito de 0 a 9. Assim, esta regexp representa qualquer número de 30000 a 39999 circundado de dois caracteres ``:'', o que serve para selecionar os números uid dos alunos. A partir desta lista, basta cortar a informação solicitada, usando o comando cut como antes:
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd \ | cut -d : -f 5 | more
Neste último exemplo, para evitar linhas excessivamente longas, estamos
usando o caracter \
para quebrar a linha em duas, sem que o
conjunto das duas linhas deixe de ser interpretado como uma única linha
pela shell, do ponto de vista lógico. De fato, o caracter \
tem,
dentro da shell, o significado de um ``quote'', significando que o
próximo caracter não deve ser interpretado pela shell da forma usual.
Neste caso é o caracter ^J, que marca o final da linha nos
sistemas da família Unix, que deixa de ser interpretado, tornando-se
equivalente a um espaço em branco para a sintaxe da shell, de forma que
as duas linhas mostradas passam a valer como uma única linha.
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd \ | tail +2 | cut -d : -f 5 | more
Observe como, ao construir aos poucos uma pipeline, podemos acrescentar novos comandos em qualquer ponto dela, não apenas no final. O comando tail serve para mostrar a parte final de um arquivo ou stream de dados, assim como o seu companheiro head serve para mostrar a parte inicial. Para saber mais sobre estes comandos, dê uma olhada em suas páginas de manual.
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd \ | tail +2 | cut -d : -f 5 \ | grep -i a | wc -l
onde a opção -i do comando grep o instrui a ignorar a diferença entre letras maiúsculas e minúsculas e a opção -l do comando wc o instrui a contar linhas. Experimente fazer este exemplo um passo de cada vez, acrescentando cada comando individualmente de forma a poder ver o que acontece nos passos intermediários. Conte quantos alunos do socrates têm cada uma das vogais ``a'', ``e'', ``i'', ``o'' e ``u'' em seus nomes. Determine também o número total de alunos que já são usuários do sistema.
ruptime | grep pop
Finalmente, tente filtrar apenas os terminais da sala usando
ruptime | grep 'pop[0-9][0-9]' | tail +2
Construa esta última versão acrescentando uma parte de cada vez.
cd ; ls
que nos levará para casa e mostrará o nosso diretório de home. Se queremos realizar alguma operação para a qual seja necessário ou conveniente estar momentaneamente em algum outro diretório, voltando imediatamente para onde estamos agora, podemos usar uma sequência de comandos como, por exemplo, a que segue.
cd /usr/local ; echo $cwd ; /bin/pwd ; cd -
Outro exemplo seria a sequência de comandos necessários para imprimir um documento doc.tex escrito em LATEX em uma impressora PS. Copie algum arquivo curto de LATEX já existente com o nome doc.tex e execute o exemplo que segue. O comando de impressão vai falhar se não houver uma impressora default funcionando em seu sistema, neste caso troque-o por gv doc.ps e tente de novo.
latex doc.tex ; dvips doc.dvi ; lpr doc.ps
Sequências de comandos como estas são muito úteis em combinação com o mecanismo de ``history'' da shell, quando é necessário executar a combinação repetidas vezes ao longo de uma sessão. Uma linha de comandos como esta é registrada no sistemas de ``history'' da shell e pode ser recuperada completa através das teclas de cursor, ao contrário do que acontece com a mesma sequência de comandos emitidos cada um numa linha.
Observe que estas sequências de comandos podem se parecer um pouco com pipelines, mas trata-se na realidade de uma estrutura completamente diferente. Numa pipeline os programas envolvidos são executados simultaneamente, com suas entradas e saídas conectadas umas às outras. Numa linha de comandos como as que exemplificamos acima os programas são executados um de cada vez em sequência, com os seus mecanismos de entrada e saída funcionando da forma usual. As duas estruturas apenas se tornam aproximadamente equivalentes quando cada um dos programas de uma sequência escreve todo o seu output para um arquivo que será lido pelo próximo programa, como é o caso do exemplo com o documento em LATEX.
if ( "$user" == "<username>" ) echo "username OK"
Neste caso é executado o comando echo caso a cláusula lógica entre parêntesis seja verdadeira. A estrutura do comando if pode ser estendida para permitir a execução de mais de um comando quando a cláusula lógica é verdadeira, mas isto só é útil em programas escritos em scripts de shell, de forma que deixaremos estas extensões para as aulas sobre scripts. Execute uma linha como esta, colocando explicitamente o seu username onde indicado, e veja o que acontece. Troque o seu username por outra palavra e repita o comando.
Neste caso a cláusula lógica faz uso do teste de comparação ==, de forma que ela resulta verdadeira se as duas strings de caracteres que a circundam forem iguais. Existem outros testes de comparação, como o teste !=, que resulta verdadeiro se as duas strings forem diferentes, ou os testes < (menor) e > (maior), que são úteis para comparar strings que representem números inteiros. Troque o operador == pelo operador != no exemplo acima e volte a executá-lo. Para se informar sobre o conjunto completo de operadores de comparação, olhe o manual da tcsh, executando man tcsh. Trata-se de um longo manual, que você provavelmente usará muitas vezes.
Retire todas as aspas e execute de novo o exemplo do ítem anterior. Em
seguida execute set teste = ""
, troque $user por $teste dentro da cláusula lógica do if, omitindo as aspas, e
tente executar o exemplo mais uma vez para ver o que acontece.
set teste = "A B"
e em seguida execute mais uma vez o comando if omitindo as aspas, para ver o que acontece. Depois tente executar o
mesmo comando com todas as aspas como no primeiro caso.
if ( -f /etc/passwd ) echo "arquivo existe"
Em seguida mude o nome do arquivo para algum outro que não exista e repita o comando. Para verificar que um determinado diretório existe podemos usar o operador -d, como no exemplo que segue.
if ( -d /etc ) echo "diretorio existe"
Depois de executar este exemplo troque /etc por um diretório que não exista ou por um arquivo que não seja um diretório e execute novamente o comando. Para se informar sobre o conjunto completo de operadores deste tipo, consulte as páginas de manual da tcsh.
___> echo $?test ___> set test = 0 ___> echo $?test ___> echo $test ___> @ test = $test + 10 ___> echo $test ___> @ test = $test * 2 ___> echo $test ___> @ test = $test / 3 ___> echo $test
Neste exemplo usamos o operador $?, que retorna não o valor da variável e sim 0 caso a variável não esteja definida e 1 se ela estiver definida. Desta forma podemos verificar se uma determinada variável está definida antes de usá-la, evitando erros que podem fazer com que o processamento de nossos comandos seja interrompido pela shell. Observe o truncamento na divisão inteira, que acontece da forma que é usual na aritmética inteira de computadores. No exemplo acima poderíamos definir e inicializar a variável tanto com o comando set test = 0 quanto com @ test = 0, com efeitos idênticos.
Note que os valores das variáveis da shell são sempre strings de caracteres. A única diferença é que, no âmbito do comando @, a shell interpreta e manipula estas strings de acordo com a aritmética inteira dos números que eles representam. Não se trata, assim, da verdadeira aritmética inteira no computador e estas não são, portanto, operações particularmente rápidas. Não é uma boa idéia usar este tipo de operação para fazer cálculos numéricos maciços com o computador, trata-se de uma estrutura que, como é típico da shell, enfatiza praticidade e não eficiência de execução.
___> set count = 0 ___> while ( $count < 10 ) while? @ count = $count + 1 while? echo $count while? end
Verifique bem e corrija cada linha que digitar, pois não será possível voltar para ela para corrigir erros depois de digitar [Enter]. Se você acidentalmente colocar um loop infinito para rodar, basta interrompê-lo com ^C.
___> set count = 0 ___> while ( 1 ) while? @ count = $count + 1 while? if ( $count > 10 ) break while? echo $count while? end
Observe que a cláusula lógica (1) é sempre verdadeira, enquanto (0) é sempre falsa. O uso da cláusula (1) faz com que o loop deste exemplo seja um loop infinito, de forma que o controle possa ser feito pelo comando if.
___> while ( 1 ) while? clear while? w while? sleep 5 while? end
Este loop rodará uma vez a cada 5 segundos, que é o tempo de espera determinado pelo comando sleep. Você pode terminar o loop com ^C. Caso você venha a ter a necessidade de monitorar de forma contínua alguma coisa, pode escrever um pequeno programa como este e deixar ele rodando dentro de um terminal xterm em algum canto de sua sessão X11.
___> foreach dir ( /bin /etc /lib ) foreach? set num = `\ls -1 $dir | wc -l` foreach? echo O diretorio $dir contem $num arquivos foreach? end
A estrutura de aquisição dos comandos para o caso do foreach é parecida com aquela para o caso do while, mudando apenas o prompt especial que é colocado pela shell. Neste exemplo a variável dir é automaticamente definida e o seu valor é trocado por cada uma das strings da lista que aparece entre parêntesis. Em cada um destes casos o conjunto de comandos é executado. Observe o uso de aspas reversas, que são substituídas pela string que resulta da execução do comando ou pipeline de comandos que elas contêm, como já vimos anteriormente.
http://latt.if.usp.br/fma215/materiais/
nos links relativos a esta aula. Trata-se de uma pequena animação, como é relatado nos créditos que se encontram no arquivo README. Use o comando file para verificar que trata-se de 40 arquivos em formato JPEG, cada um com um dos quadros da animação, cujos nomes são números, sem a habitual extensão .jpg. Em alguns sistemas (não no Linux, por falar nisto, de forma que tudo isto não passa de um exercício acadêmico para nós) é importante que os nomes dos arquivos tenham a extensão apropriada para que os aplicativos os reconheçam, de forma que vamos escrever um pequeno programa para mudar o nome de todos estes arquivos. Isto já é uma coisa ruim de se fazer, um por um, para 40 arquivos, imagine a situação se você estivesse trabalhando numa animação com várias centenas de quadros.
set files = `\ls -1 0*`
O uso do comando ls na forma \ls
evita que seja usado o
alias que existe para ele (para verificar, execute alias ls), em
vez disso é executado o comando diretamente, sem opções. Desta forma
evitamos que sejam incluídos na lista de strings os caracteres de
controle associados à colorização do output do ls, uma opção que
usamos por default em nossos sistemas. Estes caracteres de controle
poderiam interferir com a execução de nosso programa.
Para verificar que a variável files foi definida como uma variável vetorial, utilize o operador $#, que retorna o número de elementos existentes na variável, executando echo $#files. Em seguida examine o conteúdo da variável executando echo $files. Cada um dos elementos pode ser examinado separadamente através da execução de echo $files[1], echo $files[2], etc. Temos agora a lista completa associada a uma variável. Se tivermos várias centenas de elementos, isto é obviamente bem mais prático do que digitar todos os nomes individualmente na linha de comando.
___> foreach file ( $files ) foreach? echo -n "$file " foreach? mv $file $file.jpg foreach? end
Observe que incluímos uma reportagem de status dentro do loop, usando o comando echo para escrever os nomes dos arquivos conforme o programa vai operando sobre eles. O comando echo -n escreve para stdout a string que aparece em seu argumento sem colocar um caracter de final de linha depois da string. Com o uso das aspas incluímos no final da string um espaço em branco, de forma a delimitar claramente os objetos que estão sendo escritos. Depois de executar este pequeno programa dentro do diretório anim, verifique o resultado com o uso do comando ls.
Tente agora ``tocar'' a animação em seu computador. Existem duas formas
de se fazer isto, com o comando animate e com o comando xanim. A primeira forma funciona um pouco melhor mas requer mais memória
do computador, já a segunda é um pouco mais limitada mas requer menos
recursos de memória do sistema. Tente primeiro executar, dentro do seu
diretório anim, o comando animate *.jpg. Tenha paciência,
pode demorar um pouco para o programa carregar a animação completa. Se
isto falhar ou ficar muito lento, tente executar xanim *.jpg. Se
nenhum dos dois funcionar satisfatoriamente, tente mudar para uma máquina
que tenha mais memória RAM. Todas as máquinas da sala Pró-Aluno têm pelo
menos MB de memória RAM disponível, o que deve ser suficiente.
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd
Podemos agora cortar os usernames e os nomes completos usando
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd \ | cut -d : -f 1,5 | less
Finalmente, podemos usar o sed para trocar o separador : por caracteres [Tab] e escreva o resultado em um arquivo, usando
grep ':3[0-9][0-9][0-9][0-9]:' /etc/passwd \ | cut -d : -f 1,5 \ | sed -e 's|:| |g' >! mypasswd.txt
onde o espaço em branco dentro do segundo par de barras verticais do argumento da opção -e do sed consiste de dois tabs seguidos. produzindo assim uma tabela de usernames e nomes. Para digitar o caracter [Tab] na linha de comando da shell, digite primeiro o caracter de ``quote'' da shell, que é [Ctrl]-V, depois digite [Tab].