- Overzicht
- Text vs. Whatever
- Double-Quotes Around Arguments
- Lijst Unieke Soorten
- Oplossing
- Waarom commando’s in de geschiedenis opslaan voordat ze worden uitgevoerd?
- Oplossing
- Nelle’s Pipeline: Creating a Script
- Variabelen in Shell Scripts
- Oplossing
- Vind het langste bestand met een gegeven extensie
- Oplossing
- Script Reading Comprehension
- Solutions
- Debugging Scripts
- Oplossing
- Key Points
Overzicht
Leerstof: 30 min
Oefeningen: 15 minVragen
Hoe kan ik commando’s opslaan en hergebruiken?
Doelstellingen
Schrijf een shellscript dat een commando of serie commando’s uitvoert voor een vaste set bestanden.
Uitvoeren van een shellscript vanaf de commandoregel.
Schrijf een shellscript dat werkt op een reeks bestanden die door de gebruiker op de opdrachtregel zijn gedefinieerd.
Maak pijplijnen die shellscripts bevatten die u, en anderen, hebben geschreven.
We zijn eindelijk klaar om te zien wat de shell zo’n krachtige programmeeromgeving maakt.We gaan de commando’s die we vaak herhalen opslaan in bestanden, zodat we al die bewerkingen later opnieuw kunnen uitvoeren door een enkel commando te typen. Om historische redenen wordt een groep commando’s die in een bestand zijn opgeslagen meestal een shellscript genoemd, maar vergis je niet: dit zijn eigenlijk kleine programma’s.
Laten we beginnen met terug te gaan naar
molecules/
en een nieuw bestand te maken,middle.sh
dat ons shell script zal worden:$ cd molecules$ nano middle.sh
Het commando
nano middle.sh
opent het bestandmiddle.sh
in de tekst editor ‘nano’ (dat draait in de shell).Als het bestand niet bestaat, wordt het aangemaakt. We kunnen de teksteditor gebruiken om het bestand rechtstreeks te bewerken – we voegen gewoon de volgende regel in:head -n 15 octane.pdb | tail -n 5
Dit is een variatie op de pijp die we eerder hebben gemaakt: het selecteert regels 11-15 van het bestand
octane.pdb
.Onthoud dat we het nog niet als een commando uitvoeren: we zetten de commando’s in een bestand.Daarna slaan we het bestand op (
Ctrl-O
in nano), en sluiten de tekst editor af (Ctrl-X
in nano).Controleer dat de directorymolecules
nu een bestand bevat met de naammiddle.sh
.Als we het bestand hebben opgeslagen, kunnen we de shell vragen om de commando’s die erin staan uit te voeren.Onze shell heet
bash
, dus we voeren het volgende commando uit:$ 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
Zeker, de uitvoer van ons script is precies wat we zouden krijgen als we die pijplijn direct zouden uitvoeren.
Text vs. Whatever
Wij noemen programma’s als Microsoft Word of LibreOffice Writer meestal “tekstverwerkers”, maar we moeten iets voorzichtiger zijn als het op programmeren aankomt. Standaard gebruikt Microsoft Word
.docx
bestanden om niet alleen tekst op te slaan, maar ook opmaak informatie over lettertypes, koppen, en binnenkort. Deze extra informatie wordt niet opgeslagen als tekens, en betekent niets voor programma’s alshead
: zij verwachten dat invoerbestanden niets anders bevatten dan de letters, cijfers, en interpunctie van een standaard toetsenbord. Wanneer u programma’s bewerkt, moet u dus of een platte tekst editor gebruiken, of erop letten dat u bestanden als platte tekst opslaat.Wat als we regels uit een willekeurig bestand willen selecteren? We zouden
middle.sh
iedere keer kunnen bewerken om de bestandsnaam te veranderen, maar dat zou waarschijnlijk langer duren dan het commando opnieuw in de commandoregel te typen en het uit te voeren met een nieuwe bestandsnaam.Laten we in plaats daarvanmiddle.sh
bewerken en het veelzijdiger maken:$ nano middle.sh
Nu, binnen “nano”, vervangt u de tekst
octane.pdb
door de speciale variabele genaamd:
head -n 15 "" | tail -n 5
In een shellscript betekent
“de eerste bestandsnaam (of een ander argument) op de commandoregel”.We kunnen ons script nu als volgt uitvoeren:
$ 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
of op een ander bestand als dit:
$ 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
Double-Quotes Around Arguments
Om dezelfde reden dat we de lusvariabele tussen dubbele aanhalingstekens zetten, omringen we
met dubbele aanhalingstekens, voor het geval de bestandsnaam spaties bevat.
Op dit moment moeten we
middle.sh
bewerken elke keer als we de reeks regels willen aanpassen die wordt teruggestuurd. Laten we dat oplossen door ons script zo te configureren dat het in plaats daarvan drie command-line argumenten gebruikt. Na het eerste command-line argument (), zal elk extra argument dat we opgeven toegankelijk zijn via de speciale variabelen
,
,
, die respectievelijk verwijzen naar het eerste, tweede en derde command-line argument.
Dit wetende, kunnen we extra argumenten gebruiken om de reeks regels te definiëren die moeten worden doorgegeven aan respectievelijk
head
entail
:$ nano middle.sh
head -n "" "" | tail -n ""
We kunnen nu uitvoeren:
$ 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
Door de argumenten van ons commando te veranderen, kunnen we het gedrag van ons script veranderen:
$ 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
Dit werkt, maar het kan de volgende persoon die
middle.sh
leest even tijd kosten om uit te vogelen wat het doet.We kunnen ons script verbeteren door bovenaan commentaar toe te voegen:$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""
Een commentaar begint met een
#
-teken en loopt door tot het einde van de regel. De computer negeert commentaar, maar het is van onschatbare waarde om mensen (inclusief uw toekomstige zelf) te helpen scripts te begrijpen en te gebruiken.Het enige voorbehoud is dat iedere keer dat u het script aanpast, u moet controleren of het commentaar nog steeds juist is: een uitleg die de lezer in de verkeerde richting stuurt is erger dan helemaal geen uitleg.Wat als we veel bestanden in een enkele pijplijn willen verwerken?Als we bijvoorbeeld onze
.pdb
bestanden op lengte willen sorteren, zouden we typen:$ wc -l *.pdb | sort -n
want
wc -l
geeft het aantal regels in de bestanden weer (onthoud datwc
staat voor ‘woordentelling’, het toevoegen van de-l
optie betekent in plaats daarvan ‘aantal regels’) ensort -n
sorteert de dingen numeriek.We zouden dit in een bestand kunnen zetten, maar dan zou het alleen een lijst van.pdb
bestanden in de huidige directory sorteren.Als we in staat willen zijn om een gesorteerde lijst van andere soorten bestanden te krijgen, hebben we een manier nodig om al die namen in het script te krijgen. We kunnen,
, enzovoorts niet gebruiken, omdat we niet weten hoeveel bestanden er zijn. In plaats daarvan gebruiken we de speciale variabele
$@
, wat betekent,’Alle command-line argumenten voor het shell script’.We moeten$@
ook binnen dubbele aanhalingstekens zetten om het geval van argumenten die spaties bevatten te behandelen ("$@"
is speciale syntax en is equivalent aan""
""
…).Hier volgt een voorbeeld:
$ 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
Lijst Unieke Soorten
Leah heeft enkele honderden gegevensbestanden, die elk als volgt zijn opgemaakt:
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
Een voorbeeld van dit soort bestanden is te vinden in
data-shell/data/animal-counts/animals.txt
.We kunnen het commando
cut -d , -f 2 animals.txt | sort | uniq
gebruiken om de unieke soorten inanimals.txt
te produceren. Om deze reeks commando’s niet iedere keer te hoeven typen, kan een wetenschapper ervoor kiezen om in plaats daarvan een shellscript te schrijven.Schrijf een shellscript met de naam
species.sh
dat een willekeurig aantal bestandsnamen als command-line-argumenten neemt, en gebruik maakt van een variatie op het bovenstaande commando om een lijst af te drukken met de unieke soorten die in elk van die bestanden afzonderlijk voorkomen.Oplossing
# 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
Stel dat we zojuist een reeks commando’s hebben uitgevoerd die iets nuttigs hebben gedaan – bijvoorbeeld een grafiek hebben gemaakt die we in een artikel willen gebruiken. We willen de grafiek later opnieuw kunnen maken als dat nodig is, dus willen we de commando’s in een bestand opslaan.In plaats van ze opnieuw in te typen (en ze mogelijk fout te hebben) kunnen we dit doen:
$ history | tail -n 5 > redo-figure-3.sh
Het bestand
redo-figure-3.sh
bevat nu: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
Na enig werk in een editor om de serienummers op de commando’s te verwijderen, en om de laatste regel te verwijderen waar we het
history
commando hebben aangeroepen, hebben we een volledig accurate registratie van hoe we dat figuur hebben gemaakt.Waarom commando’s in de geschiedenis opslaan voordat ze worden uitgevoerd?
Als u het commando uitvoert:
$ history | tail -n 5 > recent.sh
het laatste commando in het bestand is het
history
commando zelf, d.w.z.,de shell heefthistory
toegevoegd aan het commandologboek voordat hij het daadwerkelijk uitvoerde. In feite voegt de shell altijd commando’s toe aan het logboek voordat hij ze uitvoert. Waarom denkt u dat hij dit doet?Oplossing
Als een commando iets laat crashen of hangen, kan het handig zijn om te weten wat dat commando was, om het probleem te onderzoeken. Als het commando pas na het uitvoeren ervan zou worden geregistreerd, zouden we geen registratie hebben van het laatst uitgevoerde commando in geval van een crash.
In de praktijk ontwikkelen de meeste mensen shell scripts door een paar keer commando’s op de shell prompt uit te voeren om er zeker van te zijn dat ze het juiste doen, en ze dan op te slaan in een bestand voor hergebruik. Deze manier van werken stelt mensen in staat om wat ze ontdekken over hun gegevens en hun werkstroom te recyclen met een enkele oproep aan
history
en een beetje bewerking om de uitvoer op te schonen en op te slaan als een shell script.Nelle’s Pipeline: Creating a Script
Nelle’s supervisor stond erop dat al haar analyses reproduceerbaar moesten zijn. De makkelijkste manier om alle stappen vast te leggen is in een script.
Eerst gaan we terug naar Nelle’s gegevensmap:
$ cd ../north-pacific-gyre/2012-07-03/
Ze start de editor en schrijft het volgende:
# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone
Ze slaat dit op in een bestand met de naam
do-stats.sh
zodat ze nu de eerste fase van haar analyse opnieuw kan doen door te typen:$ bash do-stats.sh NENE*.txt
Zo kan ze het ook doen:
$ bash do-stats.sh NENE*.txt | wc -l
zodat de uitvoer alleen het aantal verwerkte bestanden is en niet de namen van de bestanden die zijn verwerkt.
Eén opmerking over Nelle’s script is dat het de uitvoerder laat beslissen welke bestanden moeten worden verwerkt.Ze had het kunnen schrijven als:
# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone
Het voordeel is dat hiermee altijd de juiste bestanden worden geselecteerd: ze hoeft er niet aan te denken om de ‘Z’-bestanden uit te sluiten. Het nadeel is dat het altijd alleen die bestanden selecteert – ze kan het niet op alle bestanden uitvoeren (inclusief de ‘Z’-bestanden), of op de ‘G’- of ‘H’-bestanden die haar collega’s op Antarctica produceren, zonder het script te bewerken.Als ze avontuurlijker wil zijn, kan ze haar script zo aanpassen dat het controleert op command-line argumenten, en
NENE*.txt
gebruiken als er geen zijn gegeven. Natuurlijk introduceert dit een andere afweging tussen flexibiliteit en complexiteit.Variabelen in Shell Scripts
In de
molecules
directory, stel u heeft een shell script genaamdscript.sh
dat de volgende commando’s bevat:head -n tail -n
Wanneer u in de
molecules
directory bent, typt u het volgende commando:bash script.sh '*.pdb' 1 1
Welke van de volgende outputs zou u verwachten te zien?
- Alle regels tussen de eerste en de laatste regel van elk bestand dat eindigt op
.pdb
in demolecules
directory- De eerste en de laatste regel van elk bestand dat eindigt op
.pdb
in demolecules
directory- De eerste en de laatste regel van elk bestand in de
molecules
directory- Een fout vanwege de aanhalingstekens rond
*.pdb
Oplossing
Het juiste antwoord is 2.
De speciale variabelen $1, $2 en $3 staan voor de command line argumenten die aan het script zijn gegeven, zodat de commando’s die worden uitgevoerd zijn:
$ 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
De shell breidt
'*.pdb'
niet uit omdat het is ingesloten door aanhalingstekens. Als zodanig is het eerste argument voor het script'*.pdb'
dat binnen het script wordt uitgebreid methead
entail
.Vind het langste bestand met een gegeven extensie
Schrijf een shellscript met de naam
longest.sh
dat de naam van een directory en een extensie van een bestandsnaam als argumenten neemt, en de naam van het bestand met de meeste regels in die directory met die extensie afdrukt. Bijvoorbeeld:$ bash longest.sh /tmp/data pdb
zou de naam van het
.pdb
bestand in/tmp/data
afdrukken dat de meeste regels heeft.Oplossing
# 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
Het eerste deel van de pijplijn,
wc -l /*. | sort -n
, telt de regels in elk bestand en sorteert ze numeriek (grootste laatst). Wanneer er meer dan één bestand is, geeftwc
ook een laatste overzichtsregel, met het totaal aantal regels over alle bestanden. We gebruikentail-n 2 | head -n 1
om deze laatste regel weg te gooien.Met
wc -l /*. | sort -n | tail -n 1
zien we de laatste samenvattingsregel: we kunnen onze pijplijn in stukjes opbouwen om er zeker van te zijn dat we de uitvoer begrijpen.Script Reading Comprehension
Voor deze vraag bekijken we de
data-shell/molecules
directory nog eens.Deze bevat een aantal.pdb
-bestanden naast andere bestanden die u mogelijk hebt gemaakt. Leg uit wat elk van de volgende drie scripts zou doen wanneer ze worden uitgevoerd als respectievelijkbash script1.sh *.pdb
,bash script2.sh *.pdb
, enbash script3.sh *.pdb
.# Script 1echo *.*
# Script 2for filename in do cat $filenamedone
# Script 3echo [email protected]
Solutions
In elk geval breidt de shell het jokerteken in
*.pdb
uit alvorens de resulterende lijst met bestandsnamen als argumenten aan het script door te geven.Script 1 zou een lijst afdrukken van alle bestanden met een punt in hun naam. De argumenten die aan het script worden doorgegeven, worden nergens in het script gebruikt.
Script 2 zou de inhoud afdrukken van de eerste 3 bestanden met een
.pdb
bestandsextensie.,
, en
verwijzen respectievelijk naar het eerste, tweede, en derde argument.
Script 3 zou alle argumenten voor het script afdrukken (d.w.z. alle
.pdb
bestanden), gevolgd door.pdb
.$@
verwijst naar alle argumenten die aan een shell script worden gegeven.cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Debugging Scripts
Voorstel dat u het volgende script hebt opgeslagen in een bestand met de naam
do-errors.sh
in Nelle’snorth-pacific-gyre/2012-07-03
directory:# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone
Wanneer u het uitvoert:
$ bash do-errors.sh NENE*.txt
is de uitvoer leeg.Om erachter te komen waarom, voert u het script opnieuw uit met de
-x
optie:bash -x do-errors.sh NENE*.txt
Wat laat de uitvoer u zien? Welke regel is verantwoordelijk voor de fout?
Oplossing
De
-x
optie zorgt ervoor datbash
in debug mode draait. Dit drukt elk commando af terwijl het wordt uitgevoerd, wat u zal helpen fouten te lokaliseren. In dit voorbeeld, kunnen we zien datecho
niets afdrukt. We hebben een tikfout gemaakt in de loop variabele naam, en de variabeledatfile
bestaat niet, vandaar dat het een lege string teruggeeft.Key Points
Opslaan van commando’s in bestanden (meestal shell scripts genoemd) voor hergebruik.
bash
voert de opdrachten uit die in een bestand zijn opgeslagen.
$@
verwijst naar alle opdrachtregelargumenten van een shellscript.
,
, enz, verwijzen naar het eerste commandoregelargument, het tweede commandoregelargument, enz.
Variabelen tussen aanhalingstekens plaatsen als de waarden spaties kunnen bevatten.
Gebruikers laten beslissen welke bestanden moeten worden verwerkt, is flexibeler en consistenter met ingebouwde Unix-opdrachten.