Shellscript, föreläsningsanteckningar

2000

Originaltext: Henrik Lindgren

2001

Ändringar: Thore Husfeldt

Shell

Shellet — skalet eller kommandotolken på svenska — är det program som tolkar det man skriver i terminalfönstret. Det bestämmer vad som skall hända.

Det finns ett antal olika shell. Några exempel är:

De skiljer sig lite åt i funktionalitet. Det shell vi kommer att ta upp är C shell (och TC shell) eftersom det är standard på elevdatorsystemet.

Shellet fungerar som ett helt vanligt program och det går bra att starta ett genom att skriva dess namn: csh. Även om det ser ut som om inget har hänt så har ett nytt shell startats ovanpå det gamla. När man avslutar det nya shellet med exit kommer man tillbaks till det gamla shellet.

Variabler

Unix behöver hålla reda på en hel del information om hur saker och ting skall utföras. Det kan till exempel vara hur prompten ser ut eller var i filträdet som kommandona ligger. Mycket av den informationen kan vara bra att kunna ändra på som användare för att det ska passa ens egna behov. Därför lagrar man informationen i så kallade variabler. Det finns två typer av variabler: Omgivningsvariabler och shellvariabler.

Omgivningsvariabler

Omgivningsvariabler används mestadels av olika program för att hålla reda på diverse inställningar. Som exempel har vi kommandot man som använder sig av ett annat program för att visa texten i manualbladen. Vanligtvis används programmet more för detta. Men man kan lätt ändra detta genom att ändra i omgivningsvariabeln PAGER. Om man vill att less skall användas när man kör man skriver man setenv PAGER less. Med kommandot setenv ändrar man innehållet i en omgivningsvariabel. Den generella syntaxen för att använda setenv är setenv variabel värde. Första argumentet till setenv är den omgivningsvariabel man vill ändra, och andra argumentet är det värde man vill att den skall få. Vill man ta bort innehållet i en omgivningsvariabel används kommandot unsetenv. Det används på följande sätt: unsetenv variabel.

Om man vill veta vilket värde en omgivningsvariabel har så kan man skriva echo $omgivningsvariabel. $-tecknet talar om för shellet att man vill ha värdet i en variabel. På detta sätt kan vi till exempel titta vad variabeln PAGER innehåller genom att skriva echo $PAGER.

Vigtigt för javaprogrammerare: Omgivningsvariabeln CLASSPATH listar de kataloger (mellan :), som java-interpretatorn genomsöker för att hitta en klass. Här är till exempel min nuvarande CLASSPATH:

  echo $CLASSPATH
  .:/home/thore/src/src/jdk1.1:/home/thore/lib/java:/home/thore/kurser/dat119/lib:
Observera att det aktuella kataloget ‘.’ finns i min CLASSPATH.

Omgivningsvariablerna är unika för varje process. Varje kommandofönster är en separat process och har därför sina egna omgivningsvariabler. Detta betyder att om man ändrat t.ex. PAGER och sedan startar en ny xterm från menyn så kommer ändringen inte att märkas. Däremot kommer det att märkas om man startar en xterm genom att skriva xterm i kommandofönstret. Alla omgivningsvariabler ärvs när man startar ett nytt shell inifrån ett annat.

Omgivningsvariabler stavas alltid med stora bokstäver.

Shellvariabler

Shellet är som sagt det program som bestämmer vad som skall hända när man skriver en viss sak. Om man till exempel skriver ett programnamn så kommer det programmet att köras. För att shellet ska veta var programmet ligger använder den sig av pathen. Pathen är en så kallad shellvariabel.

Man använder shellvariabler på samma sätt som omgivningsvariabler, det vill säga genom att sätta ett $-tecken framför. Man kan till exempel skriva echo $shell, vilket kommer att ge utskriften /bin/tcsh om du använder tsch som ditt shell. Däremot sätts inte värdet på en shellvariabel på samma sätt som en omgivningsvariabel. I stället för kommandot setenv används kommandot set, vilket sker på följande sätt: set shellvariabel=värde. Notera att man måste ha ett = tecken mellan variabelns namn och dess värde. Kommandot unset finns också och används på samma sätt som unsetenv.

