ITKA203 Käyttöjärjestelmät -kurssin Demo 6 keväällä 2014. "Edistyneempi skripti"
Paavo Nieminen, paavo.j.nieminen@jyu.fi
Jyväskylän yliopiston tietotekniikan laitos.
Contents
Harjoituksen tavoitteet:
Tutustut shell-skriptaukseen syvemmin kuin mitä pakollisissa demoissa tehtiin, erityisesti aliohjelmien tekemiseen ja argumenttien käyttöön tutustutaan.
Teet itse skriptin, joka tekee jotakin puolijärkevää ja käyttäytyy hyvin, erityisesti:
- skripti käyttää väliaikaista tiedostoa taiteen sääntöjen mukaisesti
- skripti varautuu siivoamaan jälkensä, jos sen suoritus lopetetaan ulkopuolelta
- skripti yrittää olla tuhoamatta lopullisesti käyttäjän dataa, vaikka se pyrkii olemaan suora tehotyökalu.
Tässä luvussa käydään läpi tarvittavat palaset suomen kielellä. Valmista vastausta ei anneta, koska soveltaminen on olennaista. Sisällöt mukailevat demo4:ssä mainittuja englanninkielisiä ohjeita:
- http://steve-parker.org/sh/sh.shtml (linkin toimivuus tarkistettu 24.4.2014)
- http://www.shelldorado.com/ (linkin toimivuus tarkistettu 24.4.2014)
Simuloitua testiaineistoa voi hakea tuosta paketista:
- http://users.jyu.fi/~nieminen/kj14/esimerkkiaineisto_latin9.zip (sisältää latin-9 -merkistöllä koodattua nelipolvista trokeeta 400-rivisiin tekstitiedostoihin jaettuna)
Joko linkitetyistä tutoriaalista tai aiemmista demoista oletetaan tutuksi:
- Shellin perusidea: suoritus riveittäin ellei sisäänrakennetuilla ohjausrakenteilla ole muuta ilmaistu
- muuttujien asettamisen ja käytön syntaksi
- yksinkertaiset ehtolauseet
- argumenttien käyttö shell-ohjelmassa
Lisähuomautus paikallaan: Käytämme bashiä, jonka ominaisuudet (mm. [[ ... ]] -ehtolauseet) ovat hieman laajemmat kuin sen esi-isällä Bourne Shellillä. Periaatteessa kaikkein portaabeleimmat skriptit eivät käytä bashin hienouksia, ja siksi esim. tässä linkitetty tutoriaali pitäytyy alkuperäisen sh:n syntaksissa, jossa mm. ehdot ovat mallia [ ... ], operaattoria == ei käytetä jne. Toteutettava esimerkkiohjelma on niin yksinkertainen, ja toisaalta sen tarvitsee toimia vain bashissä, ettei ole niin väliä, kuinka pedantisti ominaisuuksia käyttää. Kunhan skripti toimii, eikä tee liikaa tyhmyyksiä :).
Tässä demossa tehdään "puolijärkevä" skripti toimenpiteeseen, jota ainakin luennoitsija on tarvinnut siirtäessään tämän kurssin materiaaleja ns. "latin-9" -merkistöllä kirjoitetusta formaatista nykyaikaisempaan "utf-8" -merkistöön. Yhdelle tiedostolle operaatio voidaan tehdä apuohjelmalla iconv seuraavasti:
iconv -f latin-9 -t utf-8 tiedosto.txt
Lisätietoja luonnollisesti komennolla man iconv. Mahdolliset koodaukset saa listattua optiolla iconv -l.
Ohjelma operoi yhdelle tiedostolle kerrallaan, ja tulostaa suoraan standardiulostuloon. Tulee siis usein komennettua vaikkapa seuraavanlaisesti:
mv tekstiA.txt tekstiA.txt.alkup iconv -f latin-9 -t utf-8 tekstiA.txt.alkup > tekstiA.txt mv tekstiB.txt tekstiB.txt.alkup iconv -f latin-9 -t utf-8 tekstiA.txt.alkup > tekstiA.txt
Näiden komentojen jälkeen on kokonaista kaksi tiedostoa muutettu merkistöstä toiseen, ja varmuuden vuoksi alkuperäinen tiedosto on tallessa, jos vaikka muunnos olisi sotkenut jotakin. Kirjoitusvirheen mahdollisuus on suuri, kun komentoja tulee tehtyä copy-pastella. Lisäksi liiasta kirjoittamisesta voi seurata hermoratavaurioita käsiin. Pidemmän päälle tekisikin mieli pystyä komentamaan suoraan usealle, vaikkapa sadalle, tiedostolle:
muunna latin-9 utf-8 kjdemot/*.txt
Ja siitäpä idea koko tähän "puolijärkevään" harjoitukseen: Skriptit ovat omiaan mm. laajentamaan yhdelle tiedostolle operoivan perusohjelman toimintaa käyttäjän tarvitsemaan laajempaan tarkoitukseen. [Reunahuomautuksena tein joskus itselleni mm. skriptin, jolla aina bänditreenin jälkeen muunnan nauhoitettujen raitojen raaka-audion 48kHz -formaatista 44.1kHz -formaattiin sekä mp3-muotoon bändin sisäistä jakelua varten. Kyseinen skripti on ollut käytössä vuosikausia ... olisinpa tehnyt sen aikoinaan näitä ohjeita noudattaen "hyvin käyttäytyväksi" ...]
Skripteissä, kuten muissakin ohjelmissa, saatetaan tarvita väliaikaistiedostoja, joiden on hyvä sijaita tarkoitusta varten järjestelmään asennetusta hakemistosta, johon kaikilla käyttäjillä on kirjoitusoikeus. Esimerkiksi yliopistomme suorakäyttökoneilla se on hakemisto /tmp/ ja mikroluokkien PC-koneissa C:\MyTemp. Väliaikaistiedoston idea on, että sitä ei tarvittaisi enää ohjelman päättymisen jälkeen, eli hyvin käyttäytyvä ohjelma huolehtii nämä aputiedostonsa pois suorituksen päättyessä (myös, kun suoritus päättyy virheeseen tai pakkolopetukseen!). Monet ohjelmat käyttävät väliaikaistiedostoja automaattitallennuksiin, välimuistitukseen, ynnä muuhun loppukäyttäjän elämää helpottavaan toimintaan.
Väliaikainen tiedosto pitäisi nimetä sillä tavoin, että mikään muu saman tai toisen käyttäjän suorittama ohjelma ei yritä käyttää juuri samaa tiedostoa. Tiedoston nimi ei siis kannata olla mallia /tmp/temppi vaan se voisi koostua vähintäänkin esimerkiksi käyttäjänimestä, prosessi-id:stä ja kellonajasta.
Yleis-unixmaisempiakin keinoja löytyy esim. Parkerin tutoriaalista, mutta tässä harjoituksessa käytetään GNU-apuohjelmaa nimeltä mktemp, joka luo sopivan satunnaisesti nimetyn tiedoston asianmukaiseen paikkaan ja vasta luonnin onnistuttua ilmoittaa luodun väliaikaistiedoston nimen. Luonnollisesti man mktemp kertoo lisää. Nimen luontia voi kokeilla optiolla --dry-run, jolloin luodaan vain nimi eikä itse tiedostoa:
mktemp --dry-run
Jos kokeillessasi vahingossa luot väliaikaistiedostoja, olisi kohteliasta myös poistaa ne. Yhteiskäyttöiseltä palvelimelta voit etsiä omia väliaikaistiedostojasi vaikkapa (jo aiemmin tutuksi tulleella) komentoyhdistelmällä:
ls -l /tmp/ | grep `whoami`
Osa näistä tiedostoista on todennäköisesti muiden käynnissä olevien ohjelmien tekemiä, joten pidä huolta, ettet vahingossa sekoita niiden toimintaa poistamalla vielä niiden käytössä olevia väliaikaistiedostoja.
Skriptin argumentteja on tähän asti käytelty malliin $1, $2. Tällä tavoin ei onnistu yli yhdeksän kappaleen lista esimerkiksi tiedostoja, joille skriptin halutaan operoivan. Useampia argumentteja voidaan kuitenkin käyttää yksi kerrallaan. Komento shift poistaa ensimmäisen (siis entinen $1) ja siirtää kaikkia argumentteja yhdellä pienempään numeroon. Seuraava esimerkki (suoraan Steve Parkerin tutoriaalista) tulostaa kaikki argumentit, olipa niitä kuinka monta tahansa. Silmukan loppuehtona on, että lukumäärä $# putoaa nollaan toistuvien siirtojen vaikutuksesta:
#!/bin/sh while [ "$#" -gt "0" ] do echo "\$1 is $1" shift done
Bashille annettava optio -e lopettaa skriptin suorituksen, jos mikä tahansa komento siinä epäonnistuu (eli päättyy nollasta poikkeavaan exit-koodiin). Tällöin skripti ei kuitenkaan pysty käsittelemään tilannetta mitenkään hienommin, esimerkiksi tulostamaan omaa ilmoitusta käyttäjälle. Hyvin käyttäytyvä skripti ei käytäkään valitsinta -e vaan varautuu virheisiin tarkistamalla edellisen komennon exit-koodin erikoismuuttujasta $? kuten seuraavassa esimerkissä (edelleen suoraan Steve Parkerin tutoriaalista):
#!/bin/sh /usr/local/bin/my-command if [ "$?" -ne "0" ]; then echo "Sorry, we had a problem there!" fi
Shell-skriptissä voi käyttää myös aliohjelmia (joita ilmeisesti sanotaan skriptien yhteydessä "funktioiksi"). Aliohjelmien määrittelyssä ei nimetä parametreja, vaan ne tulevat aliohjelman käyttöön ikään kuin skriptin argumentit "pääohjelmalle". Esimerkki syntaksista ja semantiikasta:
#/bin/sh jonkinlainen_aliohjelma() { echo "Olen aliohjelma.." echo "Eka parametrini on $1" echo "Toka parametrini on $2" echo "Aion palauttaa kokonaisluvun (muuta en voisikaan) 123" return 123 } # Varsinainen skripti tapahtuu aliohjelmamäärittelyjen jälkeen, koska # muuten rivi riviltä toimiva shell ei voisi aliohjelmasta mitään # tietää: echo "Kutsun nyt aliohjelmaa parametreilla kissa, koira" jonkinlainen_aliohjelma kissa koira echo "Paluuarvo oli $?" echo "(Ikään kuin olisin suorittanut ulkopuolisen ohjelman, jolla oli exit-koodi)" jonkinlainen_aliohjelma aasi apina echo "Kutsuin uudelleen. " echo "Paluuarvo on $?, mutta nyt se on jo echo-komennolta!!"
Kryptisen määrittelysyntaksin ja parametrinvälityksen lisäksi muuttujien näkyvyysalue ja sivuvaikutukset skripteissä ovat erilaiset kuin mihin normaali C#/Java/C -ohjelmoija on tottunut. Todetaan ainakin, että globaalit, ohjelman alussa määritellyt, muuttujat ovat saatavilla joka paikassa. Missään nimessä ei käsitellä asiaa tässä enempää, vaan käytetään korkeintaan em. esimerkin mukaista parametrin ja paluuarvon välittämistä ( hurjemmin kiinnostuneet lukekoot vaikkapa Parkerin ohjeesta funktioita käsittelevän kohdan http://steve-parker.org/sh/functions.shtml )
Kun tehdään väliaikaisia tiedostoja (tai avataan muita resursseja, jotka voisivat jäädä roikkumaan skriptin loppuessa yllättävästi kesken), on syytä tehdä siivoustoimenpiteitä. Näitä varten voi olla esimerkiksi aliohjelma nimeltään cleanup (tai siivoa tai vastaavaa :)), ja tämä aliohjelma pitää kytkeä käsittelemään ulkopuolelta tulevat lopetussignaalit. Esimerkki:
#!/bin/sh temppitiedosto=`mktemp` if [ "$?" -ne "0" ] then echo Väliaikaistiedostoa ei voitu luoda. Lopetan. exit 1 fi trap cleanup SIGHUP SIGINT SIGQUIT SIGABRT cleanup() { echo "Lopetetaan nätisti ja siivotaan jäljet." rm $temppitiedosto exit 1 } echo "Tiedosto $temppitiedosto on tämän skriptin käyttöön." echo "Tätä tekstiä ei haluta säilyttää" >> $temppitiedosto
Skriptisi toimii seuraavalla tavalla:
Skriptin nimi on muunna_merkit.sh ja sillä voi muuntaa argumenttina annetut tekstitiedostot merkistöstä toiseen. Lähde- ja kohdemerkistöt tulee olla argumenttina ja sen jälkeen tiedostojen nimiä tulee voida olla mikä tahansa määrä (komentorivin pituus voi olla shellissä rajoitettu, mutta se ei vaikuta tähän tehtävään)
Skriptin normaali käyttö tapahtuu siis esimerkiksi tällaisella komennolla:
./muunna_merkit.sh latin-9 utf-8 testi1.txt testi2.txt testi3.txt
Muita vaihtoehtoisia käyttötapoja ovat:
./muunna_merkit.sh --no-backup latin-9 utf-8 *.txt ./muunna_merkit.sh --help
Skripti käyttää apuohjelmaa iconv varsinaisen tehtävänsä hoitamiseen.
Skripti yrittää olla tuhoamatta/sekoittamatta käyttäjän dataa, eli:
- Ennen muunnosta se tekee tiedostosta kopion, jossa nimen perään on lisätty .alkup - jos tällainen tiedosto on jo ennestään, niin ohjelman suoritus loppuu; se ei siis missään olosuhteissa suostu tuhoamaan mitään aiempaa dataa lopullisesti.
- Käyttäjä saa kuitenkin antaa ensimmäisenä argumenttina --no-backup, jolloin em. varmuuskopioita ei tehdä; käyttäjä haluaa siis olla todellinen "tehokäyttäjä" ja hän on aivan varma, että kyllä ne kaikki tiedostot nyt ovat siinä tietyssä lähtöformaatissa ja ne vaan pitää muuntaa tässä ja nyt, ilman tuplavarmistuksia, kävi miten kävi.
- Myös itse merkistömuunnoksen skripti tekee ensin väliaikaiseen tiedostoon, ja se korvaa alkuperäisen tiedoston vain silloin kun muunnos onnistui.
Muutoinkin skripti "käyttäytyy hyvin", eli se huolehtii operaatioiden onnistumisesta ja siivoaa jälkensä (väliaikaistiedosto) riippumatta lopetuksen syystä (onnistunut suoritus / pakotettu lopetus / muu vikatilanne)
Lisäksi skriptisi on toteutettu seuraavalla tavoin:
Siinä on aliohjelma usage(), jota kutsumalla annetaan käyttäjälle käyttöohjeet silloin, kun:
- argumenttien muoto ei ole odotetunlainen
- käyttäjän ainoa argumentti on --help
Ohjeen muoto mukailee tyypillisten apuohjelmien (kuten find, grep) vastaavasti antamia ohjeita.
Aliohjelmia kannattanee muutenkin käyttää hyödykseen, nyt kun niiden toimintaperiaate on käyty läpi.
Väliaikainen tiedosto sijaitsee paikassa, jonka luo apuohjelma mktemp
VAROITUS1: Ota huomioon, että kehittäessäsi skriptiä se ei välttämättä toimi vielä niin kuin sen lopulta pitää, joten testaa turvallisella aineistolla, älä esim. kandityösi käsikirjoituksella. (Ja pidä aina huolta varmuuskopioista jne., jne., aina se sama saarna...)
VAROITUS2: Skripti tekee väliaikaistiedostoja yhteiskäyttöiseen hakemistoon, ja niin kauan kuin se ei vielä osaa poistaa niitä nätisti, voi yhteiskäyttökoneelle kertyä turhaan tiedostoja, jotka vievät tilaa - eivät ehkä kovin paljon, mutta ylimääräistä se on pienikin ylimäärä. Jos epäilyttää, ettei homma pysy näpeissä, tee tämäkin demo kurssin testipalvelimella itka203.it.jyu.fi, jossa ei voida häiritä muita.
Palautat Optimaan skriptin nimeltä "muunna_merkit.sh", joka toimii vaatimusten mukaisesti.