Readarray toi BASH:iin kaksiulotteiset matriisit ja niitähän yleensä tarkoitetaan kun muissa kielissä puhutaan matriiseista. BASH:in vanhantyyppiset matriisit ovat yksiulotteisia ja niiden kanssa toimiminen on kaikinpuolin selväpiirteisempää - mutta se täytyy myöntää että puurot ja vellit menee nyt sekaisin. Seuraavissa tarkoitetaan aina yksiulotteisia matriiseja. Nimittäin readarray pahoinpitelee muistikuvaa joten kaksiulotteisten matriisien muistikuvien käsitteleminen tapahtuu vähän erilailla.
Kyllä ne matriisitkin palautuvat funktioista -> tässä funktioon menee vertailtavat matriisit ja tyhjä matriisi, se tyhjä täytetään siellä kahden muun matriisin eroilla ja sen muistikuva muodostetaan joten se on täytettynä funktiosta palattaessa - siis mitään ei palauteta mutta tulos on sama kun jos olisi palautettu.
Surkean hitaitahan nämä muistikuvan avulla tehdyt matriisinkäsittelyt ovat, mutta ne kestävät sentään järjellisiä aikoja - nämä kestävät vain sekunnin mutta looppi-versiot kymmeniä minuutteja.
Myös matriisien eroavaisuuksia ilmoittavan funktion toiminta muistikuvaan tukeutuen on nopeampi kuin pääohjelmaan tehdyt viritykset. Tarkemmalta nimeltään skripti on: mitä toisessa matriisissa on sellaista mitä ensimmäisessä ei ole.
Tässä funktioon menee vertailtavat matriisit ja tyhjä matriisi, se tyhjä täytetään siellä kahden muun matriisin eroilla ja sen muistikuva muodostetaan - siten se on täytenä pääohjelmassa.
#!/bin/bash
function arraydiff1 () { readarray $3 < <(grep -Fxvf <(declare | grep ^$1= | tr ' ' '\n' | cut -d\" -f2 | sort) <(declare | grep ^$2= | tr ' ' '\n' | cut -d\" -f2 | sort)) ;}
matriisi1=({100000..1})
matriisi2=({-2..100002})
time arraydiff1 matriisi1 matriisi2 matriisi3
printf "%s\n" ${matriisi3[@]}
Tämä tulostaa kahden matriisin erot - jättäen pois yhteiset jäsenet:
#!/bin/bash
function arraydiff2 () { readarray $3 < <( comm -33 <(declare | grep ^$1= | tr ' ' '\n' | cut -d\" -f2 | sort) <(declare | grep ^$2= | tr ' ' '\n' | cut -d\" -f2 | sort)) ;}
matriisi1=({3..100000})
matriisi2=({0..99999})
time arraydiff2 matriisi1 matriisi2 matriisi3
printf "%s\n" "${matriisi3[@]}" | column -t
Vaikeista puuttuu enää se joka kertoo mitä yhteisiä jäseniä matriiseissa on:
#!/bin/bash
function arraydiff3 () { readarray $3 < <( comm -12 <(declare | grep ^$1= | tr ' ' '\n' | cut -d\" -f2 | sort) <(declare | grep ^$2= | tr ' ' '\n' | cut -d\" -f2 | sort)) ;}
matriisi1=({3..5} {7..11})
matriisi2=({0..9} {11..53})
time arraydiff3 matriisi1 matriisi2 matriisi3
printf "%s\n" "${matriisi3[@]}" | column -t
***
Myös matriisin maksimi ja minimi kannattaa etsiä muistikuvan avulla:
#!/bin/bash
export LC_ALL=C # toiminta-aika muttuu 260->100ms
function matriisinmaxmin () {
declare | grep ^$1= | tr ' ' '\n' | cut -d\" -f2 | sort -n > /tmp/delme
readarray $3 < <(tail -1 /tmp/delme)
readarray $2 < <(head -1 /tmp/delme) ;}
# readarray lukee myös yksijäsenisen matriisin. Ja yksijäsenisen matriisi on sama kuin tavallinen muuttuja
# joten readarray lukee muuttujiakin.
# pääohjelma toiminnan kokeilemieksi: 180 188
unset maksimi minimi
matriisi=($(seq 100000 | awk -v seed=$RANDOM 'BEGIN{srand(seed)}{printf "%1.9f\n", rand()}'))
time matriisinmaxmin matriisi maksimi minimi
echo minimi:$minimi' maksimi:'$maksimi
LC_ALL=
***
BASH:in muistissa jo valmiiksi olevasta kaksiulotteisesta matriisista etsitään maksimi näin:
#!/bin/bash
# BASH:issahan voi olla kaksiulotteinen matriisi muistissakin ja mikäli näin on niin nopeinta on etsiä
# suoraan sieltä - tässä on kyse sellaisesta tapauksesta.
# Mutta mikäli se matriisi on jo valmiiksi levyllä niin nopeinta on etsitä heti levyltä.
function kaksiulotteisenmatriisinosoitettavankentänmaximi () { readarray $3 < <(declare | grep ^$1= | cut -d\( -f2- | tr "\'" '\n' | sed '/\[/d;s/\\n//g' | tr -d \) | cut -d' ' -f$2 | sort -n | tail -1) ;}
readarray matriisi < koe
kaksiulotteisenmatriisinosoitettavankentänmaximi matriisi 2 maximi
echo $maximi
***
Funktiotkin toimivat jokaisella suorituskerralla hieman erikauan. Tämä skripti tutkii transpose-funktiota ja nimenomaan sen suoritusaikojen vaihtelua odotettaessa mittausten välissä .1-20 sekuntia - nimittäin LINUX aiheuttaa tuskaa ainakin tällä alueella. Mutta pienin koodimuunnoksin tällä voi tutkia melkein minkätahansa funktion mitätahansa ominaisuutta. Tässä mittaus kestää monta tuntia mutta se on ihan liian lyhyt aika saada dataa josta tosiaan voisi sanoa jotakin.
#!/bin/bash
function transpose () {
numberoffields=$(($( declare | grep ^$1= | cut -d\[ -f2 | tr -dc [[:space:]] | wc -c ) -1 ))
readarray $1 < <(for (( n=1; n<=$numberoffields; n++ )); do
echo $(printf "%s" "${array[@]}" | cut -f$n -d' ' ) # & nopeuttaisi hieman mutta sillä on varjopuolensa
done)
}
< testa readarray array
rm -f ~/koe
for n in $(seq .1 .1 20 | tr , .); do # mitattaessa muita funktioita x-akseli on usein logaritminen
# (esimerkiksi taajuus) ja pyyhkäisy saattaa silloin kannattaa vaihtaa "exponentiaaliseen" askellukseen
# jossa jokainen askel on aina esimerkiksi 5% suurempi kuin edellinen:
# for n in $(echo "for (i = .001 ; i < 1000; i *= 1.05) i" | bc -l ); do echo $n; done
# tällöin gnuplot:ille kannattaa kertoa että x-akseli on logaritminen: set logscale x 10
# suoritusaikoja tarkkaileva pääohjelma;
< testa readarray array # luetaan matriisi uudestaan jokakierroksella jotta alkuasetelma olisi aina sama
sleep $n
alkuhetki=$(date +%s.%N)
transpose array # tämän rivin paikalla olevan funktion ajankäyttöä tutkitaan. Sille voidaan antaa
# parametrejä tai sitten ei.
loppuhetki=$(date +%s.%N)
echo $n' '$(echo $loppuhetki-$alkuhetki | bc) | tee -a ~/koe
done
gnuplot -p -e 'set terminal postscript eps color enhanced; set xlabel "odotusaika"; set ylabel "toiminta-aika sec."; set output "transpose.eps"; plot "~/koe"'
- skripti ei tulosta käyrää suoritusajoista vaan tekee käyrästä tiedoston nimellä transpose.eps. Sen voi lukea tiedostoselaimella - jos se ehdottaa lataamaan lisäpaketin niin lataa se.
- tai eihän se varsinainen käyrä ole vaan tuloste mittauspisteistä.
***
Muutaman mittauksen pohjalta ei mitään tarkempaa voi esittää mutta muutaman huomion tein:
Skriptien toimimisen nopeus heittelee aina - usein luokkaa 10%. Mutta todella ihmeellisiäkin toiminta-aikoja esiintyy - esimerkiksi kestoaika voi joskus olla kaksinkertainen - tai joissain harvoissa tapauksissa normaalia pienempikin.
Äskeisessä skriptissä määriteltiin suoritusaika nanosekunnin tarkkuudella vaikka todellinen suoritusaika vaihtelee usein peräti useita kymmeniä millisekunteja. Sinänsä tuo nanosekunnin tarkkuus on jokseenkin oikea - mutta se kertoo vain sen kuinkakauan suoritus kesti silläkertaa. Käytännössä sen tarkkuudesta jää hyötykäyttöön vain hieman enemmän kuin time-käskystä, sen tarkkudeksi voisi sanoa parisataa mikrosekuntia.
Mutta se kelpaa tilastollisiin tarkasteluihin - tosin noiden tarkastelujen oikea suorittaminen ja tulosten tulkinta on niin vaikeaa että tulokset jäävät usein epätarkoiksi. Mutta yhden asian ne kertovat luotettavasti: kuka on keskimäärin nopein ja antaa pienimmän hajonnan. Muuten tulokset ovat vain suuntaa-antavia.
***
Silloin kun BASH vielä oli elinkelpoinen koetettiin foorumeilla vastata kysymyksiin: voiko BASH:illa tehdä sitä-ja-tätä. Onhan se kiva ehdottaa jotakin, mutta itseasiassa noihin kysymyksiin on ainoastaan yksi vastaus: tapoja tuon tekemiseen on ainakin miljoona eikä kukaan tiedä sitä parasta tapaa - joten hyvä keino osoittaa itsensä idiootiksi on vastata. Mutta koska kaikkihan me idiootteja ollaan niin ...
Tosiasia kyllä on että BASH:in työkalut on päästetty rappeutumaan surkeaan kuntoon eivätkä sen hampaat pure enää juuri ollenkaan.
***
Aikoinaan tällä foorumilla oli juttua kuinka nopeasti kukin skriptikieli lukee tiedoston ja muuttaa siitä määrättavän sanan tai kirjaimen. Näistä normaaleista skriptikielistä nopein oli Python, mutta hyväksi kakkoseksi tuli BASH:in sed - itse BASH oli niin surkea ettei kukaan siitä edes puhunut.
Mutta senjälkeen BASH on nopeutunut suunnattomasti sillä se on saanut käskyn readarray joten tetävästä saa nykyään tehtyä loopittoman version. Ei sen nopeus ole vieläkään kuin vajaa puolet sed:n nopeudesta mutta toiminta-nopeus on sentään jo mielekäs. Ja BASH:issa on koneen muistissa tiedostosta tehty matriisi josta varmaan on joskus hyötyä. Matriisin jokaisen rivin lopussa on muuten ylimääräinen rivinsiirto - joskus se kannattaa poistaa. Tämmöinen se käsky on:
readarray matriisi < tiedosto; printf '%s' "${matriisi[*]//mikä/miksi}" > jokutoinentiedosto
- muuten tuo mikä voi olla myös yksinkertainen regex, esimerkiksi [[:upper:]] tai [0-9]
***
Kukaan ei pysty puhumaan BASH:ista niin ettei puheessa olisi paljonkin korjattavaa. Joten mikäli BASH:ia opettelee oppilaitoksissa kirjojen ja virtuoosien johdolla oppii sen normaalin kyvyttömyyden. Ja mikäli kokeilee ja kertoo kokemuksistaan päätyy usein naurettavuuksiin ja virheisiin. Mutta jos et ota aasinhattua päähäsi niin BASH pysyy ikuisesti niin kuolleena kuin miksi virtuoosit ovat sen saattaneet.
Mutta vaikka virtuooseja rienaankin niin se ei merkitse sitä ettenkö tunnustaisi teorioiden arvoa ja virtuoosithan niitä parhaiten hallitsevat. Mutta erittäin harvat osaavat soveltaa niitä käytäntöön - eivätkä suhtaudu vähääkään kannustavasti kun toiset yrittävät jotain mitä luullaan teorian vastaiseksi - kun itseasiassa kyse on vain teorian väärästä tulkinnasta.
***
BASH:issa on hyvät käskyt tekstijononkäsittelyyn, mutta niiden ulkoasu on useimmiten niin hankala muistaa ja naputella koneseen ettei niitä juuri kukaan käytä. Esimerkiksi määrittely missä pienisana sijaitsee isossasanassa:
apu=kemblefordmetri; echo ${apu%met*}
# joka tulostaa kembleford - ja siitä pääsemme funktioon nimeltään tekstijononalkupaikka:
function tekstijononalkupaikka () { apu=$(echo ${1%$2*} | wc -m); (( $apu > ${#1} )) && echo ei_löydy || echo $(($apu-1)) ;}; tekstijononalkupaikka kemblefordmetri met
joka tulostaa 10. Oikeellisuuden tarkistus:
apu=kemblefordmetri; echo ${apu:10:3} tulostaa: met
- mikäli pienempää tekstijonoa ei isommassa ole niin palautusarvoksi tulee: ei_löydy
- tekstijonon ensimmäisen merkin järjestysnumero on 0.
- skannataan muuten lopusta alkuun - siis m on 10 eikä 2
***
BASH:in funktiot ovat samanlaisia kuin muissakin kielissä eli niitä voidaan kutsua samassa skriptissä eripaikoissa toisilla parametreilla. BASH:in funktiot tuntevat sekä arvo- että nimiparametrit. Niiden käyttäminen:
- arvoparametrit: kuulut joukkoon joka ajattelee "elämä ilman eval-käskyä on yksinkertaisempaa".
- nimiparametrit: haluat parametrit käsitelynjälkeen takaisin. Nimiparametrit eivät muuten ilmanmuuta johda eval-käskyn käyttöön.
Koska funktiot ovat perusosa BASH:ia on niiden kutsuminen erittäin nopeaa (noin 25mikrosec.)
Funktiot kannattaa sijoittaa kirjastoihin. Kirjasto on perusmuodossaan ihan tavallinen tiedosto josa on funktioita. Eikä kirjastossa ole lausetta: #!/bin/bash tai muita shebangeja. Kirjasto-tiedostossa saa olla funktioita kuinmonta vaan ja kirjasto-tiedostojakin saa olla kuinkamonta vaan - tai onhan niillä montakin rajaa: käyttäjän kyvyt tulevat ensin vastaan ja sitten koneesta loppuisi muisti. Kunkin tyyppin asioille kannatta tehdä oma kirjastonsa asianmukaisesti nimettyyn tiedostoon.
Jotta skriptissään saisi kirjastot käyttöön kirjoitetaan skriptissä riviä #!/bin/bash seuraaviksi riveiksi:
. ~/bash_kirjastot/ensimmäisen kirjaston nimi
. ~/bash_kirjastot/toisen kirjaston nimi
. ~/bash_kirjastot/kolmannen ... ja niin monta kirjastoa kun tarvitsee - mutta yksikin kirjastonimi kyllä riittää.
Jotkut eivät halua käyttää kirjastoja. Silloin funktion voi kopioida oman skriptinsä alkuun sieltä kirjastotiedostosta.
***
Funktionimien oikea valinta on tärkeää useastakin syystä:
- kun nimi on kerran valittu tulee sen vaihtamisesta nopeasti vaikeaa olipa siinä kuinka idioottimainen virhe hyvänsä. Nimen täytyy kuvata hyvin sitä mitä funktio tekee - ei saa välittää siitä kuinka pitkä nimestä tulee, sillä tarvittavat nimet tulee jokatapauksessa aina leikata-liimata eikä niitä naputella.
- kun olettehnyt jotakin niin varmaankin haluaisi myöhemmin löytää sen mitä olet tehnyt - ja mikä sen parempi kuin nimi jonka osia voit aavistella. BASH:in merkinnät ovat pikkuisen kummallisia joten vaihtoehtona on etsiä jotain ?<=$2 kaltaista.
Sitä voi itsekseen pähkäillä laittaako nimiin ä:tä ja ö:tä. BASH nimittäin hyväksyy ne mutta editorit eivät aina tykkkää. Sitäipaitsi jos BASH jossain vaiheessa lopettaa toimimisen niiden kanssa niin se aiheuttaa melkoisen hämmingin.
Funktionimissä ei voi olla välilyöntejä edes lainausmerkeissä.
Ovatkoha nämä nimet oikein annettu? Tulevaisuus näyttää:
function hakuasanojenvälinenteksti () { echo "$1" | grep -Po "(?<=$2).*(?=$3)" ;}; hakuasanojenvälinenteksti "jb khavain1[@'123 4567890avain2k jnj" avain1 avain2
# tulostaa: [@'123 4567890
# jos jompaakumpaa avainta ei löydy ei myöskään tulosteta mitään
function avain1onhaussa_avain2täytyyollaperässämuttasitäeivalita () { echo "$1" | grep -Po "$2(?=.*$3)" ;}; avain1onhaussa_avain2täytyyollaperässämuttasitäeivalita "jb khavain111123 4567890avain2k jnj" avain1 avain2
# tulostaa: avain1
function avain1onhaussa_avain2eisaaollahetiperässä () { echo "$1" | grep -Po $2'(?!'$3')' ;}; avain1onhaussa_avain2eisaaollahetiperässä "jb khavain1avain2k jnj" avain1 avai2
# tulostaa: avain1 # mutta ei tulosta mitään kun avai2 muutetaan muotoon avain2
function avain1täytyyollahetiedessämuttasitäeivalita_avain2onhaussa () { echo "$1" | grep -Po "(?<=$2)$3" ;}; avain1täytyyollahetiedessämuttasitäeivalita_avain2onhaussa "jb khavain1avain2k jnj" avain1 avain2
# en edes yritä keksiä nimeä seuraavalle - jolloin siitä voisi tehdä funktion:
# -Poz '(?ism:^BEGIN.*?END)' # i=älä välitä merkkikoosta, s=begin ja end voivat olla eri riveillä, m=merkin:^ käyttö sallitaan lukitsemaan begin rivin alkuun
Pieniten ja melkomerkityksettömien funktioiden nimeämistä ei saa väheksyä. Esimerkiksi kun joudut sanomaan mikä on kirjaimen A ascii-arvo niin sielusta lentää päreitä kun ei muista eikä kovin nopeasti saa siitä tehtyä skriptiäkään. Mutta funktion nimi on kaikenjärjen mukaan: merkkinumeroksi - ja se löytyy nopeasti.
function numeromerkiksi () { printf \\$(printf '%03o' $1) ;}
function merkkinumeroksi () { printf '%d' "'$1'" ;}
***
Ilman ohjeistusta skripti ei saa dataa käsitellessään olettaa millainen aakkosto on ollut siellä missä sen käsiteltäväksi annettu data on tehty. Eikä tulkki voi mitekään tietää ettei mitään kummallista merkkiä ole tulossa esimerkiksi kun skripti etsii jotain matemaattista.
Skriptikoodissa voidaankin antaa tulkille monenlaisia ohjeita, muunmuassa että käytetään yksinkertaisinta koodisivua skriptiä suoritettaessa. Skriptin alkuun laitetaan silloin lause: export LC_ALL=C. Silloin sriptin viimeiseksi lauseeksi on kirjoitettava export LC_ALL= . Skriptin toimintanopeus nousee silloin - yleensä vain vähän mutta joskus yli kaksinkertaiseksi.
- tuo C on kodisivuista yksinkertaisin ja ääkkösetkin temppuilee kun sitä käyttää.
- ongelmaa on aikoinaan ratkottu ja osin onnistuttukin mutta edelleen esimerkiksi nuo muistikuvaa hyödyntävät skriptit nopeutuvat yli kaksikertaa nopeammiksi silloinkuin käsitellään yksinomaan numeroita tai yksinkertaisia kirjaimia.
***
Yritin äskettäin mitata kuinka skriptin suoritusaika vaihtelee kerrasta toiseen. Teorihan on sellainen, että BASH:in tulkin tulkkaustuloksia säilytetään vähän aikaa - osasekunneista sekunteihin. Jos siis mitataan jonkun skriptin suoritusaika monta kertaa odottaen suoritusten välillä 0.1-20 sekutia niin suoritusajat plottaamalla pitäisi saada erittäin valaiseva käyrä. Ja niin saikin, mutta käyrässä oli aivan liikaa kohinaa jotta siitä olisi voinut varmuudella väittää mitään tarkempaa - eikä vuorokaudenkaan mittaus antanut kunnollista käyrää.
Teorian tarkistamisen sotki se että normaalisti skriptin suoritusaika heittelee luokkaa 10% ja joskus suoritusaika peräti kaksinkertaistuu. Toistin nyt saman mittauksen mutta määräsin skriptin alussa: LC_ALL=C. Se nopeutti hieman mutta ennenkaikkea peräkkäiset suorituskerrat kestivät lähes saman ajan. Ja käyrästä sai selvää jo muutamassa minuutissa: näyttää tosiaan siltä että hetkellistä talletusta tehdään ja nimenomaan että säilytettävät unohdetaan nopeasti.
Siis skriptit seikkailevat epämääräisiä aikoja koodisivullaan joten yksinkertaisin koodisivu on usein paras.
***
Toinen ohje minkä skripti voi tulkille antaa on IFS; se merkki jonka kohdalta lause jaetaan sanoiksi.
Kun skriptinsuoritus alkaa on IFS:n arvona: <välilyönti><tab><rivinsiirto> - perusmuodossaan IFS siis muodostuu kolmesta vaihtoehtoisesta merkistä - jako suoritetaan mikä niistä kohdataankaan. Tuo merkki jonka kohdalta jaetaan ei jää kummallekaan puolelle, vaan se häviää.
Mikäli skripti muuttaa IFS:ää olisi mukava jos sen arvo palautettaisiinkin etteivät muutkin pääse nauttimaan kummallisesti toimivasta päätteestä - tämä edellyttää että skriptissä on funktio jonka se suorittaa silloin kun skriptinsuoritus katkeaa virheeseen. Toki skriptin viimeiseksi lauseeksi laitetaan jokatapauksessa: unset IFS . Tuo lopussa suoritettava IFS:n palauttaminen tehdään lauseilla:
trap jälkiensiivous SIGTERM
# trap kirjoitetaan skriptin alkuun alustuksien joukkoon. Kun kaikki alustukset on tehty tulevat funktiot'
# eikä niiden järjestyksellä ole väliä. Kirjastoja käytettäessä funktioita ei ole. Kirjastojen liitoskäskyt
# kirjoitetaan skriptin alkuun muiden alustuksieen joukkoon. Siis fnktioiden joukossa on :
function jälkiensiivous (){ unset IFS ;} # mukaan liitetään kaikki muukin siivottava
***
Alkaessaan harrastamaan BASH:ia ihan jokainen saa eteensä "hyvän konstin" joka on jotain tällaista:
echo "aaa bbb ccc ddd fff" | awk '{print $3}' . Ja tuontapaisia tulee sitten tehtyä aina, sillä toimiiha se ja muistaahan sen helposti. Kitkerä kiitos väärästä opetuksesta. Sillä helposti sitä ei unohda ja totu käyttämään montakertaa nopeampaa menetelmää - tosin se on melko mahdoto muistettavaksi joten kopioipa tästä siitä funktio:
function echofieldno () { read -ra apu <<< "${2}"; echo ${apu[$1-1]}; unset apu ;}
# ja kutsuesimerkki:
teksti="aaa bbb ccc ddd"
echofieldno 3 "$teksti"
- todella pitkistä teksijonoista kentän tulostuminen kestää - kuten esimerkiksi tekstijonosta:
$(echo {100000..0}) - mutta silloinkin toiminta on parikertaa nopeampaa kuin awk:illa.
Tiedostoille sovitettuna käsky olisi ihan toisenlainen:
function catlineno () { tail -n $1 $2 | head -n 1 ;}
# ja kutsuesimerkki:
catlineno 292 /boot/grub/grub.cfg
- eihän tätä tarvita juurikoskaan. Mutta väärällä tavalla tässä millisekuntitehtävästä tulee minuutihomma ja BASH:in maine kasvaa.