Kommandotolken Bash

En kommandotolk, skal (shell), används för att ge datorn instruktioner om vad den skall göra. Idag är det vanligaste sättet att arbeta med en dator med ett grafiskt gränssnitt, GUI (Graphical User Interface). I ett GUI klickar användaren med en muspekare på vad användaren vill att datorn skall göra. I ett skal skriver användaren sina kommandon i en textbaserad miljö, som kallas CLI (Command Line Interface). Ett skal är alltså ett kommandoradsbaserat gränssnitt mot användaren.

Den första kommandotolken för UNIX skrevs av Steven Bourne 1974. Kommandotolken hette Bourne Shell och är kanske mer känd som sh. Bourne Shell har satt standarden för hur kommandotolkar skall se ut och fungera, bland annat har vi fått kommandoprompten $ från Bourne Shell.

Bash är troligen den vanligaste kommadotolken som används i operativsystemet Linux. Bash, som är en förbättring av Bourne Shell, är en förkortning av Bourne Again Shell. Bash är resultatet av GNU-projektets arbete med att förbättra Bourne Shell. Bash är helt POSIX-kompatibelt. I exemplet nedanför ser vi hur Bash är startad och väntar på kommandon från användaren.

jonas@ubuntu:~$
jonas
är användarnamnet på den användare som är inloggad
ubuntu
är namnet på den dator användaren är inloggad på
tildetecknet (~)
anger vilken katalog användaren är i, tilde är en förkortning för användarens hemkatalog och dollartecknet är kommandoprompten, det är efter kommandoprompten användaren kan skriva sina kommandon.

Vi kan arbeta med Bash interaktivt, det vill säga vi skriver våra kommandon direkt på kommandoraden, eller genom att skriva skript. Ett skript är en samling av instruktioner som sparats i en vanlig textfil. När vi kör textfilen med Bash kommer den utföra de instruktioner textfilen innehåller. Vi kommer börja att jobba med Bash interaktivt för att lära oss hur Bash fungerar och vad vi kan göra med Bash. När vi kommit en bit på vägen börjar vi skapa våra första skript.

När Bash startas letar den efter ett par konfigurationsfiler som den läser in (om de finns) och kör. Ordningen på vilka filer den läser in är:

  • Om filen /etc/profile finns läses den in. Här finns systemglobal konfiguration. Filen läser in filen /etc/bash.bashrc. Filen läser också in alla filer som finns i katalogen /etc/profile.d/ och har filändelsen .sh.
  • Om filerna ~/.bash_profile och/eller ~/.bash_login finns kommer Bash läsa in den första filen den hittar av dessa. Om de inte finns kommer ~/.profile läsas in av Bash. I Ubuntu är det ~/.profile som skapas för varje användare och används. ~/.profile läser in filen ~/.bashrc. Filen ~/.bashrc i sin tur läser in filen ~/.bash_aliases om den finns.

Vill vi skapa egna systemglobala miljövariabler eller funktioner för Bash är rekommendationen att skapa filen /etc/profile.d/custom.sh och lägga allt där. Detta för att vi skall undvika att nästa uppdatering av paketet bash skulle kunna skriva över våra ändringar i filerna.

Om vi vill skapa egna variabler och funktioner för vår egen användare är ~/.bashrc en bra plats att göra det i. Lägg till variabler och funktioner i slutet av den filen. Är det alias vi vill lägga till är filen ~/.bash_aliases en bra plats att lägga in dem i.

.bash_profile (och .bash_login , .profile) läses bara in när du loggar in i systemet. Startar du en ny instans av Bash när du är inloggad redan, till exempel genom att skriva bash på kommandoraden, läser Bash enbart in filen .bashrc.

När du loggar ut läser Bash filen ~/.bash_logout om den finns. Där kan vi till exempel lägga in kommandot clear så att Bash raderar allt på skärmen innan vi loggar ut.

Kommandoprompten

Kommandoprompten styrs av två miljövariabler: PS1 och PS2. PS1 är den kommandoprompt du normalt ser (se exemplet ovanför), medan miljövariabeln PS2 används för den kommandoprompt som visas på andra raden. PS2 är normalt ett >-tecken.

Exempel:

jonas@ubuntu:~$ echo \
> arne
arne
jonas@ubuntu:~$

Vi kan ändra kommandopromptarna så de blir som vi vill.

I tabellen nedanför visar vi ett par av de styrkoder vi kan använda för att anpassa vår kommandoprompt.

Nu vill vi skapa en kommandoprompt som ser ut så här:

[jonas@ubuntu (08:06:10) tmp ] >>

Först har vi användarnamnet på inloggad användare (jonas), sedan kommer datorns namn (ubuntu). I parantesen visas hur mycket klockan är (08:06:10) och slutligen kommer vilken katalog vi befinner oss i (tmp). För att skapa en kommandoprompt som ser ut så här skriver vi:

$ PS1="[\u@\h (\t) \W ] >> "
Styrkod Förklaring
\a ASCII bell (ett kort pip)
\d Datumet i veckodag månad datum-format, till exempel: mån dec 25
\e ASCII escape tecknet
\h Datornamnet (hostname) fram till den första punkten
\H Datornamnet (hostname)
\j Antalet processer (jobb) som hanteras av kommandotolken
\1 Vilken terminal är vi inloggade på?
\n Newline, ny rad
\r Carriage return, radbrytning
\s Skalets namn
\t Klockan i 24-timmarsformat (TT::SS)
\T Klockan i 12-timmarsformat (TT::SS)
\@ Klockan i 12-timmarsformat med am/pm
\u Användarnamnet på inloggad användare
\v Versionen på Bash
\V Versionen på Bash med patchnivå
\w Nuvarande katalog (/usr/share)
\W Basnamnet på nuvarande katalog (share)
! Historynumret på det kommando som kommer att utföras.
# Nummer som visar hur många kommandon du utfört i det nuvarande skalet.
\$ Om användaren är UID 0 (root) visas ett #-tecken, annars visas ett $-tecken.
\nnn Visar tecknet som motsvaras av det oktala numret nnn.
\\ Ett backslash-tecken
[ Påbörja en sekvens med icke utskrivningsbara tecken.
] Avsluta en sekvens av icke utskrivningsbara tecken.

