Programmeren in de Shell

ArticleCategory:

UNIX Basics

AuthorImage:

Katja & Guido Socher

TranslationInfo:

original in en Katja en Guido Socher 

en to nl Floris Lambrechts

AboutTheAuthor:

Katja is de Duitse hoofdredactrice van LinuxFocus. Ze houdt van Tux, film & fotografie en de zee. Haar homepage vind je hier.

Guido is al lang een Linux-fan omdat het gemaakt wordt door eerlijke en open mensen. Dat is één van de redenen waarom we het open source noemen. Zijn homepage staat op linuxfocus.org/~guido.

Abstract:

In dit artikel leggen we, met veel voorbeelden, uit hoe je kleine shell-scripts kunt schrijven.

ArticleIllustration:

Illustratie

ArticleBody:

Waarom programmeren in de shell?

Alhoewel er vele grafische interfaces bestaan voor Linux, is de shell nog altijd zeer handig. Het is niet gewoon een verzameling commando's maar ook een echt goede programmeertaal. Je kunt er veel dingen mee automatiseren (bijvoorbeeld systeemadministratie) en je bereikt er snel resultaten mee. Dat maakt het geschikt om snel nieuwe ideëen te testen of om kleine programma's te maken waarbij zaken als aanpasbaarheid, gemak van onderhoud en porteerbaarheid primeren boven efficiëntie.
Goed, laten we eens kijken hoe het werkt:

Een script maken

In Linux kun je kiezen uit verschillende shells, maar de meeste mensen gebruiken gewoon bash (de bourne again shell) voor programmeertaken omdat die vrij beschikbaar is en gemakkelijk in het gebruik. Alle scripts in dit artikel gebruiken bash (maar ze zullen meestal ook wel werken met zijn oudere zus, de bourne shell).
Om de shell-programma's (oftewel scripts) te schrijven gebruiken we, net als bij andere programmeertalen, een gewone editor naar keuze, bijvoorbeeld nedit, kedit, emacs, vi...
Ons programma begint met de volgende regel (op de eeste lijn in het bestand):
    #!/bin/sh 

De tekens #! vertellen het systeem dat de tekst die volgt (in dit geval /bin/sh) een programma is dat de rest van het bestand zal uitvoeren.
Wanneer je script geschreven is, moet je het nog executable (uitvoerbaar) maken voor je het kunt gebruiken.
Om dat te doen doe je
    chmod +x bestandsnaam
Je voert het vervolgens uit door te typen: ./bestandsnaam

Commentaar

Commentaar in shell-programma's begint met # en geldt tot het einde van de regel. We raden je echt aan om ze te gebruiken. Als je een oud script wilt aanpassen zul je snel merken dat ze echt helpen om te begrijpen wat het script doet en hoe het werkt.

Variabelen

Net als in andere talen kun je niet zonder variabelen. In de shell zijn alle variabelen van het type string en je hoeft ze niet afzonderlijk declareren. Om een waarde toe te wijzen aan een variabelen doe je:
    varnaam=waarde
Om de waarde uit te lezen plaats je een dollarteken voor z'n naam:
    #!/bin/sh
    # assign a value:
    a="hello world"
    # now print the content of "a":
    echo "A is:"
    echo $a
Typ deze tekst in je editor en bewaar het bijvoorbeeld als "eerste". Dan maak je het uivoerbaar (chmod +x eerste in de shell) en voer je het uit (./eerste).
Dit script geeft als uitvoer:
    A is:
    hello world
Soms kun je variabelen verwarren met de rest van de tekst:
    num=2
    echo "dit is de $numde"
Dit geeft niet, zoals je misschien zou willen, "dit is de 2de" maar wel "dit is de " omdat de shell hier gaat zoeken naar een variable met de naam "numde" die nog niet bestaat en dus leeg is. Om duidelijk te maken dat we de variable num bedoelen, zetten we er haakjes rond:
    num=2
    echo "dit is de ${num}de"
Deze keer is het wel goed: als uitvoer krijgen we "dit is de 2de".

