Scripting

Wat is een script ?

In deze context bedoel ik met script een programma dat bestaat uit leesbare tekst die direct door de computer begrepen en uitgevoerd wordt zonder dat het van tevoren in computernotatie (binary) is omgezet. Zo'n script kan andere programma's uitvoeren, sommetjes maken, dingen op het scherm laten zien, dus eigenlijk alles dat je van een computer zou verwachten.

Waarom is een script dan bijzonder ? Vooral omdat het een zeer krachtige manier is van combineren van al bestaande programma's. In het simpelste geval is een script gewoon een lijstje van programma's die na elkaar afgewerkt worden. Complexere scripts kunnen bijvoorbeeld gemaakt worden om een configuratie van programma's met diverse instellingen en afhankelijkheden in een bepaalde volgorde op te starten.

Script Executable
Wordt uitgevoerd door een host (bv. shell) Wordt uitgevoerd door het besturingssysteem
Leesbare 'text' Machine-specifieke code
Uitvoering on-the-fly (interpreteren) Uitvoering na compilatie (vaak sneller)
Gebruik van reeds bestaande programma's en commando's Gebruik van libraries

Een paar voorbeelden van script-talen

UNIX shell-scripting

Wat is een UNIX-shell ?

Het UNIX besturingssysteem (a.k.a. operating system of OS) is opgebouwd uit een kernel met daaromheen verschillende lagen die op hun eigen manier en niveau functionaliteit aan die kernel toevoegen. De kernel zorgt dat alle processen op tijd aan de beurt komen en zorgt voor het beheer van het geheugen.

Heel dicht tegen de kernel aan bevindt zich de software die externe apparatuur bestuurt, bijvoorbeeld een printer driver, besturingen van filesystemen zoals harddisk, CD en je digitale camera, een netwerk-driver of een driver die je grafische kaart bestuurt zodat je beelden op je monitor ziet.

Op hogere niveaus vind je allerlei processen die onderhoudstaken uitvoeren en pas op de hoogste niveaus, ver van de kernel, vind je de programma's die wij gebruiken. Een shell bevindt zich op dat niveau en geeft ons de mogelijkheid om met het systeem te communiceren.

Opbouw van een UNIX shell script

Hier zijn een paar voorbeeldscripts die je meteen kunt uitproberen.
Download deze file en pak die uit met de volgende twee commando's:

gzip -d script_examples.tar.gz
tar xvf script_examples.tar
en ga dan naar de directory 'scripts' met 'cd scripts' en kijk met 'ls' wat erin staat.

Hash

Het eerste character dat je in alle voorbeeldscripts ziet is # ook wel genoemd hash. Dit geeft aan dat de rest van de regel als commentaar beschouwd wordt, dus niet wordt uitgevoerd.

HashBang

