Shell Scripts

Out 29, 2021

Overvisão

Ensino: 30 min
Exercícios: 15 min
Perguntas

  • 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 como head: 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 adicionou history 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 chamado script.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?

  1. Todas as linhas entre a primeira e a última linha de cada ficheiro terminando em .pdb no directório molecules
  2. A primeira e a última linha de cada ficheiro terminando em .pdb no directório molecules diretório
  3. A primeira e a última linha de cada arquivo no diretóriomolecules> diretório
  4. 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 por head e tail.

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. Usamos tail-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, e bash 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 Nelle north-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 que bash 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 que echo não está imprimindo nada. Fizemos um erro de digitação no nome da variável de loop, e a variável datfile 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.

Deixe uma resposta

O seu endereço de email não será publicado.