Een aantal variabelen worden altijd automatisch aangemaakt. We zullen ze later in het artikel verder bespreken wanneer we ze nodig hebben.

Als je wiskundige uitdrukkingen wilt gebruiken, doe je een beroep op een programma zoals expr (zie de tabel hieronder).
Naast de normale shell-variabelen, die enkel gelden binnen het programma zelf, zijn er ook omgevingsvariabelen (environment variables). Een variabele voorafgegaan door het woord export noemen we een omgevingsvariabele. Deze zullen we hier niet bespreken omdat ze meestal enkel gebruikt worden in login-scripts.

Shell-commando's en controlestructuren

Er zijn drie soorten opdrachten in shell-scripts:

1) Unix-commando's:
Alhoewel een shell-script eender welk commando kan oproepen, zijn er toch een paar die meer worden gebruikt dan andere. Meestal gaat het dan om programma's die omgaan met bestanden of met tekst.
Syntax v.h. commando Doel
echo "een tekst" schrijft een tekst op het scherm
ls geeft een overzicht van de aanwezige bestanden
wc -l bestand
wc -w bestand
wc -c bestand
telt het aantal regels in een bestand of
telt het aantal woorden of
telt het aantal tekens
cp bronbestand doelbestand kopieert het bronbestand naar het doelbestand
mv oudnaam nieuwnaam hernoemt of verplaatst het bestand
rm bestand verwijdert een bestand
grep 'patroon' bestand kijkt of het patroon voorkomt in een bestand
Voorbeeld: grep 'zoekdezetekst' bestand.txt
cut -b kolnum bestand filtert data uit een bestand met vaste kolombreedte
Voorbeeld: toon teken 5 t/m 9
cut -b5-9 bestand.txt
Niet te verwarren met "cat", dat iets heel anders doet
cat bestand.txt schrijft bestand.txt naar stdout (standard output, het sherm dus)
file eenbestand geeft aan welk type bestand eenbestand is
read var vraagt de gebruiker om invoer en slaat het op in een variabele (var)
sort bestand.txt sorteert lijnen in bestand.txt
uniq verwijdert identieke lijnen, in combinatie met sort omdat uniq enkel opeenvolgende lijnen bekijkt
Voorbeeld: sort bestand.txt | uniq
expr doet aan wiskunde in de shell
Voorbeeld: tel 2 op bij 3
expr 2 "+" 3
find zoekt bestanden
Voorbeeld: zoeken op naam:
find . -name bestandsnaam -print
Dit commando heeft vele mogelijkheden, spijtig genoeg teveel om in dit artikel te bespreken.
tee schrijft data naar stdout (het scherm) én naar een bestand
Meestal op deze manier:
eencommando | tee hetbestand
Het schrijft de uitvoer van eencommando naar het scherm en naar hetbestand.
basename bestand geeft enkel de bestandsnaam van het gegeven bestand, het verwijdert het directory-pad
Voorbeeld: basename /bin/tux
geeft gewoon tux
dirname bestand geeft enkel de naam van de directory, niet die van het bestand
Voorbeeld: dirname /bin/tux
geeft enkel /bin
head bestand print de eerste regels van het bestand op het scherm
tail bestand print de laatste regels van het bestand op het scherm
sed in essentie is sed een programma voor het zoeken en vervangen van tekst. Het leest tekst van de standaard invoer (bijv. uit een pipe) en schrijft het resultaat naar stdout (meestal het scherm). Het zoekpatroon is een reguliere expressie (meer info in de referenties onderaan). Dit zoekpatroon mag je niet verwarren met de wildcards van de shell. Om in een bestand de tekst "linuxfocus" te vervangen door "LinuxFocus" doe je:
cat bestand.txt | sed 's/linuxfocus/LinuxFocus/' > nieuwbestand.txt
Dit vervangt de eerste "linuxfocus" in een regel door "LinuxFocus". Als de tekst meerdere keren in 1 regel voorkomt:
cat bestand.txt | sed 's/linuxfocus/LinuxFocus/g' > nieuwbestand.txt
awk awk wordt meestal gebruikt om bepaalde velden uit een tekstregel te lezen. Standaard worden de velden onderscheiden door een spatie. Een andere field separator kan je instellen met de optie -F.
 cat bestand.txt | awk -F, '{print $1 "," $3 }' 