Shellvariabler existerar bara i shellet och andra program kan inte komma åt dem. De är också helt unika för varje shell. Om man ändrar en shellvariabel i ett shell och sedan startar upp ett annat shell kommer ändringen inte att märkas, inte ens om det nya shellet startas inifrån det gamla.

Summering

Omgivningsvariabler Shellvariabler
Stavning stora bokstäver små bokstäver
Ärvs till en nystartad xterm Ja Nej

Alias

Kommandot alias gör att man kan lägga in alternativa kommandonamn. Man kan till exempel skapa ett nytt kort namn för kommandon som man skriver ofta. Om man, i stället för att behöva skriva less, vill kunna skriva bara m skriver man: alias m less. När man skapar ett nytt kortkommando på detta sätt kallas det att man skapar ett alias. Kommandot alias används alltså på följande sätt: alias namn kommando. Det första argumentet till alias talar om vad man vill att det nya aliaset ska heta. Det går bra att använda ett redan upptaget namn, till exempel man, men man bör vara lite försiktig med det. Det andra argumentet talar om vad kortkommandot skall göra.

Även om man skall se upp med att skapa alias med namn som redan är upptagna kan det ibland vara praktiskt. Antag att man vill att kommandot ls alltid skall köras med flaggorna -aF. Då kan man skriva på följande sätt: alias ls ls -aF. Det fungerar faktiskt! Detta skapar ett nytt alias ls som utför kommandot ls -aF.

För att lista alla alias som finns i det nuvarande shellet skriver man bara alias utan några argument. För att se vilket kommando ett visst alias utför skriver man alias namn där namn är namnet på aliaset. Ett exempel kan vara alias vf som på vissa datorsystem ger utskrifter cd. (Varför är detta alias bra att ha?)

Om man vill ta bort ett alias så skriver man unalias namn.

Navigering

Vi har redan bläddrat i listen av tidigare kommandon med pil-upp och pil-ner. Här är ytterligare några shell-tricks som gör livet lättare:
history
visar listan av tidigare kommandon i det aktuella shellet
!n
upprepar tidigare kommandon enligt history-listan. Om den aktuella kommandon är nummer 13 så kommer man åt nummer 11 både med !11 och !-2. Observera !! för !-1.
!n:m
Väljer argument i tidigare kommandon. Till exempel är !11:1 det första argumentet i kommando 11. Observera !!:1, som man snabbt kan bli beroende av, och som ytterligare kan förkortas !:1.
dirs, pushd, popd
Shellet sparar en stack av katalog, som man läser med dirs. pushd lägger sitt argument på stacken, och popd byter till staktoppen. Detta används ofta i följande rutin: pushd . sparar den aktuella positionen. Därefter hoppar man till någon annan katalog med cd. Är man klar där, så hoppar men tillbaka med popd. Observera doch cd -, som gör samma sak lite enklare!

Här är ett icke-trivialt exempel som användar en bra bit av de ovan introducerade konstruktionerna:

javac BounceTheBall.java
java `basename !-1:1 .java`

Vi vill gärna slippa ange “BounceTheBall” i java-anropet, eftersom vi precis skrev namnet på filen och shellet borde kunne hitta det själv. Andra raden betyder altså “java ‘senaste radens argument utan suffix’”.

Unix-kommandom basename bla.suffix .suffix tar bort suffix från sitt första argument, vilket precis är vad vi behöver för att göra om “BounceTheBall.java” till “BounceTheBall”.

Observera backåtfnuttarna till att exekvera basename-kommandot.

Slutligen skapar vi ett alias för denna kommandon (och testar den):

alias run 'java `basename \!-2:1 .java`'
java BounceTheBall.java
run

