Shell Scripts

Ott 29, 2021

Panoramica

Insegnamento: 30 min
Esercizi: 15 min
Domande

  • Come posso salvare e riutilizzare i comandi?

Obiettivi

  • Scrivere uno script di shell che esegue un comando o una serie di comandi per un insieme fisso di file.

  • Scrivere uno script di shell che opera su un insieme di file definiti dall’utente sulla linea di comando.

  • Creare pipeline che includono script di shell scritti da voi e da altri.

Siamo finalmente pronti per vedere cosa rende la shell un ambiente di programmazione così potente.Prenderemo i comandi che ripetiamo frequentemente e li salveremo in un file in modo da poter rieseguire tutte quelle operazioni in seguito digitando un singolo comando. Per ragioni storiche, un mucchio di comandi salvati in un file è solitamente chiamato script di shell, ma non fate errori: questi sono in realtà piccoli programmi.

Iniziamo tornando a molecules/ e creando un nuovo file, middle.sh che diventerà il nostro script di shell:

$ cd molecules$ nano middle.sh

Il comando nano middle.sh apre il file middle.sh nell’editor di testo ‘nano’ (che funziona all’interno della shell).Se il file non esiste, verrà creato. Possiamo usare l’editor di testo per modificare direttamente il file – inseriremo semplicemente la seguente linea:

head -n 15 octane.pdb | tail -n 5

Questa è una variazione della pipe che abbiamo costruito prima: seleziona le linee 11-15 del file octane.pdb.Ricordate, non lo stiamo ancora eseguendo come un comando: stiamo mettendo i comandi in un file.

Poi salviamo il file (Ctrl-O in nano), e usciamo dall’editor di testo (Ctrl-X in nano).Controllate che la directory molecules ora contenga un file chiamato middle.sh.

Una volta salvato il file, possiamo chiedere alla shell di eseguire i comandi che contiene.La nostra shell si chiama bash, quindi eseguiamo il seguente 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

Sicuramente, l’output del nostro script è esattamente quello che avremmo se eseguissimo direttamente quella pipeline.

Text vs. Whatever

Di solito chiamiamo programmi come Microsoft Word o LibreOffice Writer “editor di testo”, ma dobbiamo stare un po’ più attenti quando si tratta di toprogrammazione. Per default, Microsoft Word usa i file .docx per memorizzare non solo il testo, ma anche le informazioni di formattazione sui caratteri, le intestazioni e presto. Queste informazioni extra non sono memorizzate come caratteri, e non significano nulla per strumenti come head: essi si aspettano che i file di input non contengano altro che le lettere, le cifre e la punteggiatura su una tastiera standard del computer. Quando si modificano i programmi, quindi, si deve usare un editor di testo semplice, o fare attenzione a salvare i file come testo semplice.

Che cosa succede se vogliamo selezionare linee da un file arbitrario? Potremmo modificare middle.sh ogni volta per cambiare il nome del file, ma questo probabilmente richiederebbe più tempo che digitare nuovamente il comando nella shell ed eseguirlo con un nuovo nome di file.Invece, modifichiamo middle.sh e rendiamolo più versatile:

$ nano middle.sh

Ora, all’interno di “nano”, sostituite il testo octane.pdb con la variabile speciale chiamata :

head -n 15 "" | tail -n 5

In uno script di shell, significa ‘il primo nome di file (o altro argomento) sulla linea di comando’.Ora possiamo eseguire il nostro script in questo modo:

$ 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

o su un altro file come questo:

$ 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

Citazioni doppie intorno agli argomenti

Per la stessa ragione per cui mettiamo la variabile del ciclo tra virgolette, nel caso in cui il nome del file contenga degli spazi, circondiamo con virgolette doppie.

Al momento, dobbiamo modificare middle.sh ogni volta che vogliamo regolare l’intervallo di linee che viene restituito. Risolviamo questo problema configurando il nostro script in modo che usi invece tre argomenti della linea di comando. Dopo il primo argomento della riga di comando (), ogni argomento aggiuntivo che forniremo sarà accessibile tramite le variabili speciali , , , che si riferiscono rispettivamente al primo, secondo e terzo argomento della riga di comando.

Sapendo questo, possiamo usare ulteriori argomenti per definire la gamma di linee da passare rispettivamente a head e tail:

$ nano middle.sh
head -n "" "" | tail -n ""

Possiamo ora eseguire:

$ 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

Cambiando gli argomenti del nostro comando possiamo cambiare il comportamento del nostro 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

Questo funziona, ma la prossima persona che legge middle.sh potrebbe impiegare un momento per capire cosa fa.Possiamo migliorare il nostro script aggiungendo alcuni commenti all’inizio:

$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""