Hier gebruiken we de komma (,) om de velden te onderscheiden en printen we het eerste en derde ($1 $3) veld. Als bestand.txt regels heeft zoals
Adam Bor, 34, India
Kerry Miller, 22, USA

dan geeft dit commando:
Adam Bor, India
Kerry Miller, USA

Awk kan nog veel meer maar dit is een veel voorkomend gebruik.@@@


2) Concepten: Pipes, redirection en backtick
Dit zijn geen echte commando's, maar het zijn wel uiterst belangrijke concepten.

pipes (|) sturen de uitvoer (stdout) van een programma naar de invoer (stdin) van een ander programma.
    grep "hello" bestand.txt | wc -l
zoekt alle regels met de tekst "hello" in bestand.txt en telt vervolgens deze regels.
De uitvoer van het commando grep wordt gebruikt als invoer voor wc. Met pipes kun je, binnen redelijke limieten, zoveel opdrachten aan elkaar schakelen als je maar wil.

redirection: schrijft de uitvoer van een opdracht naar een bestand, of voegt data toe aan een bestand.
> schrijft de uitvoer naar een bestand en overschrijft daarbij de oude inhoud.
>> schrijft de uitvoer naar een bestand, maar voegt het er aan het einde aan toe (als het bestand niet bestaat wordt het aangemaakt, maar er wordt nooit iets overschreven).

Backtick
De uitvoer van oprachten kan je ook gebruiken als argumenten voor andere programma's. (Dit is niet hetzelfde als de stdin van hierboven, commandoregelopties zijn de stukken tekst die je meegeeft aan het commando zoals bijvoorbeeld bestandsnamen en opties.) Je kunt dit ook gebruiken om de uitvoer van een commando op te slaan in een variabele.
Het commando
find . -mtime -1 -type f -print
zoekt alle bestanden die in de laatste 24 uur veranderd zijn (-mtime -2 zou 48 uur zijn). Als je nu al deze bestanden in een tar-archief wilt opslaan (archief.tar) dan doe je dat zo:
tar xvf archief.tar bestand1 bestand2 ...
In plaats van dit allemaal zelf te typen, kun je beide opdrachten (find en tar) combineren met backticks. Tar zal dan alle bestanden archiveren die het find commando gevonden heeft:
#!/bin/sh
# De aanhalingstekens zijn backticks (`), geen gewone (') ! 
tar -zcvf gewijzigd.tar.gz `find . -mtime -1 -type f -print`

3) Controlestructuren
Met "if" test je of een bepaalde voorwaarde waar is (exit status is 0, succes). Als dit het geval is, wordt de code in het "then"-gedeelte uitgevoerd:
if ....; then
   ....
elif ....; then
   ....
else
   ....
fi
Meestal gebruik je een speciaal commando (test genaamd) om de if-statements te controleren. Je kunt er stukken tekst (strings) mee vergelijken, testen of een bepaald bestand bestaat, of het leesbaar is, enz.
De "test"-opdracht wordt geschreven als rechte haakjes " [ ] ". Merk op dat de spatie hier belangrijk is: zorg altijd voor spaties rond de haakjes. Voorbeelden:
[ -f "eenbestand" ]  : Test of eenbestand wel echt een bestand is.
[ -x "/bin/ls" ]   : Test of /bin/ls bestaat en of het uitvoerbaar is.
[ -n "$var" ]      : Test of de variabele $var wel een waarde heeft.
[ "$a" = "$b" ]    : Test of de variabelen "$a" en "$b" hetzelfde zijn.
Doe een keer "man test" en je krijgt een lange lijst met alle soorten testopdrachten voor vergelijkingen en bestanden.
Je kunt ze makkelijk gebruiken in een shell script:
#!/bin/sh
if [ "$SHELL" = "/bin/bash" ]; then
  echo "your login shell is the bash (bourne again shell)"