Enkelfnuttarna runt kommandon behövs för att förhindra shellet att exekvera basename i samma ögonblick som alias. Av någralunda samma anledning måste ! förhindras i att bli ersatt med senaste kommando. Slutligen är !-1 nu alltid kommandon run, så vi behöver kommandon precis innan detta.

Omdirigering

Som vi tidigare sett kan det ofta vara praktiskt att kunna skicka all utdata från ett program in till ett annat genom rörledningar. Ibland kan det också vara praktiskt att kunna spara all utdata från ett program till en fil. Detta görs genom att "dirigera om" texten som hamnar på skärmen till en fil.

Antag att du har en fil, telefon.txt, med dina kompisar samt deras telefonnummer. Nu skulle skulle du vilja ha en fil telefonsort.txt som innehåller samma namn och nummer som telefon.txt men är sorterad. Att sortera telefon.txt är enkelt, vi skriver bara sort telefon.txt. Men då får vi bara upp resultatet på skärmen. För att dirigera om texten skriver vi i stället sort telefon.txt > telefonsort.txt. Tecknet > talar om för shellet att det som skulle kommit ut på skärmen i stället skall dirigeras om till filen telefonsort.txt. Om det redan finns en fil som heter telefonsort.txt kommer denna att skrivas över. Om man inte vill att filen ska skrivas över kan man använda >> istället för >. Då kommer omdirigeringen att läggas till i slutet på filen.

Ibland kan man även behöva "dirigera in" innehållet i en fil till ett program, även om det är ganska sällsynt. Innehållet i filen kommer då att matas in till programmet som om man hade skrivit in det med hjälp av tangentbordet. Detta kan t.ex. användas i stället för att ange en infil direkt till ett program, även om det blir mer omständigt att dirigera om. För att dirigera in en fil används tecknet <. Exemplet ovan där vi ville sortera filen telefon.txt skulle man kunna skriva så här: sort < telefon.txt. Om man fortfarande vill att resultatet ska hamna i filen telefonsort.txt skriver man som följer: sort < telefon.txt > telefonsort.txt. Notera att indirigeringen kommer före utdirigeringen.

Ett exempel på ett program där det kan vara användbart är om man vill använda programmet mail för att skicka ett mail t.ex. i ett script. Programmet mail har nämligen ingen flagga för att läsa in text från en fil. Om man vill skicka ett mail till janne@balja.com där mailtexten finns i filen mailtext så skriver man mail janne@balja.com < mailtext.

Specialtecken och fnuttar

Som vi har sett kan shellet göra en massa bra saker, t.ex. finns där variabler, vi kan dirigera om utdata från ett program lite varstans och så vidare. För varje sådan finess används en speciell symbol som då får en särskild betydelse för shellet. Detta kan ställa till med problem ibland. Antag att vi har en fil som heter money.txt och vi vill söka efter strängen $100 i den med hjälp av kommandot grep. Det naturliga vore förstås att skriva grep $100 money.txt. Men då kommer shellet att tro att $100 refererar till variabeln 100 eftersom vi har ett $-tecken. För att tala om för shellet att det inte ska tolka $-tecknet som början på en variabel kan vi skriva ett \ (backslash) framför. Då skulle det se ut såhär:

grep \$100 money.txt

Backslash kan användas för att upphäva betydelsen för vilket specialtecken som helst, till och med blanksteg. Man kan alltså söka efter strängen "Johnny Logan" i en fil genom att skriva grep Johnny\ Logan winners.txt. Det kan dock bli lite jobbigt i längden att skriva en massa backslash hela tiden. Därför kan man istället sätta " (citationstecken) eller ' (apostrof) runt den text man vill upphäva betydelsen av specialtecknen i. Detta innebär att man kan skriva grep "Johnny Logan" winners.txt eller grep 'Johnny Logan' winners.txt. Vad är då skillnaden mellan apostrof och citationstecken? Om man skriver en variabel, t.ex. $PAGER, kommer den att tolkas som en variabel när man har citationstecken och som texten $PAGER när man använder apostrof.

