- Overview
- Text vs. Whatever
- Cuvinte duble în jurul argumentelor
- List Unique Species
- Soluție
- De ce să înregistrăm comenzile în istoric înainte de a le executa?
- Soluție
- Nelle’s Pipeline: Crearea unui script
- Variabile în scripturile shell
- Soluție
- Găsește cel mai lung fișier cu o anumită extensie
- Soluție
- Comprehensiunea de citire a scripturilor
- Soluții
- Debugging Scripts
- Soluție
- Puncturi cheie
Overview
Predare: 30 min
Exerciții: 15 minÎntrebări
Cum pot salva și reutiliza comenzile?
Obiective
Scrieți un script shell care execută o comandă sau o serie de comenzi pentru un set fix de fișiere.
Executați un script shell din linia de comandă.
Scrieți un script shell care operează asupra unui set de fișiere definit de utilizator în linia de comandă.
Creați pipe-line-uri care includ scripturi shell scrise de dumneavoastră și de alții.
În sfârșit suntem gata să vedem ce face din shell un mediu de programare atât de puternic.Vom lua comenzile pe care le repetăm frecvent și le vom salva în fișierepentru ca mai târziu să putem rula din nou toate aceste operații prin tastarea unei singure comenzi.Din motive istorice,o grămadă de comenzi salvate într-un fișier se numește de obicei script shell,dar nu vă înșelați:acestea sunt de fapt mici programe.
Să începem prin a ne întoarce la
molecules/
și să creăm un nou fișier,middle.sh
care va deveni scriptul nostru shell:$ cd molecules$ nano middle.sh
Comanda
nano middle.sh
deschide fișierulmiddle.sh
în editorul de text ‘nano'(care rulează în shell).Dacă fișierul nu există, acesta va fi creat. putem folosi editorul de text pentru a edita direct fișierul – vom introduce pur și simplu următoarea linie:head -n 15 octane.pdb | tail -n 5
Aceasta este o variație a pipe-ului pe care l-am construit mai devreme:selectează liniile 11-15 din fișierul
octane.pdb
.Țineți minte, încă nu o executăm ca o comandă:punem comenzile într-un fișier.Apoi salvăm fișierul (
Ctrl-O
în nano), și ieșim din editorul de text (Ctrl-X
în nano).Verificați că directorulmolecules
conține acum un fișier numitmiddle.sh
.După ce am salvat fișierul,putem cere shell-ului să execute comenzile pe care le conține.Shell-ul nostru se numește
bash
, așa că executăm următoarea comandă:$ 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
Sigur, ieșirea script-ului nostru este exact ceea ce am obține dacă am rula direct acea conductă.
Text vs. Whatever
De obicei numim programe precum Microsoft Word sau LibreOffice Writer „texteditors”, dar trebuie să fim puțin mai atenți când vine vorba deprogramare. În mod implicit, Microsoft Word utilizează fișiere
.docx
pentru a stoca nu numai text, ci și informații de formatare privind fonturile, titlurile și, în curând. Aceste informații suplimentare nu sunt stocate sub formă de caractere și nu înseamnă nimic pentru instrumente precumhead
: acestea se așteaptă ca fișierele de intrare să nu conțină decât literele, cifrele și semnele de punctuație de pe o tastatură standard de calculator. Prin urmare, atunci când editați programe, trebuie fie să folosiți un editor de text în clar, fie să aveți grijă să salvați fișierele ca text în clar.Ce se întâmplă dacă vrem să selectăm linii dintr-un fișier arbitrar?Am putea edita
middle.sh
de fiecare dată pentru a schimba numele fișierului,dar acest lucru ar dura probabil mai mult decât să scriem din nou comanda în shell și să o executăm cu un nou nume de fișier.În schimb, haideți să editămmiddle.sh
și să o facem mai versatilă:$ nano middle.sh
Acum, în „nano”, înlocuiți textul
octane.pdb
cu variabila specială numită:
head -n 15 "" | tail -n 5
În interiorul unui script shell,
înseamnă „primul nume de fișier (sau alt argument) de pe linia de comandă”.Acum putem rula scriptul nostru astfel:
$ 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
sau pe un alt fișier astfel:
$ 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
Cuvinte duble în jurul argumentelor
Pentru același motiv pentru care punem variabila de buclă între ghilimele duble, în cazul în care numele fișierului conține spații, înconjurăm
cu ghilimele duble.
În prezent, trebuie să modificăm
middle.sh
de fiecare dată când dorim să ajustăm intervalul de linii care este returnat. Să rezolvăm acest lucru prin configurarea scriptului nostru pentru a utiliza în schimb trei argumente în linia de comandă. După primul argument din linia de comandă (), fiecare argument suplimentar pe care îl furnizăm va fi accesibil prin intermediul variabilelor speciale
,
,
, care se referă la primul, al doilea, respectiv al treilea argument din linia de comandă.
Cunoscând acest lucru, putem folosi argumente suplimentare pentru a defini intervalul de linii care vor fi transmise la
head
și, respectiv,tail
:$ nano middle.sh
head -n "" "" | tail -n ""
Acum putem rula:
$ 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
Schimbând argumentele la comanda noastră putem schimba comportamentul scriptului nostru:
$ 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
Acest lucru funcționează, dar s-ar putea ca următoarea persoană care citește
middle.sh
să aibă nevoie de un moment pentru a-și da seama ce face.Putem îmbunătăți scriptul nostru adăugând câteva comentarii în partea de sus:$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""
Un comentariu începe cu un caracter
#
și merge până la sfârșitul liniei.Calculatorul ignoră comentariile,dar acestea sunt de neprețuit pentru a ajuta oamenii (inclusiv pe viitorul dumneavoastră) să înțeleagă și să folosească scripturile.Singurul avertisment este că de fiecare dată când modificați scriptul,ar trebui să verificați dacă comentariul este încă corect:o explicație care trimite cititorul într-o direcție greșită este mai rea decât niciuna.Ce se întâmplă dacă vrem să procesăm mai multe fișiere într-o singură conductă?De exemplu, dacă vrem să sortăm fișierele noastre
.pdb
în funcție de lungime, vom tasta:$ wc -l *.pdb | sort -n
pentru că
wc -l
enumeră numărul de linii din fișiere(amintiți-vă căwc
înseamnă „număr de cuvinte”, adăugarea opțiunii-l
înseamnă în schimb „număr de linii”)iarsort -n
sortează lucrurile numeric.Am putea pune acest lucru într-un fișier,dar atunci ar sorta doar o listă de.pdb
fișiere din directorul curent.Dacă vrem să putem obține o listă ordonată a altor tipuri de fișiere,avem nevoie de o modalitate de a introduce toate aceste nume în script.Nu putem folosi,
și așa mai departepentru că nu știm câte fișiere există.În schimb,folosim variabila specială
$@
,care înseamnă:’Toate argumentele liniei de comandă pentru scriptul shell’.De asemenea, ar trebui să punem$@
în interiorul ghilimelelor dublepentru a gestiona cazul argumentelor care conțin spații("$@"
este o sintaxă specială și este echivalentă cu""
""
…).Iată un exemplu:
$ 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
List Unique Species
Leah are câteva sute de fișiere de date, fiecare dintre ele fiind formatat astfel:
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 exemplu de acest tip de fișier este dat în
data-shell/data/animal-counts/animals.txt
.Se poate utiliza comanda
cut -d , -f 2 animals.txt | sort | uniq
pentru a produce specia unică dinanimals.txt
. Pentru a evita să fie nevoit să tasteze de fiecare dată această serie de comenzi, un om de știință poate alege să scrie în schimb un script shell.Scrieți un script shell numit
species.sh
care primește orice număr de nume de fișiere ca argumente în linia de comandă și folosește o variantă a comenzii de mai sus pentru a tipări o listă a speciilor unice care apar în fiecare dintre aceste fișiere separat.Soluție
# 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ă presupunem că tocmai am executat o serie de comenzi care au făcut ceva util – de exemplu, care au creat un grafic pe care am dori să-l folosim într-o lucrare.Am dori să putem recrea graficul mai târziu, dacă avem nevoie, așa că dorim să salvăm comenzile într-un fișier.În loc să le tastăm din nou(și eventual să le greșim)putem face așa:
$ history | tail -n 5 > redo-figure-3.sh
Fileul
redo-figure-3.sh
conține acum: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
După un moment de lucru într-un editor pentru a elimina numerele de serie de pe comenzi,și pentru a elimina linia finală în care am apelat comanda
history
,avem o înregistrare complet exactă a modului în care am creat acea figură.De ce să înregistrăm comenzile în istoric înainte de a le executa?
Dacă executați comanda:
$ history | tail -n 5 > recent.sh
ultima comandă din fișier este chiar comanda
history
, adică,shell-ul a adăugathistory
la jurnalul de comenzi înainte de a o executa efectiv. De fapt, shell-ul adaugă întotdeauna comenzi în jurnal înainte de a le executa. De ce credeți că face acest lucru?Soluție
În cazul în care o comandă provoacă un blocaj sau un blocaj, ar putea fi util să știm care a fost acea comandă, pentru a investiga problema.Dacă comanda ar fi înregistrată numai după ce a fost executată, nu am avea o înregistrare a ultimei comenzi executate în cazul unui blocaj.
În practică, majoritatea oamenilor dezvoltă scripturi shell executând comenzi la promptul shell de câteva oripentru a se asigura că fac ceea ce trebuie, apoi salvându-le într-un fișier pentru reutilizare.Acest stil de lucru le permite oamenilor să recicleze ceea ce descoperă despre datele lor și despre fluxul lor de lucru cu un singur apel la
history
și un pic de editare pentru a curăța ieșirea și a o salva ca script shell.Nelle’s Pipeline: Crearea unui script
Supervizorul lui Nelle a insistat ca toate analizele ei să fie reproductibile. Cel mai simplu mod de a captura toți pașii este într-un script.
În primul rând ne întoarcem în directorul de date al lui Nelle:
$ cd ../north-pacific-gyre/2012-07-03/
Ea rulează editorul și scrie următoarele:
# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone
Salvează acest lucru într-un fișier numit
do-stats.sh
pentru ca acum să poată reface prima etapă a analizei sale tastând:$ bash do-stats.sh NENE*.txt
De asemenea, poate face acest lucru:
$ bash do-stats.sh NENE*.txt | wc -l
pentru ca rezultatul să fie doar numărul de fișiere procesate și nu numele fișierelor care au fost procesate.
Un lucru de reținut în legătură cu scriptul lui Nelle este că acesta lasă persoana care îl execută să decidă ce fișiere să proceseze.Ea l-ar fi putut scrie ca:
# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone
Avantajul este că acesta selectează întotdeauna fișierele corecte:nu trebuie să își amintească să excludă fișierele „Z”.Dezavantajul este că selectează întotdeauna doar acele fișiere – nu îl poate rula pe toate fișierele (inclusiv pe cele „Z”),sau pe fișierele „G” sau „H” pe care le produc colegii ei din Antarctica,fără a modifica scriptul.Dacă ar vrea să fie mai aventuroasă,ar putea să modifice scriptul pentru a verifica dacă există argumente în linia de comandă și să folosească
NENE*.txt
dacă nu este furnizat niciunul.Desigur,acest lucru introduce un alt compromis între flexibilitate și complexitate.Variabile în scripturile shell
În directorul
molecules
, imaginați-vă că aveți un script shell numitscript.sh
care conține următoarele comenzi:head -n tail -n
În timp ce vă aflați în directorul
molecules
, tastați următoarea comandă:bash script.sh '*.pdb' 1 1
Care dintre următoarele ieșiri vă așteptați să vedeți?
- Toate liniile dintre prima și ultima linie a fiecărui fișier care se termină în
.pdb
din directorulmolecules
- Prima și ultima linie a fiecărui fișier care se termină în
.pdb
din directorulmolecules
. directorPrima și ultima linie a fiecărui fișier din directorul
molecules
- O eroare din cauza ghilimelelor din jurul
*.pdb
Soluție
Răspunsul corect este 2.
Variabilele speciale $1, $2 și $3 reprezintă argumentele din linia de comandă date scriptului, astfel încât comenzile executate sunt:
$ 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
Shell-ul nu extinde
'*.pdb'
deoarece este închis între ghilimele.Ca atare, primul argument al scriptului este'*.pdb'
care este extins în script prinhead
șitail
.Găsește cel mai lung fișier cu o anumită extensie
Scrieți un script shell numit
longest.sh
care ia ca argumente numele unui director și o extensie a numelui de fișier și tipărește numele fișierului cu cele mai multe linii din acel director cu acea extensie. De exemplu:$ bash longest.sh /tmp/data pdb
a imprima numele fișierului
.pdb
din/tmp/data
care are cele mai multe linii.Soluție
# 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
Prima parte a conductei,
wc -l /*. | sort -n
, numără liniile din fiecare fișier și le ordonează numeric (cele mai mari la sfârșit). În cazul în care există mai multe fișiere,wc
emite, de asemenea, o linie finală de rezumat, care indică numărul total de linii din toate fișierele. Folosimtail-n 2 | head -n 1
pentru a arunca această ultimă linie.Cu
wc -l /*. | sort -n | tail -n 1
vom vedea linia de rezumat finală: putem construi conducta noastră pe bucăți pentru a fi siguri că înțelegem ieșirea.Comprehensiunea de citire a scripturilor
Pentru această întrebare, luați din nou în considerare directorul
data-shell/molecules
.Acesta conține un număr de fișiere.pdb
în plus față de orice alte fișiere pe care le puteți fi creat.Explicați ce ar face fiecare dintre următoarele trei scripturi atunci când sunt rulate cabash script1.sh *.pdb
,bash script2.sh *.pdb
și, respectiv,bash script3.sh *.pdb
.# Script 1echo *.*
# Script 2for filename in do cat $filenamedone
# Script 3echo [email protected]
Soluții
În fiecare caz, shell-ul extinde wildcard-ul din
*.pdb
înainte de a trece lista de nume de fișiere rezultată ca argumente pentru script.Scriptul 1 ar tipări o listă a tuturor fișierelor care conțin un punct în numele lor.Argumentele transmise scriptului nu sunt de fapt folosite nicăieri în script.
Scriptul 2 ar tipări conținutul primelor 3 fișiere cu extensia
.pdb
.,și
se referă la primul, al doilea și, respectiv, al treilea argument.
Script 3 ar imprima toate argumentele scriptului (adică toate fișierele
.pdb
),urmate de.pdb
.$@
se referă la toate argumentele date unui script shell.cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Debugging Scripts
Să presupunem că ați salvat următorul script într-un fișier numit
do-errors.sh
în directorulnorth-pacific-gyre/2012-07-03
al lui Nelle:# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone
Când îl rulați:
$ bash do-errors.sh NENE*.txt
la ieșire este goală.Pentru a afla de ce, rulați din nou scriptul folosind opțiunea
-x
:bash -x do-errors.sh NENE*.txt
Ce vă arată ieșirea?Ce linie este responsabilă de eroare?
Soluție
Opțiunea
-x
face cabash
să ruleze în modul de depanare.Acest lucru tipărește fiecare comandă pe măsură ce este executată, ceea ce vă va ajuta să localizați erorile.În acest exemplu, putem vedea căecho
nu tipărește nimic. Am făcut o greșeală de tipar în numele variabilei de buclă, iar variabiladatfile
nu există, prin urmare returnează un șir gol.Puncturi cheie
Salvați comenzile în fișiere (numite de obicei scripturi shell) pentru reutilizare.
bash
execută comenzile salvate într-un fișier.
$@
se referă la toate argumentele din linia de comandă a unui script shell.
,
, etc., se referă la primul argument de linie de comandă, al doilea argument de linie de comandă etc.
Puneți variabilele între ghilimele dacă valorile ar putea avea spații în ele.
Lăsând utilizatorii să decidă ce fișiere să proceseze este mai flexibil și mai coerent cu comenzile Unix încorporate.