else
  echo "your login shell is not bash but $SHELL"
fi
De variabele $SHELL bevat de naam van de login-shell en dat is wat we testen door het te vergelijken met de string "/bin/bash".

Shortcut operators
Mensen die C kennen zullen het volgende graag zien :

[ -f "/etc/shadow" ] && echo "This computer uses shadow passwords"

De && kun je gebruiken als een kort if-statement. De rechterkant wordt enkel uitgevoerd als de linkerkant 'waar' is. Je kunt het bekijken als een AND. In dit voorbeeld: "Het bestand /etc/shadow bestaat AND de opdracht wordt uitgevoerd". De OR operator kun je ook gebruiken, hij wordt geschreven als '||'. Een voorbeeld:

#!/bin/sh
mailfolder=/var/spool/mail/james
[ -r "$mailfolder" ] || { echo "Can not read $mailfolder" ; exit 1; }
echo "$mailfolder has mail from:"
grep "^From " $mailfolder

Het script test eerst of het de mailfolder wel kan lezen. Zo ja, dan print het al de lijnen die "From" bevatten. Als het de folder niet kan lezen, dan wordt de opdracht die na de OR staat uitgevoerd. Je kunt deze code zo bekijken "De mailfolder moet leesbaar zijn, of anders stoppen we het script". Het probleem is dat er na de OR slechts 1 commando mag staan, terwijl we er twee nodig hebben.
-print een boodschap over de onstane fout
-verlaat het programma
Om ze samen als 1 opdracht te laten behandelen, plaatsen we ze tussen haakjes.
Eigenlijk kun je zonder de AND en de OR (als je alles oplost met IF-statements), maar dikwijls is het gewoon handiger en logischer om ze toch te gebruiken.

Het case-statement kan gebruikt worden om, met shell- wildcards zoals * en ?, een gegeven string te vergelijken met een aantal mogelijkheden.

case ... in
...) doe hier iets;;
esac

We bekijken een voorbeeld. Het commando file gaat na tot welk type een bepaald bestand hoort:

file lf.gz

geeft:

lf.gz: gzip compressed data, deflated, original filename,
last modified: Mon Aug 27 23:09:18 2001, os: Unix

We gaan dit nu gebruiken om een script te schrijven: smartzip. Smartzip kan bzip2, gzip en zip archieven automatisch uitpakken.

#!/bin/sh
ftype=`file "$1"`
case "$ftype" in
"$1: Zip archive"*)
    unzip "$1" ;;
"$1: gzip compressed"*)
    gunzip "$1" ;;
"$1: bzip2 compressed"*)
    bunzip2 "$1" ;;
*) error "File $1 can not be uncompressed with smartzip";;
esac


Hier zie je dat we een speciale nieuwe variabele gebruiken: $1. Die bevat het eerste argument dat op de commandoregel werd gegeven aan het programma. Stel: we doen
smartzip articles.zip
dan zal $1 de string "articles.zip" bevatten.

Het select-statement is een extentie specifiek voor bash en zeer handig voor interactieve toepassingen. De gebruiker kan een keuze maken uit een lijst van verschillende waardes:

select var in ... ; do
  break
done
.... nu kun je $var gebruiken ....

Een voorbeeld:

#!/bin/sh
echo "What is your favourite OS?"
select var in "Linux" "Gnu Hurd" "Free BSD" "Other"; do
        break
done
echo "You have selected $var"

Hier is wat dit script doet:

What is your favourite OS?
1) Linux
2) Gnu Hurd
3) Free BSD
4) Other
#? 1
You have selected Linux

In de shell heb je volgende lus-statements:

while ...; do
 ....
done

