Kevyttä skriptiohjelmointia

ITKA203 Käyttöjärjestelmät -kurssin Demo 6 keväällä 2015. "Kevyttä skriptiohjelmointia"

Paavo Nieminen, paavo.j.nieminen@jyu.fi

Jyväskylän yliopiston tietotekniikan laitos.

STATUS: SAA TEHDÄ. Olisi vuoden 2015 mielessä lopullinen

Contents

Tämän harjoituksen tavoitteet

Harjoituksen tavoitteita:

Keväällä 2015 määriteltyjen osaamistavoitteiden osalta tämän ja aiemmat demot tehtyään opiskelija:

Opitaan aivan muutama rakenne esimerkin pohjalta

Oli tarkoitus tässä käydä läpi POSIXin määräämää shell-rajapintaa tarkemmin suoraan standardista suomentaen, mutta siitä tulisi tarpeettoman pitkä selostus, joka ei ole edes tarpeen tämän tehtävän tekemiseksi tai olennaisen osaamistavoitteen saavuttamiseksi. Mahdollisia myöhempiä kurssikertoja ajatellen alkupuolikas suomenkielisestä POSIX-shell-introsta löytyy osoitteesta http://itka203.it.jyu.fi/pages/d07_posix_shell_intro.rst - siellä on lisätietoa shellin syntaksin yksityiskohdista.

Tässä demossa tutkitaan loppujen lopuksi vain muutamaa shellin ohjelmointirakennetta esimerkin perusteella. Tässä on koko tutkittava esimerkki listauksena:

#!/bin/sh
fl="-std=c99"
for opt; do
  case "$opt" in
    -std=c99|-std=iso9899:1999) fl="";;
    -std=*) echo "`basename $0` called with non ISO C99 option $opt" >&2
            exit 1;;
  esac
done
exec gcc $fl ${1+"$@"}

Käydään esimerkki läpi rivi riviltä. Katsotaan, mitä se tekee ja miten. Tarkoitus on sitten soveltaa samoja rakenteita omassa vastauksessa. Soveltaminen on mahdollista, vaikka ei ehditä ymmärtää kaikkea shellin syntaksista ja mahdollisuuksista.

Ensimmäinen rivi on shellin kannalta kommentti, koska se alkaa risuaidalla #. Monessa käyttöjärjestelmässä, mukaanlukien Linux, jota käytämme kurssin esimerkeissä, on sovittu, että merkit #! tiedoston alussa tarkoittavat, että kyseessä on tulkattava ohjelma. Loput tiedostosta syötetään komentotulkkiohjelmalle, jonka tiedostosijainti löytyy ensimmäisellä rivillä näiden kahden merkin jälkeen. (POSIX tarkentaa, että merkeillä #! alkavan skriptin suorittamisen lopputulemaa ei määritellä. Ei anneta tämän nyt liiemmin häiritä, koska käytämme Red Hat Enterprise Linuxia tämän kurssin esimerkkiartefaktina. Ensimmäisen rivin "hashbang" antaa lukijalle hyvän kuvan siitä, että jatkossa seuraa skripti, joka on tarkoitettu nimenomaan Bourne Shellillä (/bin/sh) suoritettavaksi).

Toisella rivillä asetetaan muuttuja nimeltä fl (äf ja äl -kirjain):

fl="-std=c99"

Muuttujan asetuksessa ei saa olla välilyöntejä yhtäsuuruusmerkin ympärillä. Arvo voi olla lainausmerkeissä, joita on ihan hyvä käyttääkin suurimmassa osassa tilanteista. Tässä tapauksessa muuttujaan fl menee tämän rivin suorituksen kohdalla merkkijono -std=c99. Tämän merkkijonon merkitys gcc -kääntäjän argumenttina olisi määrätä, että kaikki kääntäjälle syötettävä lähdekoodi on tulkittava C99-standardin mukaan.

Seuraavalla rivillä aloitetaan yksi shellin tukema ohjelmointirakenne, nimittäin for-silmukka:

for opt; do