Un commento inizia con un carattere # e va fino alla fine della linea.Il computer ignora i commenti, ma sono preziosi per aiutare le persone (incluso il tuo futuro io) a capire e usare gli script.L’unica avvertenza è che ogni volta che modifichi lo script, dovresti controllare che il commento sia ancora accurato: una spiegazione che manda il lettore nella direzione sbagliata è peggio che nessuna.

Che succede se vogliamo processare molti file in una singola pipeline?Per esempio, se vogliamo ordinare i nostri file .pdb per lunghezza, dovremmo digitare:

$ wc -l *.pdb | sort -n

perché wc -l elenca il numero di linee nei file (ricordate che wc sta per ‘word count’, aggiungendo l’opzione -l significa invece ‘count lines’) e sort -n ordina le cose numericamente.Potremmo metterlo in un file, ma in tal caso ordinerebbe solo una lista di .pdb file nella directory corrente.Se vogliamo essere in grado di ottenere una lista ordinata di altri tipi di file, abbiamo bisogno di un modo per mettere tutti quei nomi nello script; non possiamo usare , e così via, perché non sappiamo quanti file ci sono; usiamo invece la variabile speciale $@, che significa “tutti gli argomenti della riga di comando dello script di shell”.Dovremmo anche mettere $@ tra doppi apici per gestire il caso di argomenti che contengono spazi ("$@" è una sintassi speciale ed è equivalente a "" "" …).

Ecco un esempio:

$ 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 specie uniche

Leah ha diverse centinaia di file di dati, ognuno dei quali è formattato così:

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

Un esempio di questo tipo di file è dato in data-shell/data/animal-counts/animals.txt.

Possiamo usare il comando cut -d , -f 2 animals.txt | sort | uniq per produrre la specie unica in animals.txt. Per evitare di dover digitare questa serie di comandi ogni volta, uno scienziato può invece scegliere di scrivere uno script di shell.

Scrivere uno script di shell chiamato species.sh che prenda qualsiasi numero di nomi di file come argomenti della linea di comando, e usi una variazione del comando precedente per stampare una lista delle specie uniche che appaiono in ciascuno di quei file separatamente.

Soluzione

# 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

Supponiamo di aver appena eseguito una serie di comandi che hanno fatto qualcosa di utile – per esempio, hanno creato un grafico che vorremmo usare in un articolo.Invece di digitarli di nuovo (e potenzialmente sbagliarli) possiamo fare così:

$ history | tail -n 5 > redo-figure-3.sh

Il file redo-figure-3.sh ora contiene:

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

Dopo un attimo di lavoro in un editor per rimuovere i numeri di serie sui comandi, e per rimuovere la linea finale dove abbiamo chiamato il comando history, abbiamo una registrazione completamente accurata di come abbiamo creato quella figura.

Perché registrare i comandi nella cronologia prima di eseguirli?

Se si esegue il comando:

$ history | tail -n 5 > recent.sh

l’ultimo comando nel file è il comando history stesso, cioè,la shell ha aggiunto history al registro dei comandi prima di eseguirlo. Infatti, la shell aggiunge sempre i comandi al log prima di eseguirli. Perché pensate che lo faccia?

Soluzione

Se un comando causa un crash o un blocco di qualcosa, potrebbe essere utile sapere quale fosse quel comando, al fine di indagare il problema.Se il comando venisse registrato solo dopo averlo eseguito, non avremmo una registrazione dell’ultimo comando eseguito in caso di crash.

In pratica, la maggior parte delle persone sviluppa script di shell eseguendo alcuni comandi al prompt della shell per assicurarsi che stiano facendo la cosa giusta, poi salvandoli in un file per riutilizzarli.Questo stile di lavoro permette alle persone di riciclare ciò che scoprono sui loro dati e sul loro flusso di lavoro con una chiamata a historye un po’ di editing per pulire l’output e salvarlo come uno script di shell.

Nelle’s Pipeline: Creazione di uno script

Il supervisore di Nelle ha insistito che tutte le sue analisi devono essere riproducibili. Il modo più semplice per catturare tutti i passi è in uno script.

Prima torniamo alla directory dei dati di Nelle:

$ cd ../north-pacific-gyre/2012-07-03/

Esegue l’editor e scrive quanto segue:

# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone

Salva questo in un file chiamato do-stats.shin modo che ora possa rifare la prima fase della sua analisi digitando:

$ bash do-stats.sh NENE*.txt

Può anche fare così:

$ bash do-stats.sh NENE*.txt | wc -l

in modo che l’output sia solo il numero di file processati piuttosto che i nomi dei file processati.

Una cosa da notare dello script di Nelle è che lascia la persona che lo esegue decidere quali file elaborare.Avrebbe potuto scriverlo come:

# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone

Il vantaggio è che questo seleziona sempre i file giusti: non deve ricordarsi di escludere i file ‘Z’. Lo svantaggio è che seleziona sempre solo quei file – non può eseguirlo su tutti i file (inclusi i file ‘Z’), o sui file ‘G’ o ‘H’ che i suoi colleghi in Antartide producono, senza modificare lo script.Se volesse essere più avventurosa, potrebbe modificare il suo script per controllare gli argomenti della linea di comando, e usare NENE*.txt se non ne viene fornito nessuno, naturalmente questo introduce un altro compromesso tra flessibilità e complessità.

Variabili negli script di shell

Nella directory molecules, immaginate di avere uno script di shell chiamato script.sh contenente i seguenti comandi:

head -n  tail -n  

Mentre siete nella directory molecules, digitate il seguente comando:

bash script.sh '*.pdb' 1 1

Quale dei seguenti output vi aspettate di vedere?

  1. Tutte le linee tra la prima e l’ultima linea di ogni file che termina in .pdbnella molecules directory
  2. La prima e l’ultima linea di ogni file che termina in .pdb nella molecules directory
  3. La prima e l’ultima linea di ogni file nella directory molecules
  4. Un errore a causa delle virgolette intorno a *.pdb

Soluzione

La risposta corretta è 2.

Le variabili speciali $1, $2 e $3 rappresentano gli argomenti della linea di comando dati allo script, cosicché i comandi eseguiti sono:

$ 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

La shell non espande '*.pdb' perché è racchiuso tra virgolette.Come tale, il primo argomento dello script è '*.pdb' che viene espanso nello script da head e tail.

Trova il file più lungo con una data estensione

Scrivi uno script di shell chiamato longest.sh che prende come argomenti il nome di una directory e l’estensione di un nome di file, e stampa il nome del file con più righe in quella directory con quell’estensione. Per esempio:

$ bash longest.sh /tmp/data pdb

stamperà il nome del file .pdb in /tmp/data che ha più linee.

Soluzione

# 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

La prima parte della pipeline, wc -l /*. | sort -n, conta le linee in ogni file e le ordina numericamente (la più grande per ultima). Quando c’è più di un file, wc produce anche una linea di riepilogo finale, dando il numero totale di linee in tutti i file. Usiamo tail-n 2 | head -n 1 per buttare via quest’ultima linea.

Con wc -l /*. | sort -n | tail -n 1 vedremo la linea riassuntiva finale: possiamo costruire la nostra pipeline a pezzi per essere sicuri di capire l’output.

Script Reading Comprehension

Per questa domanda, considerate ancora una volta la directory data-shell/molecules.Questa contiene un certo numero di file .pdb oltre a qualsiasi altro file che hai creato. Spiega cosa farebbe ciascuno dei seguenti tre script se eseguito rispettivamente come bash script1.sh *.pdb, bash script2.sh *.pdb e bash script3.sh *.pdb.

# Script 1echo *.*
# Script 2for filename in   do cat $filenamedone
# Script 3echo [email protected]

Solutions

In ogni caso, la shell espande il carattere jolly in *.pdb prima di passare la risultante lista di nomi di file come argomenti allo script.

Lo script 1 stamperà una lista di tutti i file che contengono un punto nel loro nome; gli argomenti passati allo script non sono effettivamente utilizzati nello script.

Lo script 2 stamperà il contenuto dei primi 3 file con estensione .pdb., e si riferiscono rispettivamente al primo, al secondo e al terzo argomento.

Script 3 stamperebbe tutti gli argomenti dello script (cioè tutti i file .pdb), seguiti da .pdb.$@ si riferisce a tutti gli argomenti dati a uno script di shell.

cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb

Script di debug

Supponiamo che tu abbia salvato il seguente script in un file chiamato do-errors.sh nella directory north-pacific-gyre/2012-07-03 di Nelle:

# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone

Quando lo esegui:

$ bash do-errors.sh NENE*.txt

l’output è vuoto.Per capire perché, esegui nuovamente lo script usando l’opzione -x:

bash -x do-errors.sh NENE*.txt

Cosa ti mostra l’output? Quale linea è responsabile dell’errore?

Soluzione

L’opzione -x fa sì che bash venga eseguito in modalità debug, il che stampa ogni comando mentre viene eseguito, il che vi aiuterà a localizzare gli errori.In questo esempio, possiamo vedere che echo non stampa nulla. Abbiamo fatto un errore di battitura nel nome della variabile del ciclo, e la variabile datfile non esiste, quindi restituisce una stringa vuota.

Punti chiave

  • Salva i comandi in file (solitamente chiamati script di shell) per riutilizzarli.

  • bash esegue i comandi salvati in un file.

  • $@ si riferisce a tutti gli argomenti della linea di comando di uno script di shell.

  • , , ecc, si riferiscono al primo argomento della riga di comando, al secondo argomento della riga di comando, ecc.

  • Porre le variabili tra virgolette se i valori potrebbero avere spazi al loro interno.

  • Lasciare che gli utenti decidano quali file elaborare è più flessibile e più coerente con i comandi Unix integrati.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.