De while-lus zal actief blijven zolang de expressie waarop getest wordt 'true' is. Met het sleutelwoord "break" kun je de lus altijd verlaten. Met "continue" sla je het vervolg van de huidige lus over en begin je meteen aan de volgende iteratie.

De for-lus neemt een lijst van strings (gescheiden door een spatie) en wijst ze toe aan een variabele:

for var in ....; do
  ....
done

Het volgende zal bijv. de letters A tot C op het scherm printen:

#!/bin/sh
for var in A B C ; do
  echo "var is $var"
done

Een meer bruikbaar script, showrpm genaamd, toont een samenvatting van de inhoud van een aantal RPM packages.

#!/bin/sh
# list a content summary of a number of RPM packages
# USAGE: showrpm rpmfile1 rpmfile2 ...
# EXAMPLE: showrpm /cdrom/RedHat/RPMS/*.rpm
for rpmpackage in $*; do
  if [ -r "$rpmpackage" ];then
    echo "=============== $rpmpackage =============="
    rpm -qi -p $rpmpackage
  else
    echo "ERROR: cannot read file $rpmpackage"
  fi
done

Je ziet alweer een nieuwe variabele, deze keer $*. Die bevat al de argumenten van de commandoregel. Als je
showrpm openssh.rpm w3m.rpm webgrep.rpm
doet, dan bevat $* de drie strings "openssh.rpm", "w3m.rpm" en "webgrep.rpm".

GNU bash kan ook omgaan met until-lussen maar in de meeste gevallen heb je die niet nodig en kun je met 'while' en 'for' het gewenste effect bereiken.

Quoten
Voordat de shell argumenten doorgeeft aan een programma, probeert hij wildcards en variabelen te 'expanderen'. Dit houdt in dat de wildcard (bijv. *) vervangen wordt door bestandnamen of dat de variabelen vervangen worden door hun waarde. Om dit gedrag te veranderen moet je quotes gebruiken. Stel, we hebben een aantal bestanden in de huidige directory. Twee van die bestanden zijn van het jpg-type: mail.jpg en tux.jpg.

#!/bin/sh
echo *.jpg

Dit zal worden omgezet naar "mail.jpg tux.jpg".
Quotes (enkele en dubbele aanhalingstekens) zullen deze expansie verhinderen

#!/bin/sh
echo "*.jpg"
echo '*.jpg'

Dit geeft twee keer "*.jpg" op het scherm.
Enkele quotes zijn het meest strikt. Ze voorkomen zelfs de expansie van variabelen, iets wat bij dubbele quotes wel nog gebeurt.

#!/bin/sh
echo $SHELL
echo "$SHELL"
echo '$SHELL'

Dit geeft:

/bin/bash
/bin/bash
$SHELL

Tenslotte is er nog de mogelijkheid de speciale betekenis van bepaalde karakters op te heffen door er een backslash voor te zetten:

echo \*.jpg
echo \$SHELL

Dit geeft:

*.jpg
$SHELL

Here-documents
Here-documents zijn een manier om snel meerdere regels tekst naar een commando te sturen. Het is zeer handig om bijvoorbeeld een helptekst te schrijven in een script, zonder op elke regel een echo opdracht te moeten plaatsen. Een here-document begint met << , gevolgd door een stukje tekst dat ook aanwezig is aan het einde van het here-document. Hier is een voorbeeldscript genaamd ren, dat meerdere bestanden een andere naam geeft. Het gebruikt een here-document voor zijn helptekst.

