Shell Scripts

okt 29, 2021

Översikt

Undervisning: 30 min
Övningar: 15 min
Frågor

  • Hur kan jag spara och återanvända kommandon?

Mål

  • Skriv ett skalskript som kör ett kommando eller en serie av kommandon för en bestämd uppsättning filer.

  • Kör ett skalskript från kommandoraden.

  • Skriv ett skalskript som kör en uppsättning filer som definieras av användaren på kommandoraden.

  • Skapa pipelines som innehåller skalskript som du och andra har skrivit.

  • Vi är äntligen redo att se vad som gör skalet till en så kraftfull programmeringsmiljö.Vi kommer att ta de kommandon som vi upprepar ofta och spara dem i filerså att vi kan köra om alla dessa operationer igen senare genom att skriva ett enda kommando. av historiska skäl brukar ett gäng kommandon som sparas i en fil kallas för ett skalskript, men missta dig inte: dessa är faktiskt små program.

    Låt oss börja med att gå tillbaka till molecules/ och skapa en ny fil, middle.sh som kommer att bli vårt skalskript:

    $ cd molecules$ nano middle.sh

    Kommandot nano middle.sh öppnar filen middle.sh i textredigeraren ’nano’ (som körs i skalet).Om filen inte finns kommer den att skapas.Vi kan använda textredigeraren för att redigera filen direkt – vi lägger helt enkelt in följande rad:

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

    Det här är en variant på pipen som vi konstruerade tidigare: den väljer raderna 11-15 i filen octane.pdb.Kom ihåg att vi inte kör den som ett kommando ännu: vi lägger kommandona i en fil.

    Därefter sparar vi filen (Ctrl-O i nano) och avslutar textredigeraren (Ctrl-X i nano).Kontrollera att katalogen molecules nu innehåller en fil som heter middle.sh.

    När vi har sparat filen kan vi be skalet att utföra de kommandon som den innehåller. vårt skal heter bash, så vi kör följande kommando:

    $ 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

    Säkerligen är vårt skriptutgång exakt vad vi skulle få om vi körde den pipelinen direkt.

    Text vs. Whatever

    Vi brukar kalla program som Microsoft Word eller LibreOffice Writer för ”texteditorer”, men vi måste vara lite mer försiktiga när det gäller programmering. Som standard använder Microsoft Word .docx-filer för att lagra inte bara text utan även formateringsinformation om typsnitt, rubriker och snart. Denna extra information lagras inte som tecken och betyder ingenting för verktyg som head: de förväntar sig att inmatningsfiler inte innehåller något annat än bokstäver, siffror och interpunktion på ett standardtangentbord. När du redigerar program måste du därför antingen använda en redigeringsprogram för klartext eller vara noga med att spara filer som klartext.

    Vad händer om vi vill välja rader från en godtycklig fil?Vi skulle kunna redigera middle.sh varje gång för att ändra filnamnet, men det skulle antagligen ta längre tid än att skriva kommandot igen i skalet och köra det med ett nytt filnamn.Låt oss istället redigera middle.sh och göra den mer mångsidig:

    $ nano middle.sh

    Nu, i ”nano”, ersätter du texten octane.pdb med den speciella variabel som kallas :

    head -n 15 "" | tail -n 5

    Inom ett skalskript betyder ”det första filnamnet (eller annat argument) på kommandoraden”.Vi kan nu köra vårt skript så här:

    $ 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

    eller på en annan fil så här:

    $ 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

    Dubbla citattecken runt argument

    Av samma anledning som att vi placerar loop-variabeln inom dubbla citattecken, om filnamnet skulle råka innehålla blanksteg, omger vi med dubbla citattecken.

    För närvarande måste vi redigera middle.sh varje gång vi vill justera intervallet av rader som returneras. Låt oss åtgärda detta genom att konfigurera vårt skript så att det i stället använder tre kommandoradsargument. Efter det första kommandoradsargumentet () kommer varje ytterligare argument som vi tillhandahåller att vara tillgängligt via de särskilda variablerna , , , som hänvisar till det första, andra respektive tredje kommandoradsargumentet.

    Med vetskap om detta kan vi använda ytterligare argument för att definiera intervallet av rader som ska skickas till head respektive tail:

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

    Vi kan nu köra:

    $ 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

    Om vi ändrar argumenten till vårt kommando kan vi ändra vårt skriptbeteende:

    $ 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

    Detta fungerar, men det kan ta nästa person som läser middle.sh en stund att lista ut vad det gör.Vi kan förbättra vårt skript genom att lägga till några kommentarer överst:

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

    En kommentar börjar med ett #-tecken och löper fram till radens slut.Datorn ignorerar kommentarer, men de är ovärderliga för att hjälpa människor (inklusive ditt framtida jag) att förstå och använda skript.Den enda invändningen är att varje gång du ändrar skriptet bör du kontrollera att kommentaren fortfarande är korrekt: en förklaring som skickar läsaren i fel riktning är värre än ingen alls.

    Hur blir det om vi vill behandla många filer i en enda pipeline?Om vi till exempel vill sortera våra .pdb-filer efter längd skulle vi skriva:

    $ wc -l *.pdb | sort -n

    för att wc -l listar antalet rader i filerna (kom ihåg att wc står för ”word count” (antal ord), att lägga till alternativet -l innebär att man i stället räknar ”count lines” (antal rader)) och sort -n sorterar saker numeriskt.Vi skulle kunna lägga detta i en fil,men då skulle det bara sortera en lista med .pdb filer i den aktuella katalogen.Om vi vill kunna få en sorterad lista med andra typer av filer behöver vi ett sätt att få in alla dessa namn i skriptet.Vi kan inte använda , och så vidare eftersom vi inte vet hur många filer det finns.Istället använder vi den speciella variabeln $@ som betyder ”Alla kommandoradsargument till skalskriptet”.Vi bör också sätta $@ inom dubbla citattecken för att hantera fallet med argument som innehåller blanksteg ("$@" är en speciell syntax och motsvarar "" "" …).

    Här är ett exempel:

    $ 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 Unika arter

    Leah har flera hundra datafiler, som var och en är formaterad på detta sätt:

    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

    Ett exempel på denna typ av fil finns i data-shell/data/animal-counts/animals.txt.

    Vi kan använda kommandot cut -d , -f 2 animals.txt | sort | uniq för att ta fram den unika arten i animals.txt. För att slippa skriva denna serie kommandon varje gång kan en forskare välja att skriva ett skalskript i stället.

    Skriv ett skalskript med namnet species.sh som tar ett valfritt antal filnamn som kommandoradsargument och använder en variant av ovanstående kommando för att skriva ut en lista över de unika arter som förekommer i var och en av dessa filer separat.

    Lösning

    # 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

    Antag att vi just har kört en serie kommandon som har gjort något användbart – till exempel skapat en graf som vi vill använda i en uppsats. vi vill kunna återskapa grafen senare om vi behöver det, så vi vill spara kommandona i en fil.Istället för att skriva in dem igen (och eventuellt göra fel) kan vi göra så här:

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

    Filen redo-figure-3.sh innehåller 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

    Efter en stunds arbete i en editor för att ta bort serienumren på kommandona och för att ta bort den sista raden där vi anropade kommandot history har vi ett helt korrekt register över hur vi skapade figuren.

    Varför registrera kommandon i historiken innan du kör dem?

    Om du kör kommandot:

    $ history | tail -n 5 > recent.sh

    det sista kommandot i filen är själva kommandot history, dvs,skalet har lagt till history i kommandologgen innan det verkligen körs. I själva verket lägger skalet alltid till kommandon i loggen innan det kör dem. Varför tror du att den gör detta?

    Lösning

    Om ett kommando orsakar att något kraschar eller hänger, kan det vara bra att veta vilket kommando det var, för att kunna undersöka problemet.Om kommandot bara registreras efter att det har körts, skulle vi inte ha något register över det senaste kommandot som kördes i händelse av en krasch.

    I praktiken utvecklar de flesta människor skalskript genom att köra kommandon vid skalprompten några gånger för att försäkra sig om att de gör rätt, och sedan spara dem i en fil för att återanvända dem.Detta arbetssätt gör det möjligt för människor att återanvända vad de upptäcker om sina data och sitt arbetsflöde med ett enda anrop till historyoch lite redigering för att städa upp utgången och spara den som ett skalskript.

    Nelle’s Pipeline: Skapa ett skript

    Nelles chef insisterade på att alla hennes analyser måste vara reproducerbara. Det enklaste sättet att fånga alla steg är i ett skript.

    Först återvänder vi till Nelles datakatalog:

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

    Hon kör editorn och skriver följande:

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

    Hon sparar detta i en fil som heter do-stats.shså att hon nu kan göra om det första steget i sin analys genom att skriva:

    $ bash do-stats.sh NENE*.txt

    Hon kan också göra så här:

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

    så att resultatet bara är antalet bearbetade filer istället för namnen på de filer som bearbetades.

    En sak att notera om Nelles skript är att det låter personen som kör det bestämma vilka filer som skall behandlas.Hon kunde ha skrivit det som:

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

    Fördelen är att detta alltid väljer rätt filer: hon behöver inte komma ihåg att utesluta ”Z”-filerna.Nackdelen är att det alltid väljer just dessa filer – hon kan inte köra det på alla filer (inklusive ”Z”-filerna), eller på ”G”- eller ”H”-filerna som hennes kollegor i Antarktis producerar, utan att redigera manuset.Om hon vill vara mer äventyrlig kan hon ändra skriptet så att det kontrollerar om det finns kommandoradsargument och använder NENE*.txt om det inte finns några sådana.Detta innebär naturligtvis ytterligare en avvägning mellan flexibilitet och komplexitet.

    Variabler i skalskript

    I katalogen molecules kan du tänka dig att du har ett skalskript med namnet script.sh som innehåller följande kommandon:

    head -n  tail -n  

    I katalogen molecules skriver du följande kommando:

    bash script.sh '*.pdb' 1 1

    Vilken av följande utdata skulle du förvänta dig att se?

    1. Alla rader mellan den första och sista raden i varje fil som slutar på .pdbi katalogen molecules
    2. Den första och sista raden i varje fil som slutar på .pdb i katalogen molecules. katalog
    3. Den första och sista raden i varje fil i molecules katalogen
    4. Ett fel på grund av citattecken runt *.pdb

    Lösning

    Det rätta svaret är 2.

    De särskilda variablerna $1, $2 och $3 representerar de kommandoradsargument som ges till skriptet, så att de kommandon som körs är:

    $ 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

    Skalet expanderar inte '*.pdb' eftersom det är inneslutet av citationstecken.Det första argumentet till skriptet är '*.pdb' som expanderas i skriptet med head och tail.

    Finn den längsta filen med en given filändelse

    Skriv ett skalskript som heter longest.sh som tar namnet på en katalog och ett filnamnstillägg som argument och skriver ut namnet på den fil med flest rader i den katalogen med det tillägget. Till exempel:

    $ bash longest.sh /tmp/data pdb

    skulle skriva ut namnet på den fil .pdb i /tmp/data som har flest rader.

    Lösning

    # 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

    Den första delen av pipelinen, wc -l /*. | sort -n, räknar raderna i varje fil och sorterar dem numeriskt (största sist). Om det finns mer än en fil, ger wc också ut en sista sammanfattningsrad som anger det totala antalet rader i alla filer. Vi använder tail-n 2 | head -n 1 för att kasta bort denna sista rad.

    Med wc -l /*. | sort -n | tail -n 1 ser vi den slutliga sammanfattningsraden: vi kan bygga upp vår pipeline i bitar för att vara säkra på att vi förstår utgången.

    Skriptläsningsförståelse

    För den här frågan ska vi återigen titta på katalogen data-shell/molecules.Denna innehåller ett antal .pdb-filer utöver eventuella andra filer som du kan ha skapat.Förklara vad vart och ett av följande tre skript skulle göra när de körs sombash script1.sh *.pdb, bash script2.sh *.pdb respektive bash script3.sh *.pdb.

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

    Lösningar

    I varje fall expanderar skalet jokertecknet i *.pdb innan den resulterande listan med filnamn skickas som argument till skriptet.

    Skript 1 skulle skriva ut en lista över alla filer som innehåller en punkt i namnet.Argumenten som skickas till skriptet används faktiskt inte någonstans i skriptet.

    Skript 2 skulle skriva ut innehållet i de tre första filerna med filändelsen .pdb., och hänvisar till det första, andra respektive tredje argumentet.

    Skript 3 skulle skriva ut alla argument till skriptet (dvs. alla .pdb-filer), följt av .pdb. $@ hänvisar till alla argument som ges till ett skalskript.

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

    Felsökning av skript

    Antag att du har sparat följande skript i en fil som heter do-errors.shi Nelles north-pacific-gyre/2012-07-03 katalog:

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

    När du kör det:

    $ bash do-errors.sh NENE*.txt

    utgången är tom.För att ta reda på varför kör du skriptet på nytt med alternativet -x:

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

    Vad visas i utmatningen?Vilken rad är ansvarig för felet?

    Lösning

    Optionen -x gör att bash körs i felsökningsläge.Detta skriver ut varje kommando när det körs, vilket hjälper dig att hitta fel.I det här exemplet kan vi se att echo inte skriver ut något. Vi har gjort ett fel i namnet på loopvariabeln och variabeln datfile existerar inte och returnerar därför en tom sträng.

    Nyckelpunkter

    • Spara kommandon i filer (vanligtvis kallade skalskript) för att kunna återanvända dem.

    • bash kör de kommandon som sparats i en fil.

    • $@ hänvisar till alla kommandoradsargument i ett skalskript.

    • , osv, hänvisar till det första kommandoradsargumentet, det andra kommandoradsargumentet osv.

    • Placera variabler inom citationstecken om värdena kan innehålla blanksteg.

    • Att låta användarna bestämma vilka filer som ska bearbetas är mer flexibelt och mer konsekvent med inbyggda Unix-kommandon.

    Lämna ett svar

    Din e-postadress kommer inte publiceras.