Ibland skulle man vilja att shellet gjorde det motsatta mot vad vi gjorde ovan, att köra kommandon där man egentligen skulle skrivit en sträng. Ett vanligt förekommande exempel på detta är när man använder setenv. Ofta vill man att den variabel man sätter ska sättas till utdatan från ett program. Detta kan man göra på följande sätt:

setenv VARIABEL `PROGRAM`

Man använder alltså ` (accent) för att markera en textsnutt som man vill att shellet ska köra. Till exempel kan vi vilja lagra dagens datum i variabeln DATUM. För att göra detta skriver vi setenv DATUM `date`.

Kommentar: Tecknen ", ' och ` brukar i folkmun kallas dubbelfnutt, enkelfnutt och bakåtfnutt.

Scripts

När man vill göra lite mer avancerade uppgifter kan man behöva skriva flera rader och det kan vara lite bökigt att komma ihåg precis hur man skall skriva. För att underlätta detta kan man samla kommandon i en fil, ett så kallat script. Ett enkelt exempel:

Skapa en fil hej med följande innehåll:

echo Hej på dig $LOGNAME!
echo Ha det bra!

Ett vanligt fel man gör är att glömma trycka return efter sista raden, då kommer sista raden inte att utföras. Ha därför alltid som vana att avsluta scriptfilerna med return. För att starta filen med C shell skriver man csh hej. Men det blir lite omständigt i längden att skriva csh så då kan man lägga till #!/bin/csh först i filen och sedan göra den exekverbar genom att skriva chmod +x hej. Nu kan filen startas genom att helt enkelt skriva hej. Raden #!/bin/csh talar om vilket shell som skall användas för att köra shellscriptet. Den går att utelämna och om man gör det så används ett shell av samma typ som det man för närvarande använder för att köra scriptet. Detta är dock inte att rekommendera eftersom många script bara fungerar i vissa shell, och man inte vet vilket shell scriptet startas ifrån.

Source

När man kör sina shellscript så startas alltid ett nytt shell som utför kommandona i filen. Detta är inte alltid vad man vill. Antag att ni skrivit en script som ni döpt till mittscript och som har följande innehåll:

alias l ls -aF
alias grodor 'ls | grep \*groda\*'

Om ni sedan kör mittscript och försöker använda de alias som definieras i scriptet så kommer de inte att fungera. Detta beror på att ett nytt shell startas upp när man kör scriptet och de förändringar man gör i shellet, till exempel skapa nya alias, kommer bara att märkas i det nystartade shellet och inte i det ursprungliga. För att råda bot på detta finns kommandot source. Det används på följande vis: source script. source gör så att det inte startas upp ett nytt shell när man kör scriptet. Istället används det nuvarande shellet. Om ni skriver source mittscript så kommer ni sedan att kunna använda de alias som definierades i scriptet. Man bör dock notera att source inte alltid fungerar om scriptet börjar med raden #!/bin/csh.

Argument

När man kör vanliga kommandon så kan man ofta ge dem olika argument för att tala om vad man vill att de skall göra. Ett exempel: more hejsan.txt där man talar om för kommandot more att man vill att det skall visa filen hejsan.txt. Det går även bra att skriva shellscript som kan hantera argument. För att kunna använda argumenten inuti scriptet finns ett antal fördefinierade shellvariabler för detta. Deras namn är väldigt lätta att komma ihåg, namnet på variabeln som refererar till första argumentet heter 1, variabeln för andra argumentet heter 2, och så vidare. Variabeln 0 refererar till programnamnet.

Låt oss titta på ett exempel. Betrakta följande script:

echo Hejsan $1
echo Detta program heter $0

Låt oss säga att vi har sparat scriptet som hej. Tanken är att man skall köra scriptet och skriva sitt namn som första argument. Då kommer scriptet att skriva ut en hälsningsfras och tala om vad det är sparat som. Och mycket riktigt, om vi kör scriptet med hej Roberto så kommer följande att skrivas ut:

Hejsan Roberto
Detta program heter hej