#!/bin/sh
# we have less than 3 arguments. Print the help text:
if [ $# -lt 3 ] ; then
cat <<HELP
ren -- renames a number of files using sed regular expressions

USAGE: ren 'regexp' 'replacement' files...

EXAMPLE: rename all *.HTM files in *.html:
  ren 'HTM$' 'html' *.HTM

HELP
  exit 0
fi
OLD="$1"
NEW="$2"
# The shift command removes one argument from the list of
# command line arguments.
shift
shift
# $* contains now all the files:
for file in $*; do
    if [ -f "$file" ] ; then
      newfile=`echo "$file" | sed "s/${OLD}/${NEW}/g"`
      if [ -f "$newfile" ]; then
        echo "ERROR: $newfile exists already"
      else
        echo "renaming $file to $newfile ..."
        mv "$file" "$newfile"
      fi
    fi
done

Dit is het meest complexe script tot nu toe. Het eerste if-statement test of we tenminste drie opties hebben meegegeven (de speciale variabele $# bevat het aantal argumenten). Is dat niet het geval, dan sturen we de helptekst naar het cat-commando, die het op het scherm zet. Nadat de tekst weergegeven is, sluiten we het script af. Als er daarentegen wél drie of meer argumenten zijn, wijzen we het eerste toe aan de variabele OLD en het tweede aan de variabele NEW. Dan 'shiften' we de parameters twee keer om het derde argument in de eerste positie van $* te krijgen. Met $* beginnen we de for-lus. Elk van de argumenten in $* wordt nu één voor één toegewezen aan de variabele $file. Hier testen we eerst of het bestand bestaat en dan maken we de nieuwe bestandsnaam door zoeken en vervangen te doen met sed. De backticks gebruiken we om het resultaat toe te wijzen aan de variabele newfile. Nu hebben we alles wat we nodig hebben: de oude bestandsnaam en de nieuwe. Met de mv opdracht hernoemen we vervolgens het bestand.

Functies
Als je begint met meer uitgebreide scripts, zul je al snel niet meer zonder functies kunnen. Ze zien er zo uit:
functienaam()
{
 # binnnen de functie is $1 het eerste argument gegeven aan de functie
 # $2 het tweede ...
 code
}
Je moet functies "declareren" aan het begin van het script voordat je ze kunt gebruiken.

Hier is een script genaamd xtitlebar dat je kunt gebruiken om de naam van een terminal-venster te veranderen. Als je er verschillende tegelijk open houdt, kun je ze zo sneller herkennen. Het script stuurt een 'escape sequence' naar de terminal, die daardoor zijn naam in de titelbalk zal veranderen. Het script maakt gebruik van een functie genaamd 'help'. Zoals je ziet wordt de functie één keer geschreven en twee keer gebruikt:

#!/bin/sh
# vim: set sw=4 ts=4 et:

help()
{
    cat <<HELP
xtitlebar -- change the name of an xterm, gnome-terminal or kde konsole

USAGE: xtitlebar [-h] "string_for_titelbar"

OPTIONS: -h help text

EXAMPLE: xtitlebar "cvs"

HELP
    exit 0
}

# in case of error or if -h is given we call the function help:
[ -z "$1" ] && help
[ "$1" = "-h" ] && help

# send the escape sequence to change the xterm titelbar:
echo -e "\033]0;$1\007"
#

Het is een goede gewoonte om altijd goede help op te nemen in je scripts. Dat maakt het mogelijk voor anderen (en jezelf) om later het script snel te verstaan en aan te passen.

Commandoregelopties
We hebben reeds gezien dat $* en $1, $2 ... $9 de argumenten bevatten die de gebruiker heeft opgegeven op de commandoregel (alles wat getypt werd na de programmanaam). Tot nu toe hadden we slechts een paar van zulke opties nodig (een paar verplichte argumenten en een -h optie voor de help). Maar je zult snel ontdekken dat je al die opties op één of andere manier zult moeten verwerken met een parser. De conventie zegt dat alle optionele parameters voorafgegaan worden door een minteken en vóór andere parameters (zoals bijv. bestandsnamen) ingegeven moeten worden.

Je kunt de parser op vele manieren schrijven. De volgende while-lus met een case statement is een uitstekende oplossing voor een gewone parser:
#!/bin/sh
help()
{
  cat <<HELP
This is a generic command line parser demo.
USAGE EXAMPLE: cmdparser -l hello -f -- -somefile1 somefile2
HELP
  exit 0
}

while [ -n "$1" ]; do
case $1 in
    -h) help;shift 1;; # function help is called
    -f) opt_f=1;shift 1;; # variable opt_f is set
    -l) opt_l=$2;shift 2;; # -l takes an argument -> shift by 2
    --) shift;break;; # end of options
    -*) echo "error: no such option $1. -h for help";exit 1;;
    *)  break;;
