\chapter{Shell Scripting}
\label{scripting}

Er is niets hinderlijker dan keer op keer lange opdrachten intikken.
Shellscripts zijn eenvoudige tekstbestanden met shellopdrachten die
het tekstbestand omzetten in een comfortabel ``hulpmiddel'' welke 
eenvoudiger in het gebruik is dan het keer op keer weer intikken van
die opdrachten.

Nu we genoeg weten over de werking van de shell, moeten we onze
aandacht gaan richten om het maken van permanente wijzigingen. Het is van belang
eenvoudig te gebruiken omgevingen te hebben om complexe taken te
verrichten, omdat we anders blijven hangen in het dagelijks herhalen van 
talrijke kleine opdrachten. Het vanzelfsprekende aanvangspunt
is het aanpassen van \file{.bashrc} 
en \file{.cshrc} bestanden met een paar aliassen en \"e\"en of
twee prompts.

\section{Initialisatiebestanden van de shell}

We gaan eenvoudige aliassen en shellvariabelen ontwikkelen voor onze
bashshell dat het leven er een stuk eenvoudiger op zal maken wanneer
je op het Linux systeem inlogt. 

\subsection{Initialisatiebestanden voor Bash}

 Begin nu met het wijzigen van \file{.bashrc}.
 Schrijf hierin de volgende aliassen en promptdefinities:


\begin{alltt}
  alias m='less -EMsX '
  alias h='history   | tail -40'
  alias d='ls -l \$*  | less -EMsX'
  alias dt='ls -blt  \$* |less -EMsX'
  alias Dt='ls -aglt \$* |less -EMsX'
  PS1='[\bs{s}]:\bs{u}: '
\end{alltt}

De \term{\$*} staat voor alle resterende variabelen op de opdrachtregel
afgezien van de aanroepende opdracht. Het is gelijk aan alle
opeenvolgende opdrachtregelvariabelen, \term{``\$1 \$2 ...''}.

Nadat je \file{.bashrc} hebt aangepast, moet je een ``\program{source .bashrc}"
uitvoeren om je wijzigingen te initialiseren.
Typ \program{alias} om te bekijken wat de definities van het systeem zijn en
probeer je nieuwe aliassen uit.

\newpage
Denk eraan dat we ook een \file{.bash\_profile} hebben waarvan wordt
verondersteld dat het je omgevingsvariabelen bevat:

\begin{Verbatim}[fontfamily=courier,frame=single,commandchars=\\\{\}]
  # .bash_profile
  # Get the aliases and functions
  if [ -f ~/.bashrc ]; then
          . ~/.bashrc
  fi

  # Gebruikersspecifieke omgeving en opstartprogramma's
  PATH=\$PATH:/usr/local/bin:\$HOME/bin
  ENV=\$HOME/.bashrc
  USERNAME=""

  export USERNAME ENV PATH
\end{Verbatim}

%$
Let op de laatste export opdracht aan het einde van het bestand.

\subsection{Initialisatiebestanden voor Tcsh}

Laten we nu de analoge aliassen in \file{.cshrc} aanmaken:

\begin{alltt}
  alias lester "less -EsX"              # Gebruik less als onze pager..
  alias h    'history \bs!* | tail -40 | lester'
  alias m     lester                    # Gebruik om meer voor te stellen,
                                        # nu is meer less.
  alias d     'ls -l    \bs!* | lester'
  alias dt    'ls -lt   \bs!* | lester' # Sorteer in chronologische volgorde
  alias Dt    'ls -laFt \bs!* | lester' # Zelde als hierboven, toon dot (.xxx)
                                        # bestanden
  set prompt="\%n:\%~: "                  # "user:/location/location/location: "
\end{alltt}

Probeer deze onder tcsh uit na uitvoering

\tcsh{source .cshrc}

