- Overvisão
- Texto vs. O que quer que seja
- Duplas-cotações em torno de argumentos
- Lista de Espécies Exclusivas
- Solução
- Porquê Gravar Comandos no Histórico Antes de Executá-los?
- Solução
- Pipeline da Nelle: Criando um Script
- Variáveis em Shell Scripts
- Solução
- Localizar o ficheiro mais longo com uma dada extensão
- Solução
- Compreensão de Leitura doScript
- Soluções
- Debugging Scripts
- Solução
- Key Points
Overvisão
Ensino: 30 min
Exercícios: 15 minPerguntas
Como posso salvar e reutilizar comandos?
Objectivos
Escrever um script shell que executa um comando ou série de comandos para um conjunto fixo de ficheiros.
Executar um script shell a partir da linha de comandos.
Escrever um script shell que opera num conjunto de ficheiros definidos pelo utilizador na linha de comandos.
Criar pipelines que incluam scripts shell que você, e outros, tenham escrito.
Estamos finalmente prontos para ver o que torna a shell num ambiente de programação tão poderoso.Nós vamos pegar os comandos que repetimos frequentemente e salvá-los em filesso que podemos executar todas essas operações novamente mais tarde digitando um único comando. Por razões históricas,um monte de comandos salvos em um arquivo é normalmente chamado de shell script,mas não se engane:estes são na verdade pequenos programas.
Vamos começar voltando a molecules/
e criando um novo ficheiro, middle.sh
que irá dar origem ao nosso script shell:
$ cd molecules$ nano middle.sh
O comando nano middle.sh
abre o ficheiro middle.sh
dentro do editor de texto ‘nano'(que corre dentro da shell).Se o arquivo não existir, ele será criado. Podemos usar o editor de texto para editar diretamente o arquivo – vamos simplesmente inserir a seguinte linha:
head -n 15 octane.pdb | tail -n 5
Esta é uma variação no pipe que construímos anteriormente:ele seleciona as linhas 11-15 do arquivo octane.pdb
.Lembre-se, ainda não estamos executando-o como um comando:estamos colocando os comandos em um arquivo.
Então salvamos o arquivo (Ctrl-O
em nano), e saímos do editor de texto (Ctrl-X
em nano).Verifique se o diretório molecules
agora contém um arquivo chamado middle.sh
.
Após termos guardado o ficheiro,podemos pedir à shell para executar os comandos que contém.A nossa shell chama-se bash
, por isso corremos o seguinte comando:
$ bash middle.sh
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
Seguramente,a saída do nosso script é exactamente o que obteríamos se corrêssemos esse pipeline directamente.
>
Texto vs. O que quer que seja
Normalmente chamamos programas como o Microsoft Word ou LibreOffice Writer de “texteditors”, mas precisamos ser um pouco mais cuidadosos quando se trata de toprogramação. Por padrão, o Microsoft Word usa arquivos
.docx
para armazenar não apenas texto, mas também formatar informações sobre fontes, cabeçalhos, e em breve. Esta informação extra não é armazenada como caracteres, e não significa nada para ferramentas comohead
: eles esperam que os arquivos de entrada não contenham nada além das letras, dígitos, e pontuação em um teclado padrão do computador. Ao editar programas, portanto, você deve ou usar um editor de texto simples, ou ter cuidado para salvar arquivos como texto simples.
E se quisermos selecionar linhas de um arquivo arbitrário? Poderíamos editar middle.sh
cada vez para mudar o nome do arquivo, mas isso provavelmente levaria mais tempo do que digitar o comando novamente na shell e executá-lo com um novo nome de arquivo.Em vez disso, vamos editar middle.sh
e torná-lo mais versátil:
$ nano middle.sh
Agora, dentro de “nano”, substitua o texto octane.pdb
pela variável especial chamada :
head -n 15 "" | tail -n 5
Inside a shell script, significa ‘o primeiro nome de ficheiro (ou outro argumento) na linha de comandos’.Agora podemos executar nosso script assim:
$ bash middle.sh octane.pdb
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
ou em um arquivo diferente como este:
$ bash middle.sh pentane.pdb
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00
Duplas-cotações em torno de argumentos
Pela mesma razão que colocamos a variável de laço dentro de aspas duplas, caso o nome do arquivo contenha algum espaço, rodeamos
com aspas duplas.
Currentemente, precisamos editar middle.sh
cada vez que queremos ajustar o intervalo de linhas que é retornado. Vamos corrigir isso configurando nosso script para, ao invés disso, usar três argumentos de linha de comando. Após o primeiro argumento de linha de comando (), cada argumento adicional que fornecemos será acessível através das variáveis especiais
,
,
, que se referem ao primeiro, segundo e terceiro argumentos de linha de comando, respectivamente.
Sabendo isso, podemos usar argumentos adicionais para definir o intervalo de linhas a serem passadas para head
e tail
respectivamente:
$ nano middle.sh
head -n "" "" | tail -n ""
Agora podemos executar:
$ bash middle.sh pentane.pdb 15 5
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00
>
Alterando os argumentos para o nosso comando, podemos alterar o comportamento do nosso script:
>
$ bash middle.sh pentane.pdb 20 5
>
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00TER 18 1
Isto funciona, mas pode levar a próxima pessoa que ler middle.sh
um momento para descobrir o que faz.Podemos melhorar nosso script adicionando alguns comentários no topo:
$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""
Um comentário começa com um caractere #
e corre até o fim da linha. O computador ignora os comentários, mas eles são inestimáveis para ajudar as pessoas (incluindo seu eu futuro) a entender e usar scripts.A única ressalva é que cada vez que você modificar o script,você deve verificar se o comentário ainda está correto:uma explicação que envia o leitor na direção errada é pior que nenhuma.
E se nós quisermos processar muitos arquivos em um único pipeline?Por exemplo, se quisermos ordenar os nossos .pdb
ficheiros por comprimento, escreveríamos:
$ wc -l *.pdb | sort -n
porque wc -l
lista o número de linhas nos ficheiros(lembre-se que wc
significa ‘contagem de palavras’, adicionando a opção -l
significa ‘contagem de linhas’ em vez disso)e sort -n
ordena as coisas numericamente.Poderíamos colocar isto num ficheiro, mas depois só ordenaria uma lista de .pdb
ficheiros no directório actual.Se queremos ser capazes de obter uma lista ordenada de outros tipos de ficheiros, precisamos de uma forma de colocar todos esses nomes no script. Não podemos usar ,
, e assim por diante porque não sabemos quantos ficheiros existem. Em vez disso, usamos a variável especial
$@
,que significa,’Todos os argumentos de linha de comandos para o script shell’.Também devemos colocar $@
dentro de aspas duplas para lidar com o caso de argumentos contendo espaços("$@"
é sintaxe especial e é equivalente a ""
""
…).
Aqui está um exemplo:
$ nano sorted.sh
# Sort files by their length.# Usage: bash sorted.sh one_or_more_filenameswc -l "$@" | sort -n
>
$ bash sorted.sh *.pdb ../creatures/*.dat
9 methane.pdb12 ethane.pdb15 propane.pdb20 cubane.pdb21 pentane.pdb30 octane.pdb163 ../creatures/basilisk.dat163 ../creatures/minotaur.dat163 ../creatures/unicorn.dat596 total
Lista de Espécies Exclusivas
Lista tem várias centenas de ficheiros de dados, cada um deles formatado desta forma:
2013-11-05,deer,52013-11-05,rabbit,222013-11-05,raccoon,72013-11-06,rabbit,192013-11-06,deer,22013-11-06,fox,12013-11-07,rabbit,182013-11-07,bear,1
Um exemplo deste tipo de ficheiro é dado em data-shell/data/animal-counts/animals.txt
.
Podemos usar o comando cut -d , -f 2 animals.txt | sort | uniq
para produzir a espécie única em animals.txt
. A fim de evitar ter que digitar esta série de comandos toda vez, um cientista pode escolher escrever um script shell em vez disso.
Escrever um script shell chamado species.sh
que tira qualquer número de nomes offilenames como argumentos de linha de comando, e usa uma variação do comando acima para imprimir uma lista das espécies únicas que aparecem em cada um desses arquivos separadamente.
Solução
# Script to find unique species in csv files where species is the second data field# This script accepts any number of file names as command line arguments# Loop over all filesfor file in $@doecho "Unique species in $file:"# Extract species namescut -d , -f 2 $file | sort | uniqdone
Só temos que executar uma série de comandos que fizeram algo útil – por exemplo, que criou um gráfico que gostaríamos de usar em um papel.Em vez de os escrevermos novamente,podemos fazer isto:
$ history | tail -n 5 > redo-figure-3.sh
O ficheiro redo-figure-3.sh
contém agora:
297 bash goostats NENE01729B.txt stats-NENE01729B.txt298 bash goodiff stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png301 history | tail -n 5 > redo-figure-3.sh
Após um momento de trabalho em um editor para remover os números de série nos comandos, e para remover a linha final onde chamamos o comando history
, temos um registro completamente preciso de como criamos essa figura.
Porquê Gravar Comandos no Histórico Antes de Executá-los?
Se você executar o comando:
$ history | tail -n 5 > recent.sh
O último comando no arquivo é o próprio comando
history
, ou sejaa shell adicionouhistory
ao log do comando antes de o executar. Na verdade, a shell adiciona sempre comandos ao log antes de os executar. Por que você acha que ele faz isso?Solução
Se um comando faz algo travar ou pendurar, pode ser útil saber qual foi esse comando, a fim de investigar o problema. Se o comando só fosse gravado depois de executá-lo, não teríamos um registro do último comando executado no caso de um travamento.
Na prática, a maioria das pessoas desenvolve scripts shell executando comandos no prompt shell algumas vezes para ter certeza de que estão fazendo a coisa certa,depois salvando-os em um arquivo para reutilização.Este estilo de trabalho permite que as pessoas reciclem o que descobrirem sobre seus dados e seu fluxo de trabalho com uma chamada para history
e um pouco de edição para limpar a saída e salvá-la como um script shell.
Pipeline da Nelle: Criando um Script
O supervisor da Nelle insistiu que todas as suas análises devem ser reprodutíveis. A maneira mais fácil de capturar todos os passos é através de um script.
Primeiro retornamos ao diretório de dados da Nelle:
$ cd ../north-pacific-gyre/2012-07-03/
>
Ela executa o editor e escreve o seguinte:
# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone
Ela salva isso em um arquivo chamado do-stats.sh
para que ela possa agora refazer a primeira etapa de sua análise digitando:
$ bash do-stats.sh NENE*.txt
Ela também pode fazer isto:
$ bash do-stats.sh NENE*.txt | wc -l
>
para que a saída seja apenas o número de ficheiros processados, mais do que os nomes dos ficheiros que foram processados.
Uma coisa a notar sobre o script da Nelle é que o thatit permite que a pessoa que o executa decida quais os arquivos a serem processados.Ela poderia tê-lo escrito como:
# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone
A vantagem é que isso sempre seleciona os arquivos certos:ela não precisa se lembrar de excluir os arquivos ‘Z’. A desvantagem é que ela sempre seleciona apenas esses arquivos – ela não pode executá-lo em todos os arquivos (incluindo os arquivos ‘Z’),ou nos arquivos ‘G’ ou ‘H’ que seus colegas na Antártica estão produzindo,sem editar o script.Se ela quisesse ser mais aventureira, ela poderia modificar seu script para verificar argumentos de linha de comando, e usar NENE*.txt
se nenhum fosse fornecido.
Variáveis em Shell Scripts
No diretório
molecules
, imagine que você tenha um script shell chamadoscript.sh
contendo os seguintes comandos:head -n tail -n
Enquanto estiver no directório
molecules
, digite o seguinte comando:bash script.sh '*.pdb' 1 1
Qual das seguintes saídas esperaria ver?
- Todas as linhas entre a primeira e a última linha de cada ficheiro terminando em
.pdb
no directóriomolecules
- A primeira e a última linha de cada ficheiro terminando em
.pdb
no directóriomolecules
diretório- A primeira e a última linha de cada arquivo no diretório
molecules
> diretório- Um erro por causa das aspas em torno de
*.pdb
Solução
A resposta correta é 2.
As variáveis especiais $1, $2 e $3 representam os argumentos de linha de comando dados ao thescript, de modo que os comandos são:
$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb
A shell não expande
'*.pdb'
porque está enclausurada por aspas.Como tal, o primeiro argumento para o script é'*.pdb'
que é expandido dentro do thescript porhead
etail
.
Localizar o ficheiro mais longo com uma dada extensão
Escrever um script shell chamado
longest.sh
que toma como argumentos o nome do aditivo e uma extensão de nome de ficheiro, e imprime o nome do ficheiro com o maior número de linhas nesse directório com essa extensão. Por exemplo:$ bash longest.sh /tmp/data pdb
imprimiria o nome do ficheiro
.pdb
em/tmp/data
que tem o maior número de linhas.Solução
# Shell script which takes two arguments:# 1. a directory name# 2. a file extension# and prints the name of the file in that directory# with the most lines which matches the file extension.wc -l /*. | sort -n | tail -n 2 | head -n 1
A primeira parte do pipeline,
wc -l /*. | sort -n
, conta as linhas em cada ficheiro e ordena-as numericamente (a maior última). Quando há mais de um arquivo,wc
também produz uma linha de resumo final, dando o número total de linhas em todos os arquivos. Usamostail-n 2 | head -n 1
para jogar fora esta última linha.Com
wc -l /*. | sort -n | tail -n 1
veremos a linha de resumo final: podemos construir nosso pipeline em pedaços para ter certeza de que entendemos a saída.
Compreensão de Leitura doScript
Para esta pergunta, considere o diretório
data-shell/molecules
mais uma vez.Isto contém um número de arquivos.pdb
além de quaisquer outros arquivos que você possa ter criado. Explique o que cada um dos três scripts a seguir faria quando executado comobash script1.sh *.pdb
,bash script2.sh *.pdb
, ebash script3.sh *.pdb
, respectivamente.# Script 1echo *.*
# Script 2for filename in do cat $filenamedone
># Script 3echo [email protected]
Soluções
Em cada caso, a shell expande o wildcard em
*.pdb
antes de passar a lista de resultados dos nomes dos ficheiros como argumentos para o script.Script 1 imprimiria uma lista de todos os arquivos contendo um ponto em seu nome. Os argumentos passados ao script não são realmente usados em nenhum lugar do script.
Script 2 imprimiria o conteúdo dos 3 primeiros arquivos com uma extensão de arquivo
.pdb
.,
, e
referem-se ao primeiro, segundo e terceiro argumentos respectivamente.
Script 3 imprimiria todos os argumentos do script (ou seja, todos os ficheiros
.pdb
),seguido por.pdb
.$@
refere-se a todos os argumentos dados a um script shell.cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Debugging Scripts
Suponha que você salvou o seguinte script em um arquivo chamado
do-errors.sh
no diretório da Nellenorth-pacific-gyre/2012-07-03
:# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone
Quando o executa:
$ bash do-errors.sh NENE*.txt
a saída está em branco.Para descobrir porquê, execute novamente o script usando a opção
-x
opção:bash -x do-errors.sh NENE*.txt
Qual é a saída que lhe mostra? Qual é a linha responsável pelo erro?
Solução
A opção
-x
faz com quebash
corra em modo de depuração. Isto imprime cada comando à medida que é executado, o que o ajudará a localizar os erros. Neste exemplo, podemos ver queecho
não está imprimindo nada. Fizemos um erro de digitação no nome da variável de loop, e a variáveldatfile
não existe, retornando assim uma string vazia.
Key Points
Salve comandos em arquivos (normalmente chamados de shell scripts) para reutilização.
bash
executa os comandos salvos em um arquivo.
$@
refere-se a todos os argumentos de linha de comando de um script shell.
,
, etc, refere-se ao primeiro argumento de linha de comando, o segundo argumento de linha de comando, etc.
Colocar variáveis entre aspas se os valores podem ter espaços nelas.
Disponibilizar os usuários a decidir que arquivos processar é mais flexível e mais consistente com os comandos Unix embutidos.