esac
done

echo "opt_f is $opt_f"
echo "opt_l is $opt_l"
echo "first arg is $1"
echo "2nd arg is $2"

Probeer het maar! Je kunt het uitvoeren met bijv.:
cmdparser -l hello -f -- -eenbestand1 eenbestand2

Het resultaat is dan
opt_f is 1
opt_l is hello
first arg is -eenbestand1
2nd arg is eenbestand2

Hoe werkt het? Het gaat alle argumenten af en bekijkt ze met een case- statement. Als het een geldige optie is, wordt die opgeslagen in een variabele en 'shift' het de commandoregel één positie. De Unix-conventie is dat opties (dingen die beginnen met een minteken) als eerste komen. Je kunt duidelijk maken dat het einde van de opties bereikt is door een dubbel minteken (--) te plaatsen. Dit is bijvoorbeeld nodig als je met egrep gaat zoeken naar een string met een minteken in:
Zoek naar -xx- in bestand f.txt:
grep -- -xx- f.txt

Onze optie-parser kan ook omgaan met '--' zoals je ziet in de code hierboven.

Voorbeelden

Een algemene basis

Nu we de onderdelen van schell-scripts hebben besproken wordt het tijd om aan de slag te gaan. Alle goede scripts hebben een helpfunctie en je kunt misschien ook onze parser voor het lezen van de opties gebruiken. Daarom raden we aan om een basisscript bij de hand te hebben. We noemen het framework.sh en je kunt het gebruiken als basis (framework) voor andere scripts. Kopieer het telkens je aan een nieuw script begint:
cp framework.sh mijnscript
en begin dan aan het schrijven van je eigen functies.

We bekijken nog twee voorbeelden:

Een binair - naar - decimaal omzetter

Het script b2d converteert een binair getal (bijvoorbeeld 1101) naar zijn decimaal equivalent. Het is een goed voorbeeld van eenvoudige wiskunde met het expr-commando.

#!/bin/sh
# vim: set sw=4 ts=4 et:
help()
{
  cat <<HELP
b2h -- convert binary to decimal

USAGE: b2h [-h] binarynum

OPTIONS: -h help text

EXAMPLE: b2h 111010
will return 58
HELP
  exit 0
}

error()
{
    # print an error and exit
    echo "$1"
    exit 1
}

lastchar()
{
    # return the last character of a string in $rval
    if [ -z "$1" ]; then
        # empty string
        rval=""
        return
    fi
    # wc puts some space behind the output this is why we need sed:
    numofchar=`echo -n "$1" | wc -c | sed 's/ //g' `
    # now cut out the last char
    rval=`echo -n "$1" | cut -b $numofchar`
}

chop()
{
    # remove the last character in string and return it in $rval
    if [ -z "$1" ]; then
        # empty string
        rval=""
        return
    fi
    # wc puts some space behind the output this is why we need sed:
    numofchar=`echo -n "$1" | wc -c | sed 's/ //g' `
    if [ "$numofchar" = "1" ]; then
        # only one char in string
        rval=""
        return
    fi
    numofcharminus1=`expr $numofchar "-" 1` 
    # now cut all but the last char:
    rval=`echo -n "$1" | cut -b 0-${numofcharminus1}`
}
    

while [ -n "$1" ]; do
case $1 in
    -h) help;shift 1;; # function help is called
    --) shift;break;; # end of options
    -*) error "error: no such option $1. -h for help";;
    *)  break;;
esac
done

# The main program
sum=0
weight=1
# one arg must be given:
[ -z "$1" ] && help
binnum="$1"
binnumorig="$1"

while [ -n "$binnum" ]; do
    lastchar "$binnum"
    if [ "$rval" = "1" ]; then
        sum=`expr "$weight" "+" "$sum"`
    fi
    # remove the last position in $binnum
    chop "$binnum"
    binnum="$rval"
    weight=`expr "$weight" "*" 2`