Merkkiyhdistelmän for opt jälkeen tulee puolipiste ;, joka tarkoittaa samaa kuin rivinvaihto eli nykyisen komennon loppumista. Skriptissä samalle riville kirjoitettu sana do tulkitaan siis itse asiassa vasta seuraavan komentorivin osaksi. Sama tapahtuu aina puolipisteen kohdalla: rivinvaihto ja puolipiste ovat vaihdannaisia.

Skriptin ensimmäisen komennon aloittaa ns. varattu sana for, joka komentorivin alussa aina aloittaa silmukkarakenteen. Välittömästi varatun sanan for jälkeen tulee skriptin kirjoittajan valitsema muuttujanimi, tässä tapauksessa opt. Kun muoto on tällainen, että on vain for muuttujanimi eikä mitään muuta, niin silmukassa tullaan käymään läpi skriptille annetut argumentit yksi kerrallaan. Silmukassa tapahtuu kierroksia niin monta kuin argumentteja on annettu. Joka kierroksella käsitellään yksi argumentti, jonka sisältö tulee sijoitetuksi muuttujaan nimeltä opt. Rakenteella voi käydä läpi muunkinlaisia listoja, mutta tässä tapauksessa, kun for:in perässä on vain yksi muu sana, tapahtuu nimenomaan skriptin argumenttien läpikäynti yksi kerrallaan.

Huomautus rakenteesta: Varattu sana for ilmoittaa, että halutaan silmukkarakenne. Silmukan sisältävä lohko sen sijaan alkaa varatulla sanalla do ja päättyy sanaan done seuraavalla tavoin:

for opt; do
  # silmukan sisältö; muuttuja $opt aina eri
done

Tosiaan puolipisteen sijasta sanan do voisi sijoittaa omalle rivilleen vaikkapa selkeyssyistä:

for opt
do
  # silmukan sisältö; muuttuja $opt aina eri
done

Skriptissä on for -silmukan sisältö merkitty siis syntaksin mukaisesti do...done -lohkoon ja sisennetty kauniisti, kuten tyyliin aina kuuluu. Jokaisen skriptille annetun argumentin kohdalla siis argumentti sijoitetaan väliaikaisesti muuttujaan nimeltä opt ja tehdään seuraava toimenpide:

case "$opt" in
  -std=c99|-std=iso9899:1999) fl="";;
  -std=*) echo "`basename $0` called with non ISO C99 option $opt" >&2
          exit 1;;
esac

Tämä on toinen shellin ohjelmointirakenne. Varattu sana case aloittaa rakenteen, jossa verrataan jonkin merkkijonon sisältöä ns. hahmoon (pattern). Tässä tapauksessa halutaan verrata nimellä opt löytyvän muuttujan sisältöä, johon for-silmukan ansiosta tulee vuorollaan, yksi kerrallaan, jokainen skriptin käynnistyksessä annettu argumentti. Syntaksissa on yhdellä rivillä case "$opt" in, mikä on tyypillistä: sana case aloittaa rakenteen, johon kuuluu syntaktisesti myös in. Näiden välissä on verrattava merkkijono. Tässä tapauksessa siinä on $opt. Tuo opt on nyt ulomman for-rakenteen määrittämä muuttujan nimi. Muuttujan käyttäminen tapahtuu kirjoittamalla nimen alkuun dollarimerkki. Tämä on perustapa, jolla shellissä saadaan muuttujien arvot käyttöön.

Tämä case VERRATTAVA in -rakenne päättyy varattuun sanaan esac (eli vähän niinkuin "case" toisin päin). Välissä on tekstihahmoja, joihin VERRATTAVA yritetään järjestyksessä sovittaa. Hahmot ovat tässä seuraavat:

-std=c99|-std=iso9899:1999)

-std=*)

Ensimmäinen hahmo täsmää tässä esimerkissä verrattavan muuttujan eli opt kanssa silloin, kun muuttujan sisältönä on täsmälleen merkkijono -std=c99 tai merkkijono -std=iso9899:1999. Tolppamerkki | on tässä kohtaa normaali "tai"-operaation symboli. Hahmo päättyy merkkiin ), jonka jälkeen tulee koodia, joka suoritetaan silloin, kun tarkasteltava muuttuja oli hahmon mukainen.

Toinen hahmo tässä esimerkissä on -std=*. Se täsmää verrattavan muuttujan kanssa silloin, kun muuttujan sisältö alkaa merkeillä -std=. Alun jälkeen voi tulla mitä tahansa, mitä merkitään villikortin asemassa olevalla asteriskilla *.

(Yleisesti ottaen tähti täsmää minkä tahansa osamerkkijonon kanssa. Monesti case -rakenteessa on viimeisenä hahmona pelkkä *, jolla käsitellään kaikki yli jäävät tilanteet, joita edellisissä hahmoissa ei vielä tarkistettu. Välissä voisi olla useampiakin hahmoja, jotka käytäisiin läpi järjestyksessä.)

Tässä esimerkissä halutaan ensimmäisessä tapauksessa tarkistaa, onko jokin argumentti täsmälleen merkkijono -std=c99 tai -std=iso9899:1999. Näillä argumenteilla GNU:n C-kääntäjää voi pyytää toimimaan C99:n standardin mukaisesti. Standardilla on kaksi nimeä ("c99" ja "iso9899:1999"), jotka ovat molemmat OK, jos halutaan käynnistää C99-kääntäjä.

Toisessa tapauksessa halutaan havaita, alkaako jokin argumentti osajonolla -std= siten että sen jälkeen tulee mitä tahansa (*). Koska järjestyksessä ensimmäinen hahmo ei ole napannut kiinni, ilmeisesti merkkijonon jatke ei voi olla kumpikaan validi nimi nimenomaan C99:lle, eli loppuosa on jotakin muuta kuin c99 tai iso9899:1999. Tästä skripti voi päätellä, että käyttäjä on halunnut noudatettavan jotakin eri standardia kuin mihin tämä nimenomainen käynnistysskripti on tarkoitettu.

Jokaisen hahmon osalta case-rakenteessa on lohko, joka suoritetaan silloin, kun verrattava muuttuja vastaa hahmoa. Lohko päättyy kahteen puolipisteeseen ;;, joten siinä voi tarvittaessa olla useita rivejä. Tarkastellaan esimerkkiskriptin toimintaa, jossa on kaksi hahmoa ja niitä vastaavat lohkot:

-std=c99|-std=iso9899:1999) fl="";;
-std=*) echo "`basename $0` called with non ISO C99 option $opt" >&2
        exit 1;;

Jos jokin argumentti vasta hahmoa -std=c99 tai -std=iso9899:1999, skripti yksinkertaisesti tyhjentää muuttujan fl sisällön, ts. asettaa tilalle tyhjän merkkijonon. Tässä lohkossa, ennen päättävää kaksoispuolipistettä, on tasan yksi komento: fl="".

Jos sen sijaan on annettu argumentti -std=jotain-muuta-kuin-c99, skripti ilmoittaa, että argumentti ei ole yhteensopiva C99-käännöksen kanssa ja lopettaa skriptin suorittamisen virhekoodilla 1. Tässä lohkossa on kaksi erillistä komentoriviä; lohko päättyy kaksoispuolipisteeseen.

Katsotaan vielä jälkimmäisen lohkon käskyt ja niiden toiminta:

echo "`basename $0` called with non ISO C99 option $opt" >&2
exit 1;;

Tulostus tapahtuu aiemmista demoista tutulla komennolla echo, jolle annetaan tässä yksi lainausmerkeillä merkitty argumentti. Kyseiseen argumenttiin kuitenkin täydennetään tai "lavennetaan" (expand) sisään komennon basename $0 tuloste, josta tulee skriptin suorituskomennon tiedosto-osuus (esimerkiksi c99, vaikka itse komento olisi ollut /usr/bin/c99). Lisäksi sisään lavennetaan opt -nimisen muuttujan sisältö. Eli tällä hetkellä tutkittavan argumentin sisältö. Jos vaikka olisi komennettu:

/usr/bin/c99 -g -o exe.exe -std=c11 uuden_standardin_koodia.c