Activeer deze wijzigingen door het intikken van \program{source .cshrc}.
Typ \program{alias} om de systeemdefinities te bekijken en probeer er
een aantal van uit. Zowel \program{Tcsh} als \program{Csh} gebruiken
\file{.cshrc} en \file{.login}. Het bestand \file{.login} wordt
normaal gesproken gebruikt om omgevingsvariabelen en informatie
over het \term{pad} in op te slaan, terwijl \file{.cshrc} normaal
gesproken wordt gebruikt voor het bewaren van aliassen en informatie
over de prompt.

%----------------------------------------------------------------------
%----------------------------------------------------------------------
\newpage
\section{Utility Shell Scripts}

Ons eerste doel is een scriptutility aan te maken dat bestanden in een 
directory met de naam \file{/home/\user/address} af zal drukken. 
De syntax zal zijn:

\command{phone.shell}\option{ bestand1 bestand2 ....}

Vanwege de simpele en duidelijke syntax van \program{tcsh},
zullen we eerst de \program{tcsh} taal gebruiken om een oplossing te
construeren. Daarna laten we de equivalente \program{bash} oplossing zien.

\subsection{Tcsh oplossing}
Het eerste wat we willen doen is in het script aangeven dat het een
\program{csh} script is, en wat het acceptabele aantal parameters zijn.
Begin door het wijzigen van een bestand met de naam
\file{$\tilde$/bin/phone.csh}:

\begin{alltt}
  #! /bin/csh -f   # De eerste ``#'' hier, is geen commentaar.
  #
  set a=\$0         # \$0 refereert naar de naam van dit script.
  if(\$#argv < 1 ) then
    echo Usage: \$a:t 'Name'
    exit(1)
  endif
\end{alltt} 

De term \term{set a=\$0} refereert naar de $0^{de}$ component van de
opdrachtregel, wat het opdrachtbestand \program{phone.csh} is in ons geval.
De \term{\$a:t} instrueert \program{csh} alle voorafgaande componenten
van de padnaam te verwijderen, aangezien we niet ge\"interesseerd zijn
in het zien van het volledige pad. Test dit door het aanroepen van de
naam van het script:

\begin{alltt}
   \bash{phone.csh}
    Usage: phone.csh Name
\end{alltt} 

Dacht je eraan \command{chmod} op het script toe te passen zodat het
uitvoerbaar is?

Tot dusverre doet het script niets, behalve dat het ons vertelt of we het
juiste formaat hebben. Vervolgens zullen we een \term{foreach} regel
toevoegen dat ons controle zal geven voor elke bestandsnaam:

\begin{alltt}
 foreach name (\$*)
   echo ~/address/\$name
 end
\end{alltt} 

\newpage
Als het goed is zal je script nu iedere bestandsnaam die je invoert op
de opdrachtregel afdrukken als een controle in onze vooruitgang.
Test nogmaals dit script en controleer of de lus werkt:

\begin{alltt}
   \bash{phone.csh red green blue}

    /home/\user/address/red
    /home/\user/address/green
    /home/\user/address/blue
\end{alltt} 

Nu we weten dat het werkt, vervangen we de \program{echo} opdracht door
de correcte \program{cat} phrase.
Nu we er toch mee bezig zijn, zullen we controleren op het bestaan van
elk bestand met de ( -e filename ) test.
De vorige \term{foreach} clausule zou er nu ongeveer zo uit moeten zien:

\begin{alltt}
  foreach name (\$*)
    if (-e ~/address/\$name) then        # De (-e) vlag controleert op het bestaan.
      cat ~/address/\$name
    else
     echo ``no such file \$name ''
    endif 
    echo ``-------------------------''  # Scheid regels door een lijn.
  end
\end{alltt} 


Het uiteindelijke script zou hier op moeten lijken:

\begin{verbatim}
  #! /bin/csh -f                    # Vertel het systeem dat dit een tcsh script
  # phone.csh                       # Geef als commentaar de bestandsnaam.
  set a=$0                          # $0 refereert naar de naam van dit script.
  if($#argv < 1 ) then              # Als juiste aantal argumenten,
    echo Usage: $a:t 'name'         #   Echo de juiste syntax 
    exit(1)                         #   Exit met foutcode 1
  endif                             # End if
  #
  foreach name ($*)                 # Voor elke naam op de opdrachtregel.
    if (-e ~/address/$name) then    #   Als bestand bestaat (-e),
      cat ~/address/$name           #     Toon het bestand
    else                            #   Anders          
     echo ``no such file $name ''   #     Barf         
    endif                           #   Einde if        
    echo ``---------------------''  # Kosmetische scheidingslijn tussen bestanden.
  end                               # Bye-Bye
\end{verbatim} 

\newpage
Laten we ons programma controlern door een \file{~/address} directory 
aan te maken met daarin een aantal bestanden...

\begin{alltt}
   \bash{mkdir ~/address}
   \bash{cd ~/address}
\bash{echo hello1 zegt 123 \gt hello1 }  # Maakt een bestand met de naam hello1.
\bash{echo hello2 zegt 456 \gt hello2 }  # Maakt een bestand met de naam hello2.
\bash{echo hello3 zegt 789 \gt hello3 }  # Maakt een bestand met de naam hello3.
\bash{phone.csh hello1 hello2 hello3}

   hello1 zegt 123
   ------------------------
   hello2 zegt 456
   ------------------------
   hello3 zegt 789
   ------------------------
\end{alltt} 

\subsection{Bash oplossing}

De \program{bash} oplossing in de oefening is niet zo vanzelfsprekend als
die onder \program{csh} vanwege de bizarre syntax van \program{bash}.
Probeer dit eens uit als je tijd hebt:

\begin{alltt}
 #! /bin/bash                    # Vertel het systeem dat dit een bashscript is
 if [ "\$#" -lt 1 ] ; then       # Als aantal argumenten te weinig is
    echo Usage: \$0 args         # Geef het juiste gebruik
    exit 1                       # Exit met foutstatus
 fi                              # "fi" sluit "if" statements
 for i in \$@ ; do               # Start een "do" loop op de \$@ array
    if [ -f ~/address/\$i ];then # Als het bestand bestaat.
       cat ~/address/\$i  ;      # druk het af,
    else                         # Anders 
      echo There is no file \$i  # Foutmelding als afwezig.
    fi                           # Einde if
 echo --------------             # Print een scheidingslijn tussen bestanden
 done                            # "done" sluit de "do" loop
\end{alltt} 

%$

\subsection{Oefeningen}
\begin{enumerate}
\item[\bf Oefening 1:] Maak de bovenstaande bashversie werkend.
\item[\bf Oefening 2:] Voeg een extra foutmeldingsregel toe voor als het bestand
niet bestaat.
\end{enumerate}
\newpage

\section{Algemene scripting}

In deze sectie zullen we verscheidene scriptprogramma's aanmaken met de
naam ``doubletake'' waarmee dubbele records in een lijst zullen worden
verwijderd. We zullen verscheidene nieuw
geleerde talen gebruiken om die taak te bewerkstelligen.

\subsection{\program{awk} versie}  

Wijzig \file{doubletake} en plaats hierin de volgende regels:

\begin{alltt}
#! /bin/csh -f
# Dit script verwijdert dubbelen uit een gesorteerd bestand. 
# Zie de \program{uniq} handleiding.
set file1 = \$1         # \$1 wordt de bestandsnaam.
cat $file1 | awk '\{ if ($0 != last ) \{print $0; \}; \}\{ last = $0; \}'
\end{alltt}

Het script slaat het eerste opdrachtargument op in \term{\$1'}.
Door \program{cat} toe te passen op het bestand, geeft het die uitvoer door
aan \program{awk}. Merk op dat de eerste regel 

\term{\#! /bin/csh -f}

nodig is om het systeem te vertellen dat het inderdaad een \program{csh} 
script is.

Het script dat \program{awk} gebruikt doet twee dingen.
Het controleert eerst om te zien of de variabele
\term{last} werd gezien, zo niet, druk het deze af. Het tweede wat het
doet is de huidige regel in \term{last} opslaan. Natuurlijk zou
je de gehele tweede regel op de opdrachtregel in kunnen typen, maar
daar is niet zoveel aan als je het 9 keer per dag moet doen..

\subsection{Perl Versie}  

{\normalsize
\begin{alltt}
#! /usr/bin/perl -w
my \$line = "";                  # declareer deze variabele.
foreach \$filename (@ARGV)       # doorloop elke bestandsnaam in de ARGV lijst.
\{
   open (SF, \$filename) or die "Can't open \$filename" ;  # open het bestand en
geef een foutmelding als dit niet lukt.
   while (<SF>)                  # Voor elke regel in het bestand
   \{
      chomp(\$_);                # Verwijder aansluitende witruimte en/of linefeeds.
      if ( \$line ne \$_ )       # ne = niet gelijk aan
      \{
         print "\$line \bs{n}";  # Ja, print me.
      \}
      \$line = \$_ ;             # Reset \$line.
   \}
\}
\end{alltt}
}

\newpage
\section{Script automatisering}

Shellscripts zijn geweldige hulpmiddelen wanneer je ze weet te gebruiken,
en kunnen heel wat tijd besparen. Als je applicaties of backups op even
uren of 's nachts moet draaien, wil je deze taak vast wel automatiseren
met behulp van \program{cron} tools, die we reeds in hoofdstuk
\ref{crontab} hebben doorgenomen.

De volgende oefening beschrijft hoe een scriptprogramma op te zetten om
het vuile werk voor je op te knappen wanneer je aan het golfen of slapen
bent.

\subsection{Scripts ordenen}
Voor je een taak aanlevert om te laten afhandelen door \program{cron},
moet je er zeker van zijn dat je de taak handmatig uit kunt voeren.
Dit bestaat uit het voorzien in de volledige paden naar de opdrachten
en het instellen van de juiste permissies. Laten we bijvoorbeeld eens
backups nemen. Je moet privileges hebben om als normale gebruiker
het tape device te beschrijven, tenzij je van plan bent je taken als
\root uit te voeren. Hier is een typisch script
\file{/home/guest/bin/backup.sh} dat een backup zou maken van je 
homedirectory:


\begin{alltt}
  #! /bin/bash
  # Dit bestand heet backup.sh
  /bin/mount /mnt/floppy                  # Mount de diskette.
  cd /home/guest                          # Zorg dat je in /home bent.
  /bin/tar cvfz /mnt/floppy/guest.tgz .   # Maak een backup van je homedirectory.
  /bin/umount /mnt/floppy                 # Unmount de diskette.
  echo Voor elkaar baas!
\end{alltt}


In dit voorbeeld hebben we alle paden naar opdrachten opgegeven. Dit is
nodig aangezien je normale pad/shell in een script als deze niet wordt
aangeroepen. Dit script zal heel wat uitvoer genereren, wat later, wanneer
we ons crontab bestand aanmaken, naar een 
logbestand of naar \file{/dev/null} moet worden doorgestuurd.

\newpage
\subsection{De scripts testen}
Je moet er zeker van zijn dat je de scripts handmatig kunt uitvoeren 
voordat je ze met \program{cron} probeert aan te roepen. Op de meeste
Linux systemen is het niet toegestaan dat een gewone gebruiker 
het \file{/mnt/floppy} device mount (beschrijft), tenzij je
\file{/etc/fstab} zodanig wijzigt dat toegang door gebruikers
is toegestaan:

\begin{alltt}
 \bash{su -}
 password: *******
 \rootsh{vi /etc/fstab}

    .......
    /dev/fd0     /mnt/floppy    msdos     noauto,user     0 0
    /dev/cdrom   /mnt/cdrom     iso9660   noauto,ro,user  0 0
    :wq

 \rootsh{exit}
 \bash{./backup.sh}
\end{alltt}

\subsection{Oefeningen}

\begin{enumerate}
\item[\bf Ex 1:]

Zodra je er zeker van bent dat je script uit zichzelf zal draaien,
ben je zover het met \program{cron} in te stellen. Maak een crontab bestand
\file{/home/guest/crontab.backup} aan en lever het aan met

\bash{crontab crontab.backup}

zoals in sectie \ref{crontab}.
Vraag om hulp als je vastloopt.

\end{enumerate}