done

echo "binary $binnumorig is decimal $sum"
#

Het algoritme in dit script bekijkt de decimale waarde van alle cijfers, te beginnen van rechts (1,2,4,8,16,..), en voegt het toe aan de som als het cijfer in kwestie een 1 is. Dus "10" wordt:
0 * 1 + 1 * 2 = 2
Om de cijfers uit te lezen gebruiken we de functie lastchar. Dit maakt gebruikv an wc -c om het aantal karakters in een string te tellen en knipt er dan het laatste uit. De chop-functie volgt hetzelfde principe, maar verwijdert eerst het laatste karakter en geeft dan alle voorgaande weer.

Een rotatie-programma voor bestanden
Misschien ben jij zo iemand die alle uitgaande mail opslaat in een bestand. Na een paar maanden wordt het heel groot en wordt je mail-programma er een stuk trager door. Het volgende script rotatefile kan dan nuttig zijn. Het hernoemt de mailfolder, hier outmail genaamd, tot outmail.1 . Als die naam al bezet is, dan hernoemt hij die naar outmail.2 en zo verder...

#!/bin/sh
# vim: set sw=4 ts=4 et: 
ver="0.1"
help()
{
    cat <<HELP
rotatefile -- rotate the file name 

USAGE: rotatefile [-h]  filename

OPTIONS: -h help text

EXAMPLE: rotatefile out
This will e.g rename out.2 to out.3, out.1 to out.2, out to out.1
and create an empty out-file

The max number is 10

version $ver
HELP
    exit 0
}

error()
{
    echo "$1"
    exit 1
}
while [ -n "$1" ]; do
case $1 in
    -h) help;shift 1;;
    --) break;;
    -*) echo "error: no such option $1. -h for help";exit 1;;
    *)  break;;
esac
done

# input check:
if [ -z "$1" ] ; then
 error "ERROR: you must specify a file, use -h for help" 
fi
filen="$1"
# rename any .1 , .2 etc file:
for n in  9 8 7 6 5 4 3 2 1; do
    if [ -f "$filen.$n" ]; then
        p=`expr $n + 1`
        echo "mv $filen.$n $filen.$p"
        mv $filen.$n $filen.$p
    fi
done
# rename the original file:
if [ -f "$filen" ]; then
    echo "mv $filen $filen.1"
    mv $filen $filen.1
fi
echo touch $filen
touch $filen

Hoe werkt het? Eerst controleren we of de gebruiker een bestandsnaam heeft ingegeven. Dan gaan we in een lus die van 9 aftelt tot 1. Bestand 9 wordt hernoemd tot 10, 8 tot 9 en zo verder. Na deze lus noemen we het originele bestand 1 en maken we een leeg bestand aan met de originele naam.

Debuggen

De eenvoudigste manier om te debuggen is natuurlijk met het commando echo. Je kunt het gebruiken om bepaalde variabelen weer te geven die misschien de fout veroorzaken. Waarschijnlijk gebruiken de meeste shell-programmeurs deze methode in wel 80% van de gevallen. Het voordeel van een shell-script is dat je het niet hoeft te re-compileren en snel even een "echo" typen is ook al heel makkelijk.

Nochtans heeft de shell ook een echte debug mode. Als er iets foutgaat in je script "foutscript" kun je het debuggen als volgt:
sh -x foutscript
Dit zal het script uitvoeren en alle opdrachten tonen die worden uitgevoerd, samen met de variabelen en de resultaten van wildcards.

Verder heeft de shell ook nog een mode om te zoeken naar syntax-fouten zonder het script zelf te hoeven uitvoeren. Doe gewoon:
sh -n je_script
Als je geen uitvoer krijgt, dan zijn er geen syntax-fouten gevonden.

We hopen dat je nu snel aan de slag gaat met je eigen shell-scripts. Veel plezier!

Referenties