De combinatie van hash (#) en bang (!) wordt wel HashBang genoemd. Het commando of programma dat hierna op dezelfde regel staat wordt uitgevoerd en krijgt het volledige script weer als input. Op deze manier kun je aangeven met welk programma het script uitgevoerd moet worden.

Zoals je ziet wordt het volledige pad (/bin/bash) genoemd, dat moet ook omdat de kernel je PATH-variabele niet gebruikt.

Het mechanisme werkt als volgt:

  1. je hebt een executable script (chmod 755)
  2. roep het script aan alsof het een executable is
  3. de kernel ziet hashbang (#!) en voert programma uit (bv. /bin/bash)
  4. programma krijgt volledig script als input
  5. programma moet # als comment zien (vanwege hashbang) en voert de overige regels uit

Twee voorbeeldjes, cat_myself en more_myself:

#! /bin/cat
Dit script laat zichzelf zien
#! /bin/more +2
Dit script laat zichzelf zien, behalve de eerste regel

Een eenvoudig scriptje

#! /bin/bash

#
# Dit script zegt hallo
#

echo "hallo en een prettige dag verder"

Om dit script uit te voeren zet je het in een gewone text file, dus niet Word of zoiets. Laten we dit script "hallo" noemen.
Om het als uitvoerbaar programma te markeren geef je met de file-permissies aan dat het de status van executable krijgt:

chmod 755 hallo

Je kunt het nu uitvoeren door te typen

./hallo

Scriptje met een parameter

#! /bin/bash

#
# Dit script geeft een bericht als je je naam opgeeft als parameter
#
if [ $# -lt 1 ]; then
echo "$0: Hoe heet jij ?"
exit
fi

NAAM=$1

echo
echo "Ik mag gebruiker ${NAAM} feliciteren met deze groet:"
echo
banner Hallo ${NAAM}

Command line parameters

In scripts zie je diverse dollartekens. Die duiden bijna altijd op variabelen of parameters. Parameters (ook wel genoemd "command-line parameters") zijn de argumenten die je aan het script geeft door ze na het script te typen.
Het volgende stukje script verwacht drie parameters en laat deze zien. Als je er te weinig geeft krijg je een foutmelding.
#! /bin/bash

if [ $# -lt 3 ]; then
  echo "$0: Geef svp drie parameters"
else
  echo "Parameter 1: " $1
  echo "Parameter 2: " $2
  echo "Parameter 3: " $3
fi

De variabele $0 bevat de naam van het script, dus bv. 'hallo'.
$# geeft aan hoeveel parameters de gebruiker heeft opgegeven.

Nog iets nieuws: else. Hiermee geef je een alternatief wanneer de conditie bij de if niet waar is. In dit script betekent dat: als de gebruiker minder dan drie argumenten geeft wordt gevraagd om drie argumenten. Als de gebruiker drie of meer argumenten geeft wordt het stukje code tussen 'else' en 'fi' uitgevoerd.

Parse command line parameters with 'getopts'

http://wiki.bash-hackers.org/howto/getopts_tutorial

Variabelen

In het volgende script zie je FILENAME en SAMPLERATE. Dit zijn variabelen die een waarde krijgen. FILENAME krijgt als waarde de naam van de file, maar zonder een eventuele extensie .mid want die wordt er met 'basename' af gehaald. Wat je in de regel met basename verder ziet zijn backticks ( ` ), die voeren het programma dat ertussen staat uit en geven het resultaat terug aan het script. Dit zijn dus geen gewone quotes !!
#! /bin/bash

if [ $# -lt 2 ]; then
echo "$0: Geef een file en samplerate"
exit
fi

FILENAME=`basename $1 .wav`
SAMPLERATE=$2

echo "De file ${FILENAME} wordt geconverteerd naar ${SAMPLERATE} Hz"

Alternatief voor backticks

Backticks werken in alle shells, ook oudere types. Een mooi alternatief dat je kunt gebruiken in BASH is $()

Met backticks:
echo `basename song.wav .wav`
Met $():
echo $(basename song.wav .wav)

Files creëren, hernoemen en deleten

Het scriptje 'create_some_files' maakt een paar lege files aan:
#! /bin/bash

touch a.old b.old c.old d.old
Voer het uit en kijk welke files nu in de directory staan. Vervolgens voer je het scriptje 'rename_some_files' uit:
#! /bin/bash


for f in [a-c].old; do
  newname=`basename $f .old`.new
  mv $f $newname
done
en kijk opnieuw welke files er in de directory staan.

Index van songs

wc -l songs/*
./generate_index songs

Dit is het script generate_index:

#! /bin/bash

#
# This script creates an index of song files
#

if [ $# -lt 1 ]; then
echo "$0: Geef een directory met songfiles"
exit
fi

find $1 -type f -exec sed '2,$d' {} \;

sed '2,$d' betekent: delete alle regels vanaf regel 2 t/m het einde van de file. Zo houd je dus alleen de eerste regel over.

Redirection en pipes

Soms wil je de output van een programma of script in een file opslaan, bijvoorbeeld om het later nog eens te kunnen bekijken of om het in een editor te bewerken. Met redirection kun je de output van een programma een andere kant op sturen dan naar de gebruikelijke terminal.

Opslaan van het stukje tekst-output uit het eerste scriptje in een file gaat als volgt:

hallo > nieuwefile

Wil je de tekst-output van een programma onderdrukken, bijvoorbeeld omdat er zoveel tekst over het scherm schiet dat je de output van je eigen script niet meer herkent dan kun je de in veel gevallen de output van het programma met "1>" of "2>" naar het UNIX afvoerputje "dev/null" sturen.
Probeer een van de volgende regels:

programma <parameters>  1> /dev/null
programma <parameters>  2> /dev/null

Soms wil je de output van een programma direct in een ander programma gebruiken. Stel dat je een lijst van files in een directory wilt zien, gesorteerd naar aantal regels.

wc -l * | sort -n
  of
wc -l * | sort -n | awk 'BEGIN {FS=" "} {print $1 "\t--- " $2}'

In het volgende voorbeeld zie je dat vanaf EOF tot de tweede EOF als invoer wordt gebruikt voor bc. EOF is een zelf bedachte naam. Je mag daar ook iets anders neerzetten, als je het maar consequent doet.


#! /bin/bash

bc << EOF
scale=4
$@
quit
EOF

De kracht van read

#! /bin/bash

ls -1 |
while read name; do
  echo $name
  say $name
done

Alle regels uit een file regel voor regel bewerken:
cat filename |
while read name; do
  echo $name
  say $name
done

of zo:

while read name; do
  echo $name
  say $name
done < filename

De tweede oplosing is soms beter omdat die geen aparte sub-shell start.

Nog een mogelijkheid om zonder 'read' hetzelfde te bereiken:

for f in `ls -1`; do
  echo $f
  say $f
done

N.B.: say is een OSX programma. Linux gebruikers kunnen
espeak gebruiken.


Maak een textfile "klok" met deze inhoud:
2
5
1
3

Voer dit script uit dat gebruik maakt van de file "klok":
#! /bin/bash

cat klok |
while read DELAY; do
  echo "I will sleep for ${DELAY} seconds"
  say "I will sleep for ${DELAY} seconds"
  sleep $DELAY
done

Test, beslissingen en booleans

Beslissingen met if zijn we al een paar keer tegengekomen. Net als bij andere programmeertalen geeft je bij if een expressie die waar (true) of onwaar (false) is. Als het true is wordt de code na de if uitgevoerd en anders niet.

Je kunt -optioneel- een else gebruiken voor een complementair stuk code.

Maar hoe werkt dat nou precies met true en false? Er zijn meerdere manieren:

1. If met een expressie

BASH heeft een nogal cryptische manier om een expressie te testen. Dat doe je door er [ en ] omheen te zetten. Let op: er moeten spaties voor en na [ en ] staan.

TEMPERATURE=-10

if [ $TEMPERATURE -lt -5 ]; then
  echo "Elfstedentocht"
else
  echo "Zeiltocht"
fi
2. If met een boolean
DEBUG=true

if $DEBUG ; then
  echo "Debug"
fi

In beide gevallen kun je het gedrag omkeren met ! bijvoorbeeld zo:

if ! [ $TEMPERATURE -lt -5 ]; then
if ! $DEBUG ; then...

Alternatief voor testen met [ ]

Met 'test' kun je dezelfde tests uitvoeren als met [ ] en soms beter leesbaar. Voorbeeld:
if test -z $var1 && test -x file; then
    echo blah
fi

Rekenen

filenumber=8
echo $filenumber
filenumber=$(( filenumber + 1 ))
printf %04d $filenumber
Alternatief:
filenumber=8
echo $filenumber
(( filenumber = filenumber + 1 ))
printf %04d $filenumber

Loopjes

Hieronder staan twee manieren om een loop te maken.
#!/bin/bash
X=0

while [ $X -lt 1200 ]
do
  let X=X+120
  echo $X
done

#!/bin/bash

for i in `seq 1 10`;
do
  echo $i
done
Pas het script zo aan dat het elke halve seconde een nieuw getal laat zien.

Functies

Als je een stukje code hebt dat een duidelijke functie vervult dan kun je er een functie van maken. Je plakt er dan eigenlijk een label op waarmee je het vanuit elke plaats van je script weer kunt aanroepen. Het maken van een functie in Bash doe je zo:
my_function() {
LOCALVAR1=$1
LOCALVAR2=$2
  <command sequence>
}
Hier zijn de LOCALVARs variabelen die alleen binnen de functie bekend zijn. De genoemde command sequence is het rijtje instructies dat de body (het uitvoerende deel) van je functie vormt.

Aanroepen van de functie doe je zo:
GLOBALVAR1="a b c"
GLOBALVAR2=10
my_function $GLOBALVAR1 $GLOBALVAR2

BASH string replacement

BASH substring replacement: ${var//from/into} for all or ${var/from/into} for first one only

Commando's en programma's die je veel gebruikt bij scripting

Command Meaning Examples
echo print string to screen echo blabla
echo "Hello world"
awk text processing
awk 'BEGIN {FS=" "} {print $1 $2}'
awk 'BEGIN {FS=":"} {print substr($3,5,length($3))}'
awk 'BEGIN {FS="<"} {print $2}' index.html | awk 'BEGIN {FS=">"} {print $1 "," $(NF-1)}'
sed stream editor sed 's/html/xml/g' index.html
if conditional execution
if [ $# -lt 1 ]; then
  echo "Geef een MIDI-file"
  exit
fi
Other comparison operators:
-lt    <
-gt    >
-le    <=
-ge    >=
-eq    ==
-ne    !=
-z    length of string is zero
=~ matches with a regular expression
if $DEBUG ; then
    echo "Debug"
fi
test expression
[ expression ]   
evaluate expression
[ $year -gt 2008 ]
[ $year -gt 2008 ] && [ $month == "July" ]

# test if argument is not empty or file exists
[ ! -z $argument ]

# test multiple conditions
if ([ $1 == "a" ] || [ $1 == "b" ]) && ( [ $2 == "c" ] || [ $2 == "d" ]); then
 echo "waar"
else
 echo "onwaar"
fi

# test if input is a file or a terminal
if [ -t 0 ]; then
echo script running interactively
else
echo stdin coming from a pipe or file
fi

# test if output is a file or a terminal
if [ -t 1 ]; then
echo output going to the screen
else
echo output redirected to a file or pipe
fi
else alternative for conditional
excecution
if [ $1 == "aiff" ]; then
  echo "Processing AIFF files"
else
  echo "Processing other files"
fi
elif alternative condition for
conditional excecution
if [ $1 == "aiff" ]; then
  echo "Processing AIFF files"
elif [ $1 == "wav" ]; then
  echo "Processing WAVE files"
else
  echo "Processing other files"
fi
case conditional execution
with multiple conditions
case $filesize in
  [0-9])
    echo "Small file"
  ;;
  1[0-9])
    echo "Medium size file"
  ;;
  2[0-9])
    echo "Large file"
  ;;
  *)
  echo "File too big"
  ;;
esac
while read name; do
  case $name in
    5)
      echo "Nummer 5"
    ;;
    "een")
      echo "Nummer een"
    ;;
    "twee")
      echo "Nummer twee"
    ;;
    "einde")
      exit
    ;;
    *)
    echo "Ken ik niet"
    ;;
  esac
done
for loop construct for file in *.html ; do echo $file ; done
basename strip e.g. extension off a filename    basename beethoven.wav .wav
${filename#*.} strip the basename off a filename More info
${filename##*.} keep only the extension of a filename More info
sleep suspend processing for some time sleep 0.5


UNIX scripting: een aantal dingen om op te letten

Aanpassen van de PATH variabele in bash om ook de current directory '.' erin op te nemen:
In .bash_profile zet je:
. ~/.bashrc

In .bashrc zet je:
PATH=.:$PATH

Je kunt ook mijn .bashrc downloaden, daar staat het al in. Deze vind je
een niveau hoger in deze website.


Oefeningen