Ibland behöver man komma åt alla argument på en gång. Dessa finns i en speciell variabel som heter *.

Felsökning

När man sitter och skriver sina script så kommer man någon gång att skriva fel och scriptet gör inte det man tänkt sig. Ibland kan det vara lätt att se vad som är fel men titt som tätt får man fel som är mycket kryptiska. Då kan det vara bra att få hjälp med att felsöka sitt script. tcsh har en flagga som är tänkta att underlätta felsökning, nämligen flaggan -x. Om man startat shellet med denna flagga kommer alla kommandon som utförs att skrivas ut innan de körs. Om vi till exempel kör scriptet från föregående stycket på följande vis: tcsh -x hej Roberto kommer följande att skrivas ut på skärmen:

echo Hejsan Roberto
Hejsan Roberto
echo Detta program heter hej
Detta program heter hej

Den första raden skriver visar första raden i scriptet. Men den stämmer ju inte med första raden i scriptet! Det beror på att i stället för att skriva ut vad en variabel heter (i vårt fall $1) så skrivs innehållet i variabeln ut (Roberto). Detta kan tyckas märkligt men det är väldigt användbart att kunna se variablerna innehåll samtidigt som scriptet kör. Den andra raden visar utskriften av den första raden. Rad tre och fyra fungerar på samma sätt som rad ett och två.

Ett större exempel

Tänk dig att ni i en kurs skall lämna in en rapport som skall vara minst tio sidor lång. Både du och alla dina kurskompisar har väntat till sista kvällen med att göra färdigt rapporten. Framåt midnatt börjar ni bli klara med era rapporter, men problemet är att alla vill skriva ut nästan samtidigt. För att ni inte ska behöva sitta hela natten och vänta på att er utskrift ska bli klar skulle du vilja skriva ut på den skrivare som har minst antal jobb i skrivarkön. Men eftersom det finns ett antal skrivare och det tar ganska lång tid att kolla vilken skrivare som är bäst att skriva ut på kan fler personer har börjat skriva ut medan du räknade. Vad du skulle vilja ha är ett kommando som automatiskt väljer att skriva ut på den skrivare som har minst antal jobb i skrivarkön.

Det första scriptet måste göra är att ta reda på vilka skrivare som finns. Detta kan göras med kommandot lpq genom att ge det flaggan -all.

På ludat-systemet finns inte lpq. För att titta på skrivarkön används lpstat, som tyvärr har ett helt annat outputformat.

Skriver man lpq -all får man upp skrivarköerna för alla skrivare på systemet. Men vi är bara intresserade av längden på skrivarkön, eftersom vi vill välja skrivaren med kortast kö. För detta ändamål finns en annan flagga till lpq, flaggan -s. Om man skriver lpq -all -s får man en utskrift som liknar följande:

c1@animal  8 jobs
c2@animal  0 jobs
c3@animal  1 job
c4@animal  3 jobs
c5@animal  3 jobs
c6@animal  4 jobs
c7@animal  6 jobs
c8@animal  7 jobs
c0@animal  8 job
cstest@animal  7 jobs

Den första kolumnen anger skrivarens namn och siffran i mittenkolumnen anger hur många jobb som finns i skrivarkön. Ett fiffigt sätt att välja ut den skrivare med minst antal jobb i skrivarkön är att sortera listan ovan med avseende på antalet jobb och sedan välja ut den första skrivaren i den sorterade listan. Vi börjar med att koncentrera oss på hur vi ska sortera listan.

I Unix finns kommandot sort som kan lösa de flesta sorteringsproblem. Låt oss betrakta lista ovan som vi vill sortera. Vi vill att listan ska sorteras med avseende på andra kolumnen och att den kolumnen ska tolkas som siffror. För att tala om detta för sort ger man flaggan +1n. Ettan talar om att det är andra kolumnen (sort börjar räkna på noll) och n:et talar om att kolumnen ska tolkas numeriskt. För att köra sort på listan ovan kan vi använda en rörledning, vilket skulle ge följande kommando: lpq -all -s | sort +1n. Det ger följande utskrift:

c2@animal  0 jobs
c3@animal  1 job
c4@animal  3 jobs
c5@animal  3 jobs
c6@animal  4 jobs
c7@animal  6 jobs
c8@animal  7 jobs
cstest@animal  7 jobs
c0@animal  8 job
c1@animal  8 jobs

För att få den minst belastade skrivaren räcker det nu med att vi tar den första raden i den sorterade listan. Detta kan göras med kommandot head -1. Flaggan -1 talar om att vi bara vill ha en rad. Vår kommandosekvens har vuxit till: lpq -all -s | sort +1n | head -1 och det ger utskriften:

c2@animal  0 jobs

Nu vet vi vilken skrivare som vi vill skriva ut på. Men raden vi får ut innehåller inte bara skrivarens namn utan också en del skräptecken. För att få bort dem använder vi kommandot cut. cut används generellt sett för att välja ut vissa kolumner ur en rad. I vårt fall vill vi välja ut den första kolumnen. Men vi måste också tala om för cut vilket tecken vi använder som avskiljare mellan kolumner, vilket i vårt fall är vanligt blanktecken. För att beskriva detta för cut skriver vi cut -d' ' -f1. Flaggan -d talar om vilken avskiljare (delimiter) vi använder och med -f anges vilken kolumn vi vill åt. Vår kompletta kommandosekvens för att ta reda på vilken printer vi vill skriva ut på är följande:

lpq -all -s | sort +1n | head -1 | cut -d' ' -f1

vilket ger följande utskrift:

c2@animal

För att spara skrivarnamnet lagrar vi det i en omgivningsvariabel på följande sätt:

setenv BESTPRINTER `lpq -all -s | sort +1n | head -1 | cut -d' ' -f1`

Notera att vi använt `-tecken runt vår kommandosekvens. Detta måste vi göra för att sekvensen skall utföras. Hade vi satt någon annan typ av fnutt skulle variabeln BESTPRINTER innehållit strängen lpq -all -s | sort +1n | head -1 | cut -d' ' -f1 i stället.

För att skriva ut på skrivaren använder vi kommandot lpr med flaggan -P för att specificera vilken skrivare vi vill skriva ut på. Men vilka filer ska vi skriva ut? Vi kan låta vårt script ta ett argument som anger vilken fil som skall skrivas ut. Vårt script kommer då att se ut på följande sätt:

#!/bin/tcsh

setenv BESTPRINTER `lpq -all -s | sort +1n | head -1 | cut -d' ' -f1`

lpr -P$BESTPRINTER $1

Äntligen har vi ett script som fungerar! Men det är inte riktigt tillfredsställande på alla punkter. Till att börja med så kanske vi vill skriva ut flera filer på en gång. Detta är lätt åtgärdat. Istället för att använda $1 kan vi använda $* som referar till alla argument i scriptet. Om vi kallar vårt script superlpr kan vi allstå nu skriva superlpr fil1 fil2 fil3 och alla tre filerna kommer att skrivas ut.

En sak som är lite mer frustrerande är att vi inte får reda på vilken skrivare det är som vi skriver ut på. Det vore trevligt om vårt lilla script kunde tala om det för oss, så att vi kan hitta våra papper. Ett enkelt sätt att lösa detta på är att lägga till echo $BESTPRINTER i slutet av scriptet. Men vi kan få en lite trevligare utskrift med hjälp av lpq. Gör vi lpq -P$BESTPRINTER så kommer andra raden att ge oss en fin beskrivning av vilken sorts skrivare det är och var den finns belägen. För att välja ut andra raden kan vi använda oss av kommandona head och tail genom att lägga till | tail +2 | head -1 efter lpq. Vårt slutgiltiga script blir då:

#!/bin/tcsh

setenv BESTPRINTER `lpq -all -s | sort +1n | head -1 | cut -d' ' -f1`

lpr -P$BESTPRINTER $*

lpq -P$BESTPRINTER | tail +2 | head -1