Vi kan ange färger för vår kommandoprompt, vi måste dock tänka på att se till att escapa (vilket görs med backslash \, som också kallas escapetecknet) färgkoderna korrekt - annars fungerar det inte. Leta upp den färg du vill använda i tabellen för färgkoder här nedanför och skriv den som \[\033[0;32m\] (väljer grön färg), allt som kommer efter det kommer skrivas ut i grönt. För att byta färg igen skriver du den nya färgkoden och sedan vad du vill skriva ut.

Ett exempel:

$ PS1="\[\033[0;32m\]\u\[\033[1;31m\]@\[\033[1;32m\]\h\[\033[34;1m\]\w\[\033[0;37\
m\] >

Vill du få tillbaka din vanliga prompt igen kan du logga ut och in igen, eller skriva:

$ source ~/.bashrc

För att göra ändringarna permanenta (dvs de aktiveras varje gång du loggar in) lägger du till dem i filen .bashrc i din hemkatalog.

Färg Färgkod Färg Färgkod
Svart 0;30 Mörkgrå 1;30
Röd 0;31 Ljusröd 1;31
Grön 0;32 Ljusgrön 1;32
Brun 0;33 Gul 1;33
Blå 0;34 Ljusblå 1;34
Lila 0;35 Ljuslila 1;35
Turkos 0;36 Ljusturkos 1;36
Ljusgrå 0;37 Vitt 1;37
An icon indicating this blurb contains information

Förutom miljövariablerna PS1 och PS2 finns det en miljövariabel för PS3 och PS4 också. De används sällan, men finns där. Sök i manualsidan för Bash, man bash, för att se vad de används till.

Reserverade ord

Bash har, precis som andra kommandotolkar, reserverade ord. Ord som används för speciella ändamål av Bash. I Bash kan du använda orden som variabelnamn (det går inte i alla kommandotolkar), men du bör undvika det för att slippa bli förvirrad när fel uppstår.

|———-|————|—————|————-| | ! | case | coproc | do | | done | elif | else | easc | | fi | for | function | if | | in | select | then | until | | while | { } | time | [[ ]] |

Alias i Bash

Många kommandon använder sig av argument och ibland kommer vi på oss själva att alltid använda samma argument till kommandot. Vore det inte ganska smart att kunna skapa ett nytt kommando som gör att vi slipper skriva in kommandot och argumenten varje gång? Genom att skapa ett alias för ett kommando kan vi anropa kommandot med argument utan att själva behöva skriva argumenten. Ett sådant kommando kan vara ls som vi ofta använder med argumentet -l för att visa en detaljerad lista över filerna i en katalog.

Vi skapar ett alias för att visa en katalog med detaljer för filerna i den (kommandot ls -l). Aliaset döper vi till ll:

$ alias ll='ls -l'

Nu kan vi skriva ll istället för att skriva ls -l varje gång vi vill visa en detaljerad lista över filer i en katalog.

$ ll
totalt 12
-rw-r--r-- 1 jonas users  923 2011-12-27 11:46 pictures.php
-rw-r--r-- 1 jonas users 4454 2011-12-27 11:46 xorg.conf

För att ta bort ett alias använder vi kommandot unalias. För att ta bort aliaset ll som vi precis skapade skriver vi:

$ unalias ll

Om vi vill lista alla alias vi har tillgängliga skriver vi bara alias:

$ alias
alias cd..='cd ..'
alias dir='ls -l'
alias l='ls -alF'
alias la='ls -la'
alias ll='ls -l'

Ett alias kan heta samma sak som kommandot, vi skulle alltså kunna skapa aliaset ls som ger oss kommandot ls med argumentet -l (ls -l):

$ alias ls='ls -l'

Vi kan skapa våra alias direkt på kommandoraden eller spara dem i vår ~/.bash_aliases-fil som finns i vår hemkatalog. Om vi skapar våra alias i .bash_aliases kommer de alltid vara tillgängliga för oss när vi loggar in. För att skapa ett alias i filen .bash_aliases öppnar vi filen med en texteditor och skriver in aliaset i filen, precis som vi gjorde på kommmandoraden ovanför:

alias ls='ls -'

Om vi inte sparar våra alias i filen .bash_aliases kommer alla alias att försvinna när vi loggar ut.

Historik i Bash

Bash håller en historik på vilka kommandon vi använt. Hur många kommandon Bash skall komma ihåg bestäms av miljövariabeln HISTSIZE, vill vi se vad den är satt till kan vi skriva:

$ echo ${HISTSIZE}
1000

Alla kommandon i historiken sparas i filen .bash_history i användarens hemkatalog, om vi inte ändrat miljövariablen HISTFILE som används för att ange vilken fil historiken skall sparas i.

Vi kan söka i historiken med !-tecknet.

$ date
ons dec 27 13:43:04 CET 2011
$ !d
date
ons dec 27 13:43:07 CET 2011

Kommandot !d letar efter det senaste kommandot som använts som börjat med bokstaven d, vi skrev date precis innan så det är date som är senast använt. Om vi fortsätter och testar, kan vi skriva:

$ dir
totalt 12
-rw-r--r-- 1 jonas users  923 2011-12-27 11:46 pictures.php
-rw-r--r-- 1 jonas users 4454 2011-12-27 11:46 xorg.conf
$ !da
date
ons dec 27 13:45:36 CET 2011
$ !di
dir
totalt 12
-rw-r--r-- 1 jonas users  923 2011-12-27 11:46 pictures.php
-rw-r--r-- 1 jonas users 4454 2011-12-27 11:46 xorg.conf

Eftersom vi nu anger !da kommer inte kommandot dir köras igen, istället letar Bash i historiken efter det senaste kommandot som började på da och kör kommandot date. Efter det skriver vi !di för att köra kommandot dir igen.

Vill vi se vilket kommando history kommer köra innan den kör kommandot skriver vi:

$ history -p !d
history -p date
date

I fallet ovanför kommer ett !d att köra kommandot date. Dubbla utropstecken (!!) kör det senaste kommandot igen:

$ date
ons dec 27 13:50:38 CET 2011
$ !!
date
ons dec 27 13:50:39 CET 2011

Om vi vill köra ett kommando som vi körde för n gånger sedan skriver vi !-n:

$ !-10
ls
totalt 12
-rw-r--r-- 1 jonas users 923 2011-12-27 11:46 pictures.php
-rw-r--r-- 1 jonas users 4454 2011-12-27 11:46 xorg.conf

Vill vi se hela historiken skriver vi history. Vill vi se de senaste fem (5) kommandona i historiken skriver vi:

$ history 5
 1218  ls
 1219  dir
 1220  date
 1221  dir
 1222  history 5

Vill vi köra kommandot 1220 (date) igen skriver vi:

$ !1220
date
ons dec 27 13:55:58 CET 2011

Vill vi ta bort ett kommando från historiken använder vi kommandot history -d nummer:

 ...
 1223  date
 1224  history
$ history -d 1223
$ history 5
 1221  dir
 1222  history 5
 1223  history
 1224  history -d 1223
 1225  history 5

Vill vi tömma hela historiken använder vi kommandot history -c:

$ history -c
$ history
 227  history

Flera kommandon på samma rad

Ibland vill vi köra flera kommandon på en och samma rad. Varför sitta och vänta på att ett kommando skall köras klart innan vi kan skriva nästa? Det finns två sätt att göra detta, antagligen med ett semikolon (;) eller med två och-tecken (&&). Skillnaden på dem är att && inte kör nästa kommando om föregående inte lyckades.

Detta visas bäst med ett exempel:

$ date ; date <.>
ons dec 27 14:36:06 CET 2011
ons dec 27 14:36:06 CET 2011
$ dat ; date <.>
-bash: dat: command not found
ons dec 27 14:36:08 CET 2011
$ dat && date <.>
-bash: dat: command not found

Första gången vi kör kommandot lyckas det båda gångerna. Andra gången stavar vi fel på date (vi skriver dat) och får ett felmeddelande och det andra kommandot (date) körs. Den sista gången gör vi samma misstag (stavar fel på date) och får ett felmeddelande.

Det andra date-kommandot körs inte, eftersom vi använder &&. Om vi istället vill köra ett annat kommando om det första inte lyckas (motsatsen till &&) kan vi använda dubbla pipe-tecken (||):

$ date || echo "ett datum visades inte" <.>
ons dec 27 14:43:14 CET 2011
$ dat || echo "ett datum visades inte" <.>
-bash: dat: command not found
ett datum visades inte

Eftersom date fungerar första gången kommer vi inte skriva ut texten som kommer efter. I det andra försöket skriver vi date utan e (dat) och det blir fel, därför skrivs texten ut.

I de exempel vi har har här ovanför använder vi två kommandon, vi kan naturligtvis använda flera kommandon. Som exempel kan vi konfigurera och kompilera ett källkodspaket direkt från en kommandorad:

$ ./configure && make && make install

Här är det viktigt att vi använder &&, vi vill ju inte kompilera paketet om det inte gick att konfigurera och än mindre vill vi installera ett sådant paket i vårt system.

Ibland blir raderna med kommandon vi skriver långa, då kan vi använda backslash (\-tecknet) för att fortsätta mata in kommandon på nästa rad innan vi kör alltihopa:

$ echo "Nu skall vi skriva ut tid och datum."; \
> date; \
> echo "Det gick ju bra."
Nu skall vi skriva ut tid och datum.
ons dec 27 14:52:44 CET 2011
Det gick ju bra.

Samma sak skulle kunna skrivas som:

$ echo "Nu skall vi skriva ut tid och datum."; date; echo "Det gick ju bra."

Omdirigering och rör

Innan vi börjar med omdirigeringar måste vi ha klart för oss att Bash arbetar med tre olika standarder för in- och utmatning av information:

  • Standard in (stdin, 0)
  • Standard out (stdout, 1)
  • Standard error (stderr, 2)

Siffrorna 0, 1 och 2 kommer vi snart tillbaka till.

Bash använder som standard Standard Out (stdout) för att skicka utmatning från kommandon. Ibland vill vi ändra detta så utmatningen lagras i en fil. Säg att vi visar innehållet i en katalog och vi vill ha den listan till en textfil som vi kan arbeta med. Vi skriver:

$ ls -l 1> ls.txt

Här listar vi katalogen med kommandot ls -l, men utskriften kommer inte visas på skärmen utan skickas vidare till en fil som vi kallar ls.txt. Vi skulle kunna skriva ls -l > ls.txt också, eftersom Bash då tolkar > som 1>. Om vi nu istället vill lista en katalog som inte finns kommer vi få ett felmeddelande:

$ ls -l /tmp/a
ls: /tmp/a: Filen eller katalogen finns inte

Om vi nu skickar stdout till filen ls.txt kommer filen ls.txt vara tom, eftersom katalogen /tmp/a/ inte finns kan vi inte lista innehållet i katalogen. Vi kommer få ett felmeddelande som ser ut så här:

$ ls -l /tmp/a 1> ls.txt
ls: /tmp/a: Filen eller katalogen finns inte
$ cat ls.txt
$

Felmeddelanden från kommandon skickas till stderr och vi kan fånga upp stderr på samma sätt som vi gjorde med stdout. Istället för att använda 1 (som vi gjorde för stdout) använder vi 2 (för stderr). Vi tittar på ett exempel där vi skickar alla felmeddelanden till en fil som vi kallar ls.fel.txt:

$ ls -l /tmp/a 1> ls.txt 2> ls.fel.txt
$ cat ls.txt
$ cat ls.fel.txt
ls: /tmp/a: Filen eller katalogen finns inte
$

Nu fångar vi alltså upp stdout (1) till filen ls.txt och stderr (2) till filen ls.fel.txt.

Nu tittar vi på hur vi kan dirigera om stdin (för input). Vi börjar med att skapa en fil, som vi döper till namn.txt, med följande innehåll:

anette
therese
madelene
ann-kristin
emma
jessica
samantha
magdalena
hanna
anna
susanne
maria
malin

När vi matat in alla namn sparar vi filen och avslutar vår textredigerare. Nu använder vi kommandot cat för att visa innehållet av filen:

$ cat namn.txt

Inget konstigt händer, filens innehåll skrivs ut som vi skrev den i textredigeraren. Låt oss nu sortera filen i alfabetisk ordning:

$ sort < namn.txt

Namnen skrivs ut i alfabetisk ordning. Här använder vi omdirigeraren för stdin, <, för att läsa innehållet i filen namn.txt och skicka innehållet till kommandot sort. Eftersom kommandot sort vill skicka sin sorterade data till stdout hamnar listan på skärmen, om vi vill spara den sorterade listan i en ny fil dirigerar vi om stdout till en fil så här:

$ sort < namn.txt > namn.sorterad.txt

Ingenting händer på skärmen, men tittar vi i katalogen så skapades en fil: namn.sorterad.txt. Vi använder kommandot cat för att visa innehållet:

$ cat namn.sorterad.txt

Den sorterade listan skickades till filen namn.sorterad.txt istället för skärmen.

När vi använder > (ett tecken) skriver vi över innehållet i filen varje gång, ibland vill vi istället lägga till saker i slutet av en befintlig fil. Då kan vi inte använda >. För att lägga till innehåll i en befintlig fil, utan att skriva över filen, använder vi två stycken >> istället. >> gör att det vi dirigerar om till filen läggs till i slutet av filen:

$ sort < namn.txt > namn.sorterad.txt
$ cat namn.txt >> namn.sorterad.txt
$ cat namn.sorterad.txt

Vi kan använda rör (eng. pipe) för att skicka data vidare till andra kommandon. Du kan ha sett exempel på kommandon som ls -l | less någon gång. ls -l visar en detaljerad lista över vad en katalog innehåller och den listan kan bli ganska lång om katalogen innehåller många filer, så genom att skicka utskriften från kommandot ls till kommandot less, som visar en skärmsida i taget, får vi en bättre översikt av listan. Det är tecknet | (pipe) som genomför det magiska här. Pipe fångar upp stdout från kommandot ls och skickar texten vidare till stdin för kommanot less. Nu provar vi det här i verkligheten:

$ ls -l | less

Varför kan vi inte använda < istället? < fungerar bara för filer, inte på kommandons utmatningar. Så för att fånga upp stdout från ett kommando och skicka det till ett annat kommandos stdin använder vi |-tecknet, för att hämta innehållet från en fil och skicka det vidare till ett kommandos stdin använder vi <-tecknet. Vi skulle kunna skriva så här, om vi vill slippa använda pipe:

$ ls -l > filer.txt && less filer.txt

Visa en detaljerad lista över alla filer i katalogen och spara listan i filen filer.txt, visa sedan filen filer.txt med kommandot less. Att använda ett rör för ändamålet är betydligt effektivare än att gå via en fil.

Vi kan naturligtvis använda flera rör på en och samma rad om vi behöver det. Om vi vill visa alla filer i en katalog, sortera listan baklänges och visa den med kommandot less skriver vi:

$ ls -1 | sort -r | less

Skulle vi vilja ha den baklängessorterade listan till en fil istället för skickad till kommandot less skulle vi kunna skriva så här:

$ ls -1 | sort -r > sorterad.txt

Det går alltså att kombinera flera av < , > och | på en och samma kommandorad.

Jokertecken

När vi arbetar med filnamn och katalognamn, till exempel med kommandot ls, kan vi använda jokertecken. Det finns tre jokertecken: +*+, ? och [].

Det första jokertecknet är +*+ som betyder inget eller flera, vi använder +*+ när vi vet en del av, men inte hela, filnamnet. Vet vi början, något i mitten eller slutet av filnamnet? Vi tittar på ett exempel:

$ cd /usr/bin
$ ls xz*
xz xzcat xzcomp xzdiff xzegrep xzfgrep xzless xzmore
$ ls *xz
unxz xz
$ ls *xz*
unxz xz xzcat xzcomp xzdiff xzegrep xzfgrep xzgrep xzless xzmore

Först listade vi filnamn som börjar med bokstäverna xz (+xz*+), sedan listade vi filnamn som slutar med bokstäverna xz (+*xz+) och slutligen listade vi filnamn som innehåller bokstäverna xz (+*xz*+).

Det andra jokertecknet är ? som betyder exakt ett okänt tecken. Vill vi söka efter flera okända tecken får vi lägga till ett ?-tecken för varje okänt. är det tre okända tecken blir det till exempel ???.

$ cd /usr/bin
$ ls ???e
file free line nice time
$ ls ?i?e
file line nice time

I det första exemplet ovan visar vi alla filnamn som är fyra tecken långa, där de tre första är okända och det fjärde tecknet är ett e (???e). I det andra exemplet visar vi alla filnamn som är fyra tecken långa, där det andra tecknet är bokstaven i och det fjärde tecknet är bokstaven e (?i?e).

Det tredje jokertecknet är egentligen två tecken: +[+ och +]+. +[ ]+ används för att ange en uppsättning tecken. Om vi vill lista alla filer som börjar med a, b eller c i katalogen kan vi skiva +ls a* b* c*+. Det är inte speciellt snyggt, men det fungerar. Prova att skriva +ls [abc]*+ istället, det gör samma sak. Om uttrycket mellan +[+ och +]+ börjar med ett utropstecken (!) eller cirkumflex (^) betyder det allt som inte matchar uttrycket. Vi kan till exempel lista alla filer utom de som börjar med bokstaven a, b eller c. Några exempel på hur vi använder detta:

$ ls a* b* c*
$ ls [abc]*
$ ls [!abc]*

Vi kan ange områden i våra sökningar istället för att ange ett specikt tecken. Om vi till exempel vill lista alla filer som börjar med någon av bokstäverna a till c skriver vi +ls [a-c]*+. För att ange stora bokstäver skriver vi istället +ls [A-C]*+ och för siffror skriver vi +ls [0-9]*+. För att lista alla filer som börjar med en liten eller en stor bokstav skriver vi +ls [A-Za-z]*+.

$ ls [a-c]*
$ ls [A-C]*
$ ls [0-9]*
$ ls [A-Za-z]*

Hur gör vi för att visa de dolda filerna med hjälp av jokertecken? Vi måste ange punkten (.) först, sedan används jokertecken precis som med vanliga filer:

$ ls .g*
$ ls .[a-z]*

Skriva skript i Bash

Innan vi börjar skriva vårt första skript i Bash skall vi gå igenom ett par grundläggande saker. Skript kan skivas i många olika språk, nu använder vi Bash. Vi skulle lika gärna kunna skriva våra skript i csh, zsh, ksh eller någon annan kommandotolk. Vi kan skriva skript i Bash även om vi använder zsh som kommandotolk, och tvärtom. Skript är helt vanliga textfiler som innehåller information till datorn vad vi vill att den skall göra. Lite grann som ett recept: ta en deciliter mjöl, blanda ut det med en tesked salt, värm upp fyra deciliter mjölk och rör ner mjölet i det..

Trots att Linux inte använder filändelser för filer i filsystemet använder vi traditionellt filändelsen .sh för bashskript. Du har säkert redan listat ut att .sh är en förkortning för det engelska ordet shell. Den första raden i filen vi skapar för vårt skript måste ange vilken kommandotolk vi vill använda, detta eftersom användaren kanske kör skriptet under en helt annan kommandotolk som har ett annat programspråk.

För att ange att det är Bash vi vill använda skriver vi: +#!/usr/bin/env bash+ på den första raden i vår fil. Ibland stöter vi på +#!/bin/sh+ i skriptfiler. Tittar vi på filen /bin/sh med kommandot ls -l /bin/sh i filsystemet upptäcker vi att det är en länk till /bin/dash. Inte alls samma kommandotolk som vi vill använda. Ganska ofta kommer vi stöta på +#!/bin/bash+ på den första raden i bashskript också, det är inte helt fel men vi bör undvika det eftersom bash kan ligga någon annan stans i filsystemet hos den som kör vårt skript. Vi håller oss därför till +#!/usr/bin/env bash+ i våra skript.

Vi tittar på vårt skript så här långt:

#!/usr/bin/env bash

Staket-tecknet (#) används normalt för kommentarer, förutom just för denna rad där den anger vilken kommandotolk vi skall starta för att köra skriptet. Nu lägger vi till en rad i vårt skript så det faktiskt kan göra något:

#!/usr/bin/env bash
echo 'Hej Bash!'

Kommandot echo innebär att Bash skall skriva ut något, i det här fallet texten Hej Bash!. Vi avslutar vårt skript på rätt sätt, det vill säga anropar kommandot exit och anger ett exitvärde:

#!/usr/bin/env bash
echo 'Hej Bash!'
exit 0

Ett skript som lyckats köra utan problem skall returnera exitvärdet noll (0), alla andra värden än noll (0) är felmeddelanden. Vi kommer tillbaka till detta med returvärden och exitvärden lite senare.

Nu är vårt skript färdigt, vi sparar filen och skriver chmod +x hello.sh (om vi döpte skriptet till hello.sh). Detta gör vi för att skriptet skall gå att köra utan att först anropa bash.

$ chmod +x hello.sh
$ ./hello.sh
Hej Bash!

Om vi inte sätter executeflaggan (x) med chmod kan vi ändå köra vårt skript, om vi startar det med bash, så här:

$ bash hello.sh
Hej Bash!

Nu skall vi titta på lite miljövariabler i Bash. Kommandot printenv visar vilka variabler som är satta i systemet och vad de innehåller:

$ printenv
ACLOCAL_FLAGS=-I /opt/gnome/share/aclocal
COLORTER=1
CPU=i686
...
$

Det finns en miljövariabel som heter USER, den innehåller användarnamnet för den användare som är inloggad. Vi visar variabeln genom att skriva:

$ echo $USER
jonas

Innan vi skall använda variabeln i vårt skript skall vi ta och titta på skillnaden mellan tecknen " och ' i Bash:

$ echo "$USER"
jonas
$ echo '$USER'
$USER

Om vi använder citattecken (") kommer variabeln expanderas, det vill säga - innehållet i den kommer att visas. Använder vi däremot apostrof-tecken (') kommer variabeln inte expanderas. Det är skillnaden mellan de två.

Nu öppnar vi vårt skript, hello.sh, igen och ändrar lite i det:

#!/usr/bin/env bash
echo "Hej $USER"
exit 0

Nu sparar vi filen och kör skriptet. Nu kommer skriptet att skriva ut Hej Jonas istället (om vårt användarnamn är jonas).

En viktig del av de skript vi skriver är kommentering. Kommentarer i skripten kommer hjälpa oss när vi kommer tillbaka till skriptet tre, eller sex månader sedan och inte längre minns hur vi tänkte när vi skrev det. Att kommentera vårt korta skript Hej Bash verkar kanske lite onödigt, men vi tittar ändå på hur vi kan skapa kommentarer i våra skript:

#!/usr/bin/env bash
# Ett skript som skriver ut Hej och användarens namn
echo "Hej $USER" # Skriver ut Hej och användarens namn
exit 0 # Avslutar programmet

Nu sparar vi filen och kör skriptet. Det är ingen synlig skillnad från innan, det skriver fortfarande ut Hej jonas på skärmen (om vårt användarnamn är jonas). Med hjälp av +#+-tecknet kan vi alltså skriva kommentarer i våra skript. Allt som kommer efter +#+-tecknet, fram till radbrytningen, kommer att tolkas som en kommentar och ignoreras av Bash. Skriver vi ett +#+-tecken i början av raden ignoreras hela raden. Skriver vi ett +#+-tecken någon annanstans på raden ignoreras allt från tecknet fram till radbrytningen.

Vi bör alltid skriva bra kommentarer i våra skript. När vi tittar på skripten ett halvår senare kommer vi tacka oss själva för att vi kommenterade skripten. Hur väl insatta vi än känner oss i våra skript idag, kommer situtionen se annorlunda ut om ett par månader då vi sitter och funderar på varför vi har med det där konstiga i skriptet.

Variabler

För att kunna skapa användbara skript måste vi ha koll på variabler. En variabel är en plats i datorns minne som innehåller ett värde. Variabler deklareras innan de kan användas. I Bash behöver vi inte deklarera variabeln innan vi använder den, utan kan tilldela den ett värde direkt:

#!/usr/bin/env bash
NAMN="jonas"
echo ${NAMN}
exit 0

I exemplet ovanför deklarerar vi variabeln NAMN och tilldelar den värdet jonas. Variabelnamnen i Bash skrivs traditionellt med stora bokstäver för att skilja dem från kommandon i skriptet. Variabler deklareras med sitt namn (NAMN i exemplet ovanför) men refereras till som ${variabelnamn} (som ${NAMN} i exemplet ovanför).

Vi kan inte använda mellanslag mellan variabelnamnet, likamed-tecknet (=) och värdet vi vill tilldela, några exempel:

$ VAR = "jonas"
-bash: VAR: command not found
$ VAR= "jonas"
-bash: VAR: command not found
$ VAR="jonas"
$ echo ${VAR}
jonas
$

Det är bara det sista kommandot i exemplet ovanför, VAR="jonas", som fungerar - de andra skapar felmeddelanden. Skall vi skapa riktigt fina skript bör vi använda declare för att deklarera våra variabler. declare har dessutom några flaggor som vi har nytta av ibland:

  • -a för array (listor)
  • -i för integer (heltal)
  • -p för print (visa variabelns värde)
  • -r för read-only (variabeln går inte att ändra)
  • -x för export (variabeln exporteras)

Listor (array) och heltal (integer) kommer vi använda senare. Vi tittar på print, read-only och export så länge:

$ declare NAMN="jonas"
$ declare -p NAMN
jonas
$ declare -r NAMN="jonas"
$ declare -x NAMN="jonas"

Print (-p) visar variablens värde, det vi lagrat i variablen. Notera att vi inte använder ${NAMN} här. Nu tittar vi på read-only:

$ declare -r OS="Linux"
$ echo ${OS}
Linux
$ OS="Windows"
-bash: OS: readonly variable

I exemplet ovanför skapar vi en read-only variabel som vi döper till OS, värdet för OS sätter vi till Linux. Vi visar variabelns värde med hjälp av kommandot echo. Sedan försöker vi ändra värdet till Windows, vilket Bash sätter stopp för. Variabler som inte går att ändra (read-only) kallas för konstanta variabler eller bara kort: konstant.

Export (-x) använder vi för att göra våra variabler tillgängliga utanför det skript som vi skapar dem i. Nu skapar vi skriptet export.sh:

#!/usr/bin/env bash
declare DIST="Ubuntu"
echo "export.sh: ${DIST}"
bash import.sh
exit 0

Vi sparar skriptet och skapar ett nytt skript som vi döper till import.sh:

#!/usr/bin/env bash
echo "import.sh: Aha, du kommer med en dist som heter: ${DIST} ?"
exit 0

Lägg märke till att vi inte deklarerar variabeln DIST i filen import.sh. Det skall vi inte göra. Vi anropar också skriptet import.sh från export.sh genom att skriva bash import.sh. Nu kör vi skriptet export.sh:

$ chmod +x export.sh
$ ./export.sh
export.sh: Ubuntu
import.sh: Aha, du kommer med en dist som heter: ?
$

Det fungerade ju ganska bra. export.sh startas och anropar import.sh, det ser vi på utskriften. Däremot visas inte värdet i variabeln DIST i skriptet import.sh, så vi ändrar i export.sh:

#!/usr/bin/env bash
declare -x DIST="Ubuntu"
echo "export.sh: ${DIST}"
bash import.sh
exit 0

Det vi gör är att vi exporterar variabeln DIST med hjälp av flaggan -x till declare. Exporten sker till alla skal som startas från det vi kör just nu, när vi startar import.sh med bash-kommandot i export.sh kommer variabeln alltså att exporteras till Bash som startar import.sh. Nu fungerar vårt skript som vi tänkt.

$ ./export.sh
export.sh: Ubuntu
import.sh: Aha, du kommer med en dist som heter: Ubuntu ?
$

Istället för att använda declare -x kan vi istället skriva export. declare -x DIST="Ubuntu" skrivs så här med export: export DIST="Ubuntu".

Ibland kan det vara bra att ta bort sina variabler också, det gör vi med kommandot unset . unset är inget krångligt alls utan fungerar så att man skriver unset VARIABELNAMN. Vi skapar ett skript för att visa hur det fungerar:

#!/usr/bin/env bash
declare DIST="Ubuntu"
echo "Din dist är ${DIST}"
unset DIST
echo "Vart tog ${DIST} vägen?"
exit 0

Nu sparar vi filen och kör den för att se vad som händer när vi använder unset.

In- och utmatning

För att användaren skall kunna mata in svar och på så sätt påverka hur skriptet kommer köras behöver vi använda bashs inbyggda kommando read. Med read kan vi vänta på att användaren skall svara på en fråga innan vi fortsätter köra skriptet. Vi skriver ett enkelt skript (inmatning.sh):

#!/usr/bin/env bash
echo "Vad heter du?"
read USERNAME
echo "Hej ${USERNAME}"
exit 0

Vi sparar skriptet som inmatning.sh och sätter körflaggan (chmod +x inmatning.sh) på filen och testar skriptet. Det användaren matar in hamnar i variabeln USERNAME, vi använder den variabeln för att skriva ut Hej ... på raden efter inmatningen. Lade du märke till att du fick svara på raden under frågan? Det ser inte så bra ut, helst skulle vi vilja att användaren svarade direkt efter frågan. Det löser vi med en flagga till echo, nämligen -n:

#!/usr/bin/env bash
echo -n "Vad heter du? "
read USERNAME
echo "Hej ${USERNAME}"
exit 0

Flaggan -n till echo innebär att echo inte skapar en ny rad efter att den skrivit ut texten. Varför inte använda kommandot read för detta istället? Vi tittar på flaggan -p för kommandot read:

#!/usr/bin/env bash
read -p "Vad heter du? " USERNAME
echo "Hej ${USERNAME}"
exit 0

Se där, det fungerar ju. Flaggan -p (för prompt) till kommandot read innebär att read skall skriva ut något innan användaren matar in sitt svar. Variabeln som skall innehålla svaret skriver vi sist på raden med read -p.

Hur gör vi om användaren inte svarar inom en tidsperiod, säg tre sekunder? Skall skriptet stå och vänta i all evighet då, eller skall vi gå vidare ändå med standardvärden? Naturligtvis har read ett sådant val också, -t. Tiden read skall vänta mäts i sekunder. Vi tittar på ett exempel:

#!/usr/bin/env bash
echo -n "Vad heter du? "
read -t 3 USERNAME
echo "Hej ${USERNAME}"
exit 0

Nu kommer skriptet vänta i tre (3) sekunder innan det går vidare. Här är det lämpligt att lägga in ett standardvärde, om användaren inte matar in något:

#!/usr/bin/env bash
echo -n "Vad heter du? "
read -t 3 USERNAME
USERNAME=${USERNAME:="Hemlig"}
echo "Hej ${USERNAME}"
exit 0

Om användaren inte matar in sitt namn inom tre sekunder kommer variabeln USERNAME sättas till värdet Hemlig.

Slutligen skall vi titta på hur vi hanterar för långa inmatningar. Kommandot read kan begränsa hur många tecken vi skall läsa in från kommandoraden med hjälp av flaggan -n. Vi vill inte ha längre namn än tio (10) tecken så vi begränsar användaren till det:

#!/usr/bin/env bash
echo -n "Mata in max tio tecken: "
read -n 10 TECKEN
echo "Dina tecken är: ${TECKEN}"
exit 0

När användaren har matat in tio tecken, eller tryckt på RETUR, avbryts inmatningen och Dina tecken är … skrivs ut. Notera att det blir skönhetsfel om användaren matat in tio tecken, Dina tecken är … skrivs ut direkt efter det inmatade. En lösning på detta är att lägga till ett extra echo på raden mellan read och echo i skriptet.

Om vi inte anger ett variabelnamn till read kommer svaret användaren matar in att hamna i variabeln REPLY:

$ read
jonas
$ echo $REPLY
jonas
$

Utmatningen sker med antagligen kommandot echo, som vi använt hittils, eller med kommandot printf som har fler funktioner för formatterad utskrift.

Några av specialtecknen för utmatning i printf:

Specialtecken Förklaring
\b Backspace
\n Newline, skapa en ny rad
\t Tabb
\\ Ett backslashtecken
\0n Visa ASCII-tecknet med koden n

Vi testar printf på kommandoraden:

$ MENING=Jag tycker Bash är roligt
$ printf "%s\n" $MENING
Jag
tycker
Bash
är
roligt
$

Det där blev inte riktigt vad vi tänkte oss. Eftersom vi inte omsluter värdet vi tilldelar variabeln MENING med citattecken blir utskriften felaktig, vi provar igen:

$ MENING="Jag tycker Bash är roligt"
$ printf "%s\n" $MENING
Jag tycker Bash är roligt
$

Nu fungerade det mycket bättre. %s anger att vi vill skriva ut en sträng, strängen hittar printf i variabeln vi anger efter printf . Specialtecknet \n skapar en radbrytning.

Vi tittar på hur vi kan hantera tal med %d:

$ BRED=abc
$ printf "Priset på bredband sänktes med %d%%.\n" $BRED
-bash: printf: abc: invalid number
Priset på bredband sänktes med 0%.
$

Med %d kan vi inte skriva ut bokstäver, det är bara hela tal som fungerar. Vi skriver BRED=25 och försöker igen:

$ BRED=25
$ printf "Priset på bredband sänktes med %d%%.\n" $BRED
Priset på bredband sänktes med 25%.
$

De två %%-tecknen som kommer efter %d är till för att skriva ut ett procenttecken (%).

Räkna med bash - de fyra räknesätten

I det här avsnittet skall vi titta på hur vi kan räkna med de fyra räknesätten: addition, subtraktion, multiplikation och division i Bash.

Addition

Addition av två tal beräknas med plustecknet (+). Vi kan använda kommandot let för detta:

$ let SUMMA=10+10
$ echo $SUMMA
20

Vi kan också använda dubbla paranteser på det här viset:

$ SUMMA=$(( 10 + 10 ))
$ echo $SUMMA
20

Subtraktion

Subtraktion av två tal beräknas med minustecknet (-), vi använder kommandot let för detta:

$ let DIFF=100-10
$ echo $DIFF
90

Eller med dubbla paranteser:

$ DIFF=$(( 100 - 10 ))
$ echo $DIFF
90

Multiplikation

Multiplikation av två tal beräknas med *, vi använder kommandot let för detta:

$ let PRODUKT=100*10
$ echo $PRODUKT
1000

Eller med dubbla paranteser:

$ PRODUKT=$(( 100 * 10 ))
$ echo $PRODUKT
1000

Division och modulus

Division med heltal skapar en kvot och en rest. Eftersom vi bara kan räkna med heltal kan vi inte inte få fram decimalerna i våra uträkningar med division. Vi tar 5/2 som exempel, kvoten kommer bli 2.5, men eftersom vi enbart kan räkna med heltal kommer kvoten från Bash bli 2. 2 * 2 = 4 kvar har vi alltså 1. För att få fram resten (1) använder vi oss av modulus som är ett procenttecken (%): 5%2 ger oss 1. Resten av heltalsdivisionen 5/2 är alltså 1.

Vi använder kommandot let för att få fram kvoten av 100 dividerat med 10:

$ let KVOT=100/10
$ echo $KVOT
10

Vi kan naturligtvis göra samma sak med dubbla paranteser:

$ KVOT=$(( 100 / 10 ))
$ echo $KVOT
10

Hur gör vi om divisionen inte går jämnt ut? Vi måste få fram resten, vilken vi får med hjälp av modulus (%):

$ let KVOT=100/13
$ echo $KVOT
7
$ let REST=100%13
$ echo $REST
9

Vad hände här? 100 går att dela med 13 sju gånger ( 13 * 7 = 91 ). Kvar blir 9 (91 +9 = 100).

Det finns flera olika sätt att räkna med decimala tal (flyttal) i Bash, vi skall titta på ett sätt vi kan använda. Kommandot bc.

Vi börjar med att räkna ut 2.5*5 med decimaler:

$ bc <<< "2.5*5"
12.5

Detta fungerar med addition, subtraktion och multiplikation. Även division fungerar, men vi måste använda scale för att ange noggrannheten (precisionen) på antalet decimaler. Vi tittar på ett exempel:

$ bc <<< "5/2"
2

Fem delat med två blir 2.5 och inte 2 som i exemplet ovanför. För att visa decimalerna använder vi scale:

$ bc <<< "scale=2;5/2"
2.50

Genom att sätta scale=2 före uttrycket vi vill beräkna säger vi till bc att vi vill visa talet med två decimalers noggrannhet. scale=1 ger oss en decimal, scale=4 ger oss fyra decimaler och så vidare.

Ofta vill vi lagra resultatet av en beräkning i en variabel för att kunna använda den senare i våra skript. För att göra detta använder vi $( ) på detta viset:

$ Tal=$( bc <<< "2.5*5" )
$ echo ${Tal}
2.50

För division med scale ser det ut så här:

$ Tal=$( bc <<< "scale=2;5/2" )
$ echo ${Tal}
2.50

Genom att ange flaggan -l till kommandot bc får vi maximal precision på decimalerna, ofta är det dock bättre att specifiera hur stor noggrannhet vi vill ha med scale:

$ bc -l <<< "5/2"
2.50000000000000000000

Villkor

Villkor använder vi för att skapa alternativ för skriptet, vad händer till exempel om programmet vi vill använda i skriptet inte finns?

Ett enkelt villkor skrivs så här:

if villkor ; then
    vad skall vi göra om villkoret uppfylls?
fi

Villkor för filer

Vi använder ofta villkor för att testa filer i början av skript. Finns filen vi vill läsa? Om den inte finns kommer skriptet inte fungera och bör avbrytas snarast. Kan vi skriva till en fil? om inte verkar det dumt att ens försöka.

Villkor Förklaring
-b Är filen en block device?
-c Är filen en character device?
-d Är filen en katalog (directory)?
-e Finns filen (exists)?
-h och -L Är filen en länk (link)?
-r Är filen läsbar för vårt skript?
-s Finns filen och har den något innehåll?
-w Är filen skrivbar för vårt skript?
-x Är filen körbar för vårt skript?
fil1 -nt fil2 Är fil1 nyare än fil2 (newer than)?
fil1 -ot fil2 Är fil1 äldre än fil2 (older than)?

Vi skriver ett exempel som visar hur det fungerar:

if [ -r ./hemliginfo.dat ] ; then
    echo "Vi kan läsa filen."
fi

Ovanstående exempel testar om filen hemliginfo.dat i katalogen är läsbar för vårt skript. Om den är det kommer texten Vi kan läsa filen. att skrivas ut. Nu är det kanske inte alltid det här vi vill testa, utan snarare vill vi testa om en fil går att läsa, om inte skall skriptet avbrytas. Vi använder utropstecknet (!) för att göra om ett sant villkor till ett falskt. Ett villkor som uppfylls, till exempel att filen finns är sant. Ett villkor som inte uppfylls är falskt. Ett villkorsuttryck kan alltså bara resultera i en av två värden: sant eller falskt.

Ofta vill vi se om ett villkor inte är uppfyllt, till exempel om filen inte finns. I exemplet ovanför testade vi om filen hemliginfo.dat var läsbar för skriptet, om filen fanns skulle vi skriva ut Vi kan läsa filen. Istället för att enbart skriva ut texten med kommandot echo skulle vi kunna skriva hela vårt skipt mellan raden med if och raden med fi. Det här skulle bli ganska opraktiskt att underhålla, speciellt när vi egentligen bara ville se om filen var läsbar och om den inte var det skulle vi avbryta skriptet.

Vi ändrar skriptet så det ser ut så här:

if [ ! -r ./hemliginfo.dat ] ; then
    echo "Vi kan läsa filen."
fi

Om filen finns och är läsbar för vårt skript kommer if-satsen resultera i att villkoret är sant. Genom att sätta ett utropstecken innan kommer vi göra om sant till falskt. Eftersom villkoret nu blir falskt kommer inte echo köras. Om filen istället inte är läsbar för oss kommer villkoret bli falskt, och med ett utropstecken kommer falskt bli sant och echo kommer köras.

Vi tittar på en mer användbar kodsnutt:

if [ ! -e ./hemliginfo.dat ] ; then
    echo "FEL: Filen hemliginfo.dat finns inte. Avbryter."
    exit 1
fi

Här använder vi -e för att titta om filen hemliginfo.dat finns. Om den inte finns skall skriptet avslutas. Ett mycket bra sätt att felhantera skriptet om vårt skript är beroende av att en fil finns.

Villkor med strängar

I denna del skall vi titta på hur vi kan hantera villkor för strängar.

Villkor Förklaring
-z Är strängen tom?
-n Är strängen inte tom?
str1 = str2 Är str1 lika som str2?
str1 != str2 Är str1 inte lika som str2?
str1 < str2 Är str1 mindre än str2?
str1 > str2 Är str1 större än str1?

Vi börjar med att titta på -z och -n:

#!/usr/bin/env bash
STR="Ubuntu"
if [ -z "${STR}" ] ; then
    echo "Strängen är tom."
fi
        
if [ -n "${STR}" ] ; then
    echo "Strängen är inte tom."
fi
    
exit 0

Skriv ovanstående exempel som ett skript och kör skriptet. ändra sedan så det står STR="" och kör skriptet igen. Att veta om en variabel är tom innan vi börjar använda den är mycket användbart.

Vi tittar nu på hur vi kan se om två variabler är lika:

#!/usr/bin/env bash
STR1="Ubuntu"
STR2="Linux"
if [ "${STR1}" = "${STR2}" ] ; then
    echo "${STR1} är likadan som ${STR2}"
fi
        
if [ "${STR1}" != "${STR2}" ] ; then
    echo "${STR1} är inte samma som ${STR2}"
fi
        
exit 0

Varje tecken vi kan skriva i en dator finns i en teckenkodningstabell som kallas ASCII (American Standard Code for Information Interchange). ASCII är en sju bitarstabell som rymmer de tecken som behövdes i USA, vilket gör att våra svenska tecken som Å, Ä och Ö inte finns med i tabellen. Därför utökades ASCII till olika ISO-standarder som hade åtta bitar för varje tecken. För att skriva västeuropeiska tecken (som å, ä och ö) användes ISO-8859-1 teckenkodning. När valutan euro introducerades i Europa utökades ISO-8859-1 med tecknet för euro och cent. Nu förtiden använder vi teckenkodningstabellen UTF-8 som sägs täcka alla behov av tecken i hela världen.

Tecknet 0 har det numeriska värdet 48 i ASCII-tabellen, tecknet 9 har det numeriska värdet 57. Stora A har det numeriska värdet 65. Lilla a har det numeriska värdet 97.

Att titta på om en sträng är större än eller mindre än fungerar inte rikigt som vi tänker oss att det borde fungera. Vad större än och mindre än gör är att jämföra tecknens ASCII-koder (se ovan angående ASCII). Om vi till exempel jämför orden sommar och vår skulle vi kunna tänka oss att sommar skulle vara större än vår, eftersom sommar är längre än vår. Så är inte fallet i Bash, jämförelsen säger att vår är större än sommar, eftersom ASCII-koden för bokstaven v är 118 och för bokstaven s är ASCII-koden 115. Villkoret skall alltså tolkas som 118 är större än 115:

#!/usr/bin/env bash

STR1="sommar"
STR2="vår"

if [[ "${STR1}" < "${STR2}" ]] ; then
    echo "${STR1} är mindre än ${STR2}"
fi
        
if [[ "${STR1}" > "${STR2}" ]] ; then
    echo "${STR1} är större än ${STR2}"
fi
        
exit 0

Vill vi se vilken sträng som är störst (längst) får vi räkna ut längden på en sträng med # som i ${#STR1}. Vi provar:

$ VAR="jonas"
$ echo ${#VAR}
5

I exemplet ovanför ser vi att variabeln VAR innehåller fem (5) tecken (jonas). Vi kan använda detta för att se vilken sträng som är längst.

#!/usr/bin/env bash
        
STR1="linux"
STR2="windows"
        
if [[ ${#STR1} -gt ${#STR2} ]] ; then
    echo "${STR1} är längre än ${STR2}"
fi
        
if [[ ${#STR1} -lt ${#STR2} ]] ; then
    echo "${STR1} är kortare än ${STR2}"
fi
        
exit 0

Eftersom linux är fem tecken och windows är sju tecken långt kommer vi i ovanstående skript få utmatningen linux är kortare än windows. Vi kan göra skriptet kortare med else:

#!/usr/bin/env bash

STR1="linux"
STR2="windows"
    
if [[ ${#STR1} -gt ${#STR2} ]] ; then
    echo "${STR1} är längre än ${STR2}"
else
    echo "${STR1} är kortare än ${STR2}"
fi
        
exit 0

Om STR1 inte är längre än STR2 kan vi utgå från att STR1 är kortare än STR2. Notera dock att vi inte tar hänsyn till om strängarna är lika långa i exemplen ovanför.

Matematiska villkor

Vi kan jämföra tal med Bash också.

Villkor Förklaring
nr1 -eq nr2 Är nr1 och nr2 lika (equal)?
nr1 -ne nr2 Är nr1 och nr2 olika (not equal)?
nr1 -lt nr2 Är nr1 mindre än nr2 (less than)?
nr1 -le nr2 Är nr1 mindre än, eller lika med, nr2 (less than or equal)?
nr1 -gt nr2 Är nr1 större än nr2 (greater than)?
nr1 -ge nr2 Är nr1 större än, eller lika med, nr2 (greater than or equal)?

För att jämföra två variabler med värden kan vi göra så här:

#!/usr/bin/env bash
        
TAL1=10
TAL2=20
if [ ${TAL1} -eq ${TAL2} ] ; then
    echo "${TAL1} är lika med ${TAL2}"
fi
if [ ${TAL1} -ne ${TAL2} ] ; then
    echo "${TAL1} är inte lika med ${TAL2}"
fi
        
exit 0

Vi skapar en fil där vi skriver in skriptet ovanför och kör det för att se vad som händer. Sedan ändrar vi TAL1 och TAL2 till andra värden för att kontrollera att det fungerar som det skall.

Flera villkor

Det händer ibland att vi vill kunnna jämföra flera villkor samtidigt. Att en fil finns betyder inte alltid att den går att skriva till. För att kunna använda flera villkor behöver vi använda -a (för and, och) och -o (för or, eller). Säg att vårt skript behöver veta om en fil finns och att skriptet kan skriva till filen, om dessa villkor inte uppfylls skall vi avsluta skriptet:

if [ ! -e ./datafil.dat -o ! -w ./datafil.dat ] ; then
    echo "Filen datafil.dat finns inte, eller går inte att skriva till."
    exit 1
fi

Vad vi säger i vårt villkor (i skriptet ovanför) är om filen datafil.dat inte finns (! -e ./datafil.dat) eller (-o) filen inte är skrivbar (! -w ./datafil.dat) skall vi skriva ett felmeddelande och avsluta skriptet.

Om inte ett villkor uppfylls, kanske ett annat gör det?

I våra if-satser (villkoren) har vi testat om ett, eller flera (med and och or), villkor är uppfyllda. Hur gör vi om vill testa om ett villkor uppfylls, och göra något annat om villkoret inte är uppfyllt?

if [ ${TAL} -ge 10 ] ; then
    echo "Talet är 10 eller högre."
else
    echo "Talet är mindre än 10."
fi

I exemplet ovanför testar vi om variabeln $TAL är större eller lika med 10, om $TAL är större eller lika med 10 kommer vi skriva ut texten Talet är 10 eller högre om det är mindre än 10 (villkoret uppfylls inte) kommer vi skriva ut texten Talet är mindre än 10 istället.

Här använder vi oss av else som vi kan läsa som: om villkoret är sant gör vi det här, om det inte är sant (else) gör vi det här.

Det händer att vi vill testa flera villkor på en och samma variabel. Vi tittar på ett exempel där vi skall skriva ett skript som räknar ut betyg för elever. Vi skriver följande:

if [ ${BETYG} -ge 80 ] ; then
    echo "VG"
elif [ ${BETYG} -ge 50 ] ; then
    echo "G"
else
    echo "IG"
fi

Variabeln $BETYG innehåller antalet poäng eleven har skrivit på provet. Gränsen för godkänd har vi satt till 50 poäng och gränsen för väl godkänd har vi satt till 80 poäng. Allt under 50 poäng kommer resultera i betyget icke godkänd.

Vi använder oss av elif i skriptet ovanför. elif är en förkortning av else if (annars om). elif använder vi när vi behöver testa flera olika villkor.

Om vi sätter variabeln $BETYG till 90 kommer det första villkoret (större än 80) att uppfyllas, dessutom kommer det andra villkoret (90 är större än 50) att uppfyllas. Varför kommer skriptet inte skriva ut både VG och G?

En if-elif sats avbryter hela if-satsen när det första villkoret är sant. Därför kommer vi avsluta hella villkorstestet när villkoret ${BETYG} -ge 80 uppfyllts.

Loopar

Med loopar kan vi upprepa samma sak om och om igen, i all oändlighet eller tills något uppfyller ett villkor. Vi skall titta på tre olika loopar i Bash: while, for och until

While

while-loopar används för att skapa loopar som håller på så länge som ett villkor är uppfyllt, eller avbryts med kommandot break. En while-loop skrivs så här:

while villkor ; do
    vad skall vi göra?
done

Vi testar det i ett skript:

#!/usr/bin/env bash

while read -p "Mata in ett namn: " NAMN ; do
    echo "Du matade in ${NAMN}"
done

Skriptet kommer köras i all oändlighet, eller tills vi avbryter det genom att trycka CTRL och D samtidigt. Om ett villkor inte uppfylls på första försöket kommer kommandona mellan while och done aldrig att köras.

För att skapa ett bättre skript bör vi lägga till något sätt att ta oss ur while-loopen, förutom att trycka CTRL+D. Vi skriver om vårt skript:

#!/usr/bin/env bash
    
while read -p "Mata in ett namn (exit avslutar): " NAMN ; do
    if [ "${NAME}" = "exit" ] ; then
        break
    fi
    echo "Du matade in ${NAMN}"
done

Ett villkor kan vara sant eller falskt. Vi kan använda orden true och false för att skapa våra villkor:

#!/usr/bin/env bash
        
while true ; do
    read -p "Mata in ett namn (exit avslutar): " NAMN
    if [ "${NAMN}" = "exit" ] ; then
        break
    fi
   echo "Du matade in ${NAMN}"
done

Vi kan naturligtvis också bestämma hur många gånger något skall upprepas genom att skapa en räknare:

#!/usr/bin/env bash

NUM=0
while [ ${NUM} -lt 3 ] ; do
    echo "Nummer: ${NUM}"
    NUM=$[ NUM + 1]
done
    
exit 0

for

En for-loop repeterar något så länge den har något att repetera.

Låt oss säga att vi har ett antal filer som vi vill komprimera med gzip, en for-loop skulle klara saken enkelt:

#!/usr/bin/env bash
    
for FILER in "fil1.sh fil2.sh fil3.sh" ; do
    gzip ${FILER}
done
exit 0

I skriptet ovanför kommer vi att komprimera filerna fil1.sh, fil2.sh och fil3.sh med gzip.

En for-loop kan användas med en räknare också, om vi vill göra något ett bestämt antal gånger:

#!/usr/bin/env bash
        
for (( RAKNARE=1; RAKNARE < 10; RAKNARE++ )) ; do
    echo "Räknaren är nu: ${RAKNARE}"
done
exit 0

until

until använder vi för att skapa loopar som skall upprepas tills ett villkor uppfylls. Ett exempel:

#!/usr/bin/env bash
        
NAMN=""
until [ "${NAMN}" = "jonas" ] ; do
    read -p "Skriv ett namn: " NAMN
done
exit 0

I exemplet ovanför kommer until köras tills användaren matar in namnet jonas.

Vill vi att until-loopen skall köras ett visst antal gånger kan vi skriva som i följande skript:

#!/usr/bin/env bash
        
NUM=1
until [ ${NUM} -gt 3 ] ; do
    echo "Nummer: ${NUM}"
    NUM=$[ NUM + 1 ]
done
exit 0

I exemplet ovanför kommer until-loopen skriva ut 1, 2 och 3.

Argument

När vi använder kommandon från kommandoraden kan vi ofta skicka med argument till kommandot. När vi vill kopiera en fil kan det se ut så här:

$ cp filen.txt kopian.txt

Kommandot cp (copy) tar emot två argument: filen.txt som är filen vi vill kopiera och kopian.txt som är namnet på den kopia vi vill skapa.

Ofta vill vi använda argument till våra egna skript också, så användaren av skriptet kan påverka vad som skall hända, eller göras, i skriptet. I den här delen tittar vi på hur det fungerar i Bash.

I Bash finns det ett par fördefinerade variabler som hanterar argument:

  • $# innehåller hur många argument vi har skickat till skriptet.
  • $0 innehåller skriptets namn (filnamnet).
  • $1 innehåller det första argumentet.
  • $2 innehåller det andra argumentet.
  • $3, $4, $5 och så vidare innehåller det tredje, fjärde och femte argumentet som skickats till skriptet.
  • $* innehåller alla argument.

Om vi nu går tillbaka och tittar på kopieringen av filen i exemplet ovanför:

$ cp filen.txt kopian.txt

Här skulle $# säga 2, eftersom vi har två argument. $0 skulle vara cp, eftersom det är skriptets namn. $1 skulle innehålla filen.txt och $2 skulle innehålla kopian.txt.

Nu skriver vi ett skript som använder sig av argument:

#!/usr/bin/env bash

echo "Ditt argument var: $1"
exit 0

Vi sparar skiptet och kör det:

$ bash argument.sh argumentet
Ditt argument var: argumentet

Det är lämpligt att deklarera en variabel som fångar upp argumentet i skriptet, så vi inte får problem med till exempel $1 senare i skriptet:

#!/usr/bin/env bash
        
ARG=$1
echo "Ditt argument var: $ARG"
exit 0

Om ett skript vi skapar behöver argument för att fungera bör vi också ha en kontroll om det kommer ett argument till skriptet. För att kontrollera om vi får in argument till ett skript kan vi skriva så här:

#!/usr/bin/env bash

if [ $# -ne 1 ] ; then
    echo "$0: Behöver ett argument"
    exit 1
fi

I skriptet ovanför kontrollerar vi om vi har ett ($# -ne 1) argument skickat till oss. Om vi inte har ett argument skall vi skriva ut ett felmeddelande, här använder vi $0 för att skriva ut skriptets namn innan meddelandet. Efter felmeddelandet avslutar vi skiptet med exitkoden 1.

Om vi istället vill ha minst ett, eller fler argument skriver vi [ $# -ge 1 ] i skripet. Då kommer vi kontrollera om vi har ett, eller flera, agument skickade till skriptet.

Med variabeln $* får vi fram alla argument som skickades till skriptet, till exempel så här:

#!/usr/bin/env bash
echo $*
exit 0

Vi sparar skiptet med filnamnet argument.sh och kör det:

$ bash argument.sh anna lena lotta jessica
anna lena lotta jessica

Vill vi använda argumenten till något användbart kan vi till exempel använda en for-loop:

#!/usr/bin/env bash

for NAMN in $* ; do
    echo "Ett flicknamn är $NAMN"
done
exit 0

När vi kör skriptet kommer det se ut så här:

$ bash argument.sh anna lena lotta jessica
Ett flicknamn är anna
Ett flicknamn är lena
Ett flicknamn är lotta
Ett flicknamn är jessica

Slumpade tal

Slumpade tal är användbara när vi vill att slumpen skall ha en avgörande roll i våra skript, till exempel slå en tärning. Bash har en variabel som ger oss slumpade tal mellan 0 och 32767: $RANDOM .

$ echo $RANDOM
4716
$ echo $RANDOM
10055

Om vi vill ha ett slumpat tal mellan 0 och 100 låter vi $RANDOM slumpa ett tal och sedan använder vi modulus för att få resten vid en division:

$ echo $((RANDOM%101))
85

För att få ett tal mellan 1 och 100 skriver vi:

$ echo $((RANDOM%100+1))
38

Först tar vi ett slumpat tal med $RANDOM, sedan kör vi det genom modulus 100 vilket ger oss ett tal mellan 0 och 99. Eftersom vi ville ha ett tal mellan 1 och 100 adderar vi 1 till talet vi slumpat fram.

En tärning där vi vill ha ett värde mellan 1 och 6 slumpar vi fram enligt:

$ echo $((RANDOM%6+1))
4

Om vi vill ha ett slumpat tal mellan 20 och 40 börjar vi med att använda modulus 21 (vilket ger oss tal mellan 0 och 20, 21 olika värden) och sedan adderar vi 20:

$ echo $((RANDOM%21+20))
35

Vi måste alltså hålla koll på vad vår offset är (20) och hur många värden från det vi vill nå (21). Om vi vill ha ett slumpat tal mellan 50 och 80 är det 31 som är hur långt vi vill (80 - 50) nå och vår offset är 50 (där vi skall börja):

$ echo $((RANDOM%31+50))
75

Köra program och fånga upp dem från skriptet

Vi kan köra program och jämföra deras utmatning mot något villkor i en if-sats eller tilldela en variabel värdet av vad kommandot matar ut. Detta gör vi genom att skriva kommandot inom $( ), till exempel $(ls) eller $(ls -l).

För att kontrollera om användaren är inloggad som root kan vi skriva så här i vårt skript:

if [ $(id -u) -ne 0 ] ; then
    echo "Du måste köra det här skriptet som root."
    exit 1
fi

Ett bättre sätt att kontrollera om skriptet körs som användaren root är [ $EUID -ne 0 ]. Där $EUID innehåller användar-id för den användare som kör skriptet. Användaren root har användar-id noll (0).

Vi kan tilldela en variabel värdet av vad ett program returnerar:

DATUM=$(date +%Y%m%d)
echo ${DATUM}
touch ${DATUM}.txt

Alla program som körs i systemet returnerar en exit code, precis som vi har gjort med våra skript: exit 0. Noll (0) är standard för att visa att allt gick bra, programmet lyckades utan fel. Exit värden mellan 1 och 255 är felkoder som har olika betydelser. Vanligast är att 1 betyder fel, men många program har olika värden beroende på vilket fel som uppstod. På så sätt har vi lättare att fånga upp fel i våra skript. Vi tittar på det från kommandoraden:

$ touch /tmp/hej
$ echo $?
0
$ touch /proc/hej
touch: kan inte beröra "/proc/hej": Filen eller katalogen finns inte
$ echo $?
1

Eftersom vi har rättigheter att skapa filer i katalogen /tmp kommer det första kommandot lyckas. I katalogen /proc däremot har vi inga skrivrättigheter och kommandot touch misslyckas, vi får tillbaka en etta (1) som indikerar ett fel.

Vi får också ett felmeddelande som inte är så fint att ha med i ett skript, vi skulle kunna dirigera om STDERR till /dev/null för att dölja felmeddelandet:

$ touch /proc/hej 2> /dev/null
$ echo $?
1

/dev/null är en speciell fil som ofta används för att kasta bort utskrifter från skript. Allt vi dirigerar om till /dev/null försvinner, som i ett svart hål. Om vi läser /dev/null får vi enbart tillbaka specialtecknet end of file (EOF).

Om vi vill använda exitvärdena i våra skript kan vi till exempel skriva:

touch /proc/hej 2> /dev/null
if [ $? -ne 0 ] ; then
    echo "$0: fel vid skapandet av filen"
    exit 1
fi

Funktioner

Funktioner använder vi för att återanvända kod. Tänk dig att vi vill göra samma sak flera gånger i ett skript, men på olika ställen i skriptet. Varför skriva om samma kod om och om igen då?

Funktioner skrivs så här:

function funktionens_namn {
    vad funktionen skall göra...
}

En enkel funktion i ett skript kan skrivas så här:

#!/usr/bin/env bash

function funk {
    echo "Hej från funktionen funk"
}
        
funk
exit 0

När vi kör ovanstående skript kommer vi anropa funktionen funk som kommer skriva ut Hej från funktionen funk på skärmen.

Variabler som skapas i skriptet är som standard globala, det vill säga: de är tillgängliga överallt i skriptet. Vi kan alltså använda variabler i våra funktioner, som i det här skriptet:

#!/usr/bin/env bash
    
function ditt_namn {
    echo "Ditt namn är $NAMN"
}
read -p "Mata in ditt namn: " NAMN
ditt_namn
exit 0

I skriptet ovanför kommer vi be användaren att mata in sitt namn, som vi lagrar i variabeln $NAMN. Sedan anropar vi funktionen ditt_namn som kommer använda variabeln $NAMN för att skriva ut det inmatade namnet.

Variabler vi skapar i funktionen kommer också att vara globala, vårt skript kommer alltså åt dem och kan använda dem. Detta hindrar vi med kommandot local som skapar en lokal variabel i funktionen. Den lokala variabeln är inte tillgänglig utanför funktionen. Vi tittar på ett exempel:

#!/usr/bin/env bash

function lokalvar {
    local MAGISK=42
    echo "Inne i funktionen har vi talet $MAGISK"
}
lokalvar
echo "Det magiska talet är $MAGISK"
exit 0

I exemplet ovanför kommer vi att anropa funktionen lokalvar som deklarerar en lokal variabel, $MAGISK, som vi sedan skriver ut på raden under med echo. Efter all kod i funktionen körts hoppar skriptet tillbaka till raden under funktionsanropet (lokalvar i skriptet) och skriver ut $MAGISK igen med echo. Den här gången är $MAGISK tom, eftersom vi inte har deklarerat variabeln och $MAGISK i funktionen lokalvar är en lokal variabel för just den funktionen.

Vi har tidigare tittat på hur vi kan skicka in argument till skript, detta fungerar för funktioner också. $# innehåller hur många argument som skickas, $1 innehåller det första argumentet och så vidare.

Vi börjar med att skapa ett skript med funktionen summa som adderar två tal och skriver ut summan:

#!/usr/bin/env bash
        
function summa {
    SUMMA=$(( $1 + $2 ))
    echo "Summan är $SUMMA"
}
summa 10 20
read -p "Mata in tal1: " TAL1
read -p "Mata in tal2: " TAL2
summa $TAL1 $TAL2
exit 0

Först anropar vi funktionen summa med två tal: summa 10 20 och funktionen skriver ut summan av de två talen. Därefter ber skriptet användaren om två tal. När användaren har matat in dem anropas funktionen summa med de två inmatade talen.

Returvärden

Funktioner kan returnera värden, precis som våra skript kan returnera värden med exit. Funktionerna använder sig av return istället och kan returnera ett värde mellan 0 och 255. Returvärdet fångar vi upp med variabeln $?.

#!/usr/bin/env bash
        
function summa {
    SUMMA=$(( $1 + $2 ))
    return 0
}
summa 10 30
echo "Returvärdet är: $?"
echo "Summan är: $SUMMA"
exit 0

I skriptet ovanför skickar vi in talen 10 och 30 till funktionen summa som räknar ut summan av talen och lagrar svaret i variabeln $SUMMA. Sedan returnerar vi noll med return 0.

Vi fångar upp returvärdet med variabeln $? på raden under anropet till funktionen och variablen $SUMMA på nästa rad.

Tänk på att returvärden bara kan returnera heltal mellan 0 och 255. För att fånga upp resultat av beräkningar och liknande i funktioner får vi jobba med variabler (som $SUMMA) i exemplet ovanför.

Source

Ibland vill vi inkludera ett annat skript i det skript vi kör just nu. Det kan till exempel vara ett skript med funktioner och konfigurering som vi vill använda i flera andra skript. I Bash använder vi source för att inkludera andra skript i det skript vi kör.

När vi använder source läser vårt skript in det andra skriptet och kör det direkt. Vi tittar på ett exempel.

Vi börjar med att skapa skriptet source2.sh:

#!/usr/bin/env bash
echo "Hej från source2.sh"
exit 0

Vi fortsätter med att skapa skriptet source1.sh:

#!/usr/bin/env bash
echo "Hej från source1.sh"
source source2.sh
exit 0

Nu kör vi skriptet source1.sh och ser vad som händer:

$ bash source1.sh
Hej från source1.sh
Hej från source2.sh
$

Om vi vill använda oss av variabler mellan skripten ser det ut så här. Vi ändrar source2.sh till:

#!/usr/bin/env bash
echo "Hej $NAMN från source2.sh"
exit 0

Sedan ändrar vi source1.sh till:

#!/usr/bin/env bash
read -p "Mata in ditt namn: " NAMN
echo "Hej från source1.sh"
source source2.sh
exit 0

Nu kör vi source1.sh och ser vad som händer:

$ bash source1.sh
Mata in ditt namn: jonas
Hej från source1.sh
Hej jonas från source2.sh
$

Vi skapar alltså en variabel ($NAMN) i skriptet source1.sh som sedan används i skriptet source2.sh.

source kan också förkortas som en punkt (.) som i . skript2.sh. Notera mellanslaget mellan punkten och skriptets namn.