niin argumentti -g ei nappaisi kiinni mihinkään caseen. -o ei nappaisi myöskään eikä exe.exe. Sen sijaan -std=c11 nappaisi kiinni kohtaan -std=* ja skriptissä tapahtuisi seuraavaa:

echo "`basename $0` called with non ISO C99 option $opt" >&2
exit 1;;

Tämä korvautuisi sitten seuraavalla (koska skriptin ajonimi $0 olisi /usr/bin/c99 ja tutkittavana oleva argumentti olisi -std=c11:

echo "c99 called with non ISO C99 option -std=c11" >&2
exit 1;;

Näin ollen käyttäjä saisi tietää, että on yrittänyt kääntää C99-komennolla C11:n mukaista koodia, mikä ei ole sallittua. Ja käännösyritys loppuisi skriptin loppumiseen virhekoodilla 1.

Edellisessä on lisäksi käytetty uudelleenohjausoperaattoria (redirection operator) >&2, joka ohjaa edeltävän komennon tulosteen standardivirhetulosteeseen (tyypillisesti tiedostodeskriptori numero 2). Käyttäjä oletettavasti haluaa tällaiset virhetilannetta kuvaavat tulosteet (tai ohjelman varsinaiseen toimintaan vaikuttamattomat diagnostiikkatiedot) eri virtaan kuin ohjelman normaalit tulosteet. Shell-skripteissä em. syntaksi mahdollistaa tulosteiden viennin virhekanavaan.

Viimeinen rivi ei ole oikeastaan tämän demon palautustehtävän kanssa olennainen, mutta palastellaan sekin nyt, kun lähdettiin hommaa tekemään... Rivi on seuraavanlainen:

exec gcc $fl ${1+"$@"}

Komento exec lataa skriptiä suorittavan prosessin tilalle uuden ohjelman, joka käynnistyy komennon exec perässä olevalla komentorivillä. Eli tällainen komentorivi toimii hyvin samalla tavoin kuin C-kielinen exec() -käyttöjärjestelmäkutsu. PID tulee olemaan skriptin saama PID, mutta latauksen jälkeen prosessi tulee olemaan ohjelman gcc koodia. Esimerkin tapauksessa skripti siis käynnistää komennon gcc, jolle annetaan ensimmäisenä argumettina $fl ja sen jälkeen jotakin, mikä näyttäisi olevan ${1+"$@"}.

Tässä $fl korvautuu muuttujan fl sisällöllä, joka oli aivan aluksi asetettu arvoon -std=c99. Sisältö on edelleen sama, ellei käyttäjän antamissa argumenteissa ollut määritelty sopivaa C-standardia. Jos oli argumenttina joko -std=c99 tai synonyymi -std=iso9899:1999, muuttuja fl tuli tyhjennetyksi case-lauseessa aikaisemmin.

Syntaksin ${1+"$@"} paikalle tulee kaikki käyttäjän alunperin antamat argumentit (syntaksi $@), paitsi siinä tapauksessa (syntaksin + johdosta), että yhtään argumenttia ei ollut annettu. Jos argumentteja ei ollut, tapahtuu siis käytännössä gcc -std=c99. Muussa tapauksessa argumenttja oli, ja vaihtoehdot ovat:

gcc -std=c99 ALKUPERAISET_ARGUMENTIT

jos alkuperäisissä argumenteissa ei ollut yhtään laillista -std=* -variaatiota. Tai sitten:

gcc "" ALKUPERAISET_ARGUMENTIT

jos alkuperäisissä argumenteissa oli mukana laillinen -std=c99 tai -std=iso9899:1999. Silloinhan fl on tyhjennetty tässä vaiheessa.

Tätä viimeistä riviä ei siis tarvita tämän demon palautustehtävässä. Sen sijaan tulle jotakin seuraavan näköistä:

echo "Opiskelijan OK fiilikset:"
echo $fiilikset

Ohjelman logiikan tulee tähän mennessä kasata argumettien perusteella sopiva sisältö muuttujalle nimeltä fiilikset, joka on alussa asetettu tyhjäksi merkkijonoksi:

#!/bin/sh
fiilikset=""
for opt; do
# ... ja sitten loppu koodi, mitä tarvitaan

Lisämausteet tämän harjoitteen tekemistä varten

Argumenttien määrän saa lavennettua komentoriville syntaksilla $# (eli dollari ja risuaita). Ehtolauseita on monenlaisia, mutta esimerkiksi voi testata lukumääriä seuraavasti:

if [ "$#" -lt 2 ]
then
  # tee jotain tässä
fi

Muuttujaan voi lisätä perään asioita seuraavasti:

muuttuja="jotain"
hmm="muuta"
muuttuja="$muuttuja $hmm"

Lopputulemana pittäis olla muuttujan muuttuja sisältönä jotain muuta eli yhdistelmä aiemmin olemassa olleista muuttujista, ja välilyönti siinä välissä.

Näitä soveltamalla noin periaatteessa pittäis onnistua tämän demon tekeminen. Kysyy vinkkiä sitten, jos ei meinaa onnistua...

Pakollinen palautustehtävä

Pakollisessa palautustehtävässä verifioidaan, että pystyt kopioimalla ja muokkaamalla toteuttamaan olemassaolevan shell-skriptin variaation.

Tutki suorakäyttökoneillamme sijainnissa /usr/bin/c99 olevaa skriptiä, ja kopioi se vaikka pohjaksi omalle vastauksellesi. Selvitä skriptin toimintaa siinä määrin, että pystyt toteuttamaan allaolevan määritelmän mukaisen skriptin.

Tavoitteenasi on tehdä ohjelma, joka:

Testaa skriptisi, ja varmista, että se toimisi seuraavan shell-session mukaisella tavalla, jos tarkastava opettaja kokeilisi suorittaa sitä:

[nieminen@halava demo6]$ ./d06.sh || echo "Epäonnistui virhekoodilla $?"
Anna argumenttina fiilistäsi kuvaavia sanoja!
Epäonnistui virhekoodilla 1
[nieminen@halava demo6]$ ./d06.sh hyvin menee || echo "Epäonnistui virhekoodilla $?"
Opiskelijan OK fiilikset:
hyvin menee
[nieminen@halava demo6]$ ./d06.sh hyvin laiskasti menee || echo "Epäonnistui virhekoodilla $?"
d06.sh disallows word laiskasti
Opiskelijan OK fiilikset:
hyvin menee
[nieminen@halava demo6]$ ./d06.sh hyvin laiskasti menee 2>/dev/null
Opiskelijan OK fiilikset:
hyvin menee
[nieminen@halava demo6]$ ./d06.sh a b tyhmä c laiskuri d suomi ottaa kultaa laiskasti
d06.sh disallows word tyhmä
d06.sh disallows word laiskuri
d06.sh disallows word laiskasti
Opiskelijan OK fiilikset:
a b c d suomi ottaa kultaa
[nieminen@halava demo6]$ ./d06.sh tyhmästi tyhmä kurssi loppuu jo
d06.sh disallows word tyhmä
Opiskelijan OK fiilikset:
tyhmästi kurssi loppuu jo
[nieminen@halava demo6]$

Yksityiskohtia: Varoitusviestit menevät virhevirtaan kuten esimerkissä (c99-käynnistysskripti). Muut tulosteet menevät ulostulovirtaan. Ohjelma loppuu virhekoodilla 1 ("yleinen virhe") mikäli sille ei anneta yhtään argumenttia. Osa sanoista poistuu sanarungon perusteella, mutta osa täsmällisellä vertailulla (''tyhmästi'' menee läpi vaikka ''tyhmä'' ei).

Vinkkejä:

Keväällä 2015 kurssin demot ja niiden palautus hoidetaan osoitteessa itka203.it.jyu.fi olevan järjestelmän kautta. Kullekin demolle on oma palautuslaatikko, johon tehtävässä tuotettu tiedosto palautetaan.

Tästä demosta palautetaan tasan yksi tiedosto nimeltään "d06.sh", joka vastaa ylläolevaa toimintakuvausta ja toimisi esimerkkisession mukaisesti, jos opettaja kokeilisi sitä.