Kirjoittaja Aihe: Ohjeita shell-skriptaukseen (bash)  (Luettu 376761 kertaa)

Whig

  • Käyttäjä
  • Viestejä: 356
  • puppu-generaattori
    • Profiili
    • localhost
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #240 : 10.10.18 - klo:09.35 »
Koodia: [Valitse]
list-urls.sh linkit.txt > linkit.html

Kiitän.
Tämä antoi tällaisen herjan:
bash: ./scripti.sh: /bin/sh^M: bad interpreter: No such file or directory

Tosin minulla on pieni epäillys, että tämä johtuu siitä että copy/pastesin scriptin windows koneella tikulle tiedostoon.
Tuo on joskus aikaisemminkin aiheuttanut ongelmia jotka on saanut dos2unix:lla ratkaistua mutta sitä ei tuossa kyseissä koneessa ollut joten joudun testaamaan uudelleen myöhemmin.

nm

  • Käyttäjä
  • Viestejä: 16428
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #241 : 10.10.18 - klo:11.03 »
Tämä antoi tällaisen herjan:
bash: ./scripti.sh: /bin/sh^M: bad interpreter: No such file or directory

Tosin minulla on pieni epäillys, että tämä johtuu siitä että copy/pastesin scriptin windows koneella tikulle tiedostoon.

Kyllä. Vaatii dos2unixin tai vastaavan korjausoperaation CR-merkkien poistamiseen rivinvaihdoista. Pitäisi onnistua myös tr:llä:

Koodia: [Valitse]
tr -d '\r' < scripti.sh > korjattuscripti.sh

Whig

  • Käyttäjä
  • Viestejä: 356
  • puppu-generaattori
    • Profiili
    • localhost
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #242 : 11.10.18 - klo:08.37 »
Tämä antoi tällaisen herjan:
bash: ./scripti.sh: /bin/sh^M: bad interpreter: No such file or directory

Tosin minulla on pieni epäillys, että tämä johtuu siitä että copy/pastesin scriptin windows koneella tikulle tiedostoon.

Kyllä. Vaatii dos2unixin tai vastaavan korjausoperaation CR-merkkien poistamiseen rivinvaihdoista. Pitäisi onnistua myös tr:llä:

Koodia: [Valitse]
tr -d '\r' < scripti.sh > korjattuscripti.sh

Jees. Täytyy ottaa tulevaisuutta varten tuo tr rivi talteen. Sain tällä kertaa onneksi iPhonen kautta yhteyden nettiin ja sain copy/pastettua tuon scriptin ubuntu puolella. Scripti toimi ja teki juuri sen mitä pitikin joten kiitoksia taas kerran.

sigfrid

  • Käyttäjä
  • Viestejä: 5
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #243 : 27.10.18 - klo:17.41 »
Moi,

tuli tenkkapo.  Pitäisi korvata positioarvo toisesta tiedostosta kaikilta riveiltä,  jos vastaava position arvo on ensimmäisessa tiedostossa 9.  Esimerkki

Tiedosto 1:
1221110091211219000121

Tiedosto 2:
1212210011200220000222

Ensimmäisen tiedoston 9. positio tulisi arvoon 1 ja 16. positio arvoon 0.  Tiedostot ovat kooltaan suuria ja identtiset rivimääriltään ja-pituudeltaan.  Voin tietenkin muuttaa rivien merkit sarakkeiksi lisäämällä erottimet merkkien väliin.



nm

  • Käyttäjä
  • Viestejä: 16428
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #244 : 27.10.18 - klo:20.32 »
@sigfrid:

Kuulostaa sen verran hankalalta ongelmalta, että kannattaa ottaa joku Bashia monipuolisempi kieli käyttöön. Vaikka Python, tai mikä tuntuu itselle helpoimmalta.

Whig

  • Käyttäjä
  • Viestejä: 356
  • puppu-generaattori
    • Profiili
    • localhost
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #245 : 04.01.19 - klo:13.33 »
Saan lähetetty sähköpostia komentoriviltä sendemail:lla OK ja saan lähetettyä HTML muotoisia sähköposteja myös joihin saan liitteeksi kuvan mutta mitenköhän sen saisi sisällytettyä itse viestiin?

Tarkoitus olisi siis lähettää sähköposteja joissa on kuva viestissä HTML:n seassa <img src="kuva.jpg"> mutta en millään saa päähäni miten saisin tämän tehtyä. Tiedostonahan tuon saa helposti mukaan -a kuva.jpg:llä mutta tällöin se näkyy viestin lopussa erillisenä liitteenä.

nm

  • Käyttäjä
  • Viestejä: 16428
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #246 : 04.01.19 - klo:15.06 »
Tarkoitus olisi siis lähettää sähköposteja joissa on kuva viestissä HTML:n seassa <img src="kuva.jpg"> mutta en millään saa päähäni miten saisin tämän tehtyä. Tiedostonahan tuon saa helposti mukaan -a kuva.jpg:llä mutta tällöin se näkyy viestin lopussa erillisenä liitteenä.

Voit viitata viestin HTML-koodissa liitteenä olevaan kuvaan CID:llä, kuten täällä on esitetty: https://stackoverflow.com/a/17981118

Koodia: [Valitse]
sendmail -t <<EOT
TO: XXX@YYY.com
FROM: <TEST_IMAGE@YYY.com>
SUBJECT: Embed image test
MIME-Version: 1.0
Content-Type: multipart/related;boundary="XYZ"

--XYZ
Content-Type: text/html; charset=ISO-8859-15
Content-Transfer-Encoding: 7bit

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-15">
</head>
<body bgcolor="#ffffff" text="#000000">
<img src="cid:part1.06090408.01060107" alt="">
</body>
</html>

--XYZ
Content-Type: image/jpeg;name="sathy.jpg"
Content-Transfer-Encoding: base64
Content-ID: <part1.06090408.01060107>
Content-Disposition: inline; filename="sathy.jpg"

$(base64 sathy.jpg)
--XYZ--
EOT



Kuvan voi myös enkoodata suoraan img-tagiin base64-muodossa, mutta kaikki sähköpostiohjelmat eivät tue sitä. Toinen laajemmin yhteensopiva ratkaisu on ladata kuva jollekin nettipalvelimelle ja viitata siihen koko osoitteella.

Whig

  • Käyttäjä
  • Viestejä: 356
  • puppu-generaattori
    • Profiili
    • localhost
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #247 : 07.01.19 - klo:11.53 »
Voit viitata viestin HTML-koodissa liitteenä olevaan kuvaan CID:llä, kuten täällä on esitetty: https://stackoverflow.com/a/17981118


Kiitoksia vinkistä mutta nyt pätkii sen verran pahasti päässä, että en tajua miten homma toimii ja onnistuuko käyttämälläni sendemail:lla samoin, kuin sendmail:lla.
Pitänee lukea tuo ohje vielä pariin otteeseen ja lähteä testailemaan sillä tuskin tuo niin vaikea on, kuin miltä se luettuna näyttää.

Itse viesti tökätään --XYZ rivin väliin ja "<img src="cid:part1.06090408.01060107" alt="">" src:llä viitataan myöhemmin (--XYZ:n jälkeen) "Content-ID: <part1.06090408.01060107>" kohdassa olleeseen .jpg:n jonka polku kerrotaan "Content-Disposition: inline; filename="sathy.jpg"" kohdassa.
« Viimeksi muokattu: 07.01.19 - klo:11.55 kirjoittanut Whig »

nm

  • Käyttäjä
  • Viestejä: 16428
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #248 : 07.01.19 - klo:12.21 »
Itse viesti tökätään --XYZ rivin väliin ja "<img src="cid:part1.06090408.01060107" alt="">" src:llä viitataan myöhemmin (--XYZ:n jälkeen) "Content-ID: <part1.06090408.01060107>" kohdassa olleeseen .jpg:n jonka polku kerrotaan "Content-Disposition: inline; filename="sathy.jpg"" kohdassa.

Content-Type-rivin parametrilla name ja Content-Disposition-rivin parametrilla filename määritellään liitteen tiedostonimi, jonka sähköpostiohjelma sitten osaa näyttää. Epäilen, että Content-Disposition-rivi on sähköpostin tapauksessa turha, tai inlinen sijaan se voisi yhtä hyvin olla attachment. Sähköpostiohjelmat esittävät molemmat samalla tavalla liitteinä.

Joka tapauksessa lähdetiedoston polkua ei määritellä viestissä, eikä sitä anneta sendmailin käsiteltäväksi. Sen sijaan tiedoston sisältö enkoodataan base64-muotoon ja sisällytetään viestiin rivillä:

Koodia: [Valitse]
$(base64 sathy.jpg)
Eli jos kuvatiedosto sijaitsee vaikkapa käyttäjän kotihakemiston Kuvat-kansiossa, sen voisi syöttää base64:lle näin:

Koodia: [Valitse]
$(base64 "$HOME/Kuvat/kuva.jpg")

Lisäys: RFC2183:n mukaan "Content-Disposition: inline" määrittää, että liitteet tulisi esittää automaattisesti, kun viesti avataan. Enpä sitten tiedä, miten moni ohjelma oikeasti tekee eroa inline- ja attachment-asetuksen välillä...
« Viimeksi muokattu: 07.01.19 - klo:13.09 kirjoittanut nm »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #249 : 12.05.19 - klo:09.51 »
BASH-raamattu: https://www.gnu.org/software/bash/manual/html_node/index.html#SEC_Contents on iso - ja epäilenpä ettei sitä kukaan ole edes lukenut kunnolla sillä sen hyödyt selviävät vasta kun hallitse sen kokonaan - ja siihen useimpien muistikaan ei riitä eikä kenenkään aika ainakaan - sitäpaitsi suuri osa sitä tuntuu lasten leikeiltä ja aikuisten lasten temmellyskenttä se onkin.

Mutta onhan BASH:illa sittenkin omat virtuoosinsa sillä suurelta osalta sen tiedot ovat lukemattakin selviä mikäli omaa hyvän pohjakoulutuksen - ennenkaikkea asioista jotka ovat kaikille kielille yhteisiä jotka siis tietää jo ennen BASH:in opiskelua. Mutta BASH on kieleksi niin omituinen että tulkinta-möhläyksiä tulee.

Suomessa pahin este BASH:in oppimisessa on se että BASH-raamattu on Englanninkielinen ja tosiharvoilla taviksilla on riittävä sanavarasto puhumattakaan sen käsitteiden ymmärtämisestä. Ei toki ole useimmilla virtuooseillakaan mutta heistä sentään monilla. Eikä hyvää Suomenkielistä vastaavanlaista esitystä ole.

Ja BASH-raamatussakin asiat kuvataan siten että vain vankan perus-tietopohjan omaavat virtuoosit voivat ymmärtää ne - sillä tiivistetty selitys vaatii paljon pohjatietoja jotta ymmärtäisi kaiken oikein.

Mutta taviksia oli aikoinaan tuhansia kertoja enemmän kuin virtuooseja joten BASH:ille tehtiin karmeaa hallaa sillä tavisten kautta kaikki kehittyy, ei virtuoosien.

Joten:
- opetuksen tulee ehdottomasti tapahtua Suomenkielellä - siitä sekä tavikset että virtuoosit saavat täyden hyödyn
- virtuoosi ei ole hyvä opettaja sillä asiat täytyy esittää taviksille aivan toisin kuin virtuooseille - mutta virtuoosi kyllä ymmärtää taviksille tarkoitetun puheen.

Mutta ainoastaan sellainen tavis joka on yksinään itkien opetellut asiat pystyy niitä myös opettamaan. Joten alanpa kuvata asioita sillä olen itkenyt jo tarpeeksi:

Pikkupojat ovat aina halunneet puhua niin etteivät toiset ymmärrä - tai vielä mukavampaa on jos ymmärtävät väärin. Näin on BASH:issakin ja tätä salakirjoitus-tapaa kutsutaan laajennoksiksi jotka kertovat miten asioita on lyhenetty - siis kuinka pika-kirjoituksella kirjoitetut lauseet tulisi tulkita. Ja siitä se riemu alkaakin sillä usein sama lyhenne laajennetaan eritavoin riippuen asiayhteydestä: esimerkiksi * voi olla kertomerkki tai se voi kuvata kovalevyn jonkun kansion sisältöä tai se voi olla mikä merkki tai merkkiryhmä tahansa. Asiat ratkaistaan seuraavassa järjestyksessä - järjestys on erittäin tärkeää - sillä kun ilmaisun paikalle on kirjoitettu tulkkaustulos niin sitä ilmaisuhan ei senjälkeen enää ole - joten sitä ei enää tulkita mitenkään. Tulkkaus tapahtuu seuraavassa järjestyksessä:

1.  kommentit poistetaan - siis alkaen merkistä # seuraavaan ; merkkiin tai rivinvaihtoon. Mikäli merkki # on keskellä riviä tulee sen edessä olla välilyönti.
2.  {tekstijono} paikalle kirjoitetaan mitä siitä ratkaistuna tulee. Esimerkiksi käsky: echo {1..5} paikalle kirjoitetaan: 1 2 3 4 5 
3.  ~ -merkin paikalle kirjoitetaan skriptinktekijän kotikansion nimi - esimerrkiksi: /home/käyttäjä_nimi
4.  $tekstijono:n paikalle kirjoitetaan sen arvo - jos tekstijonoa ei ole määritelty tulee arvoksi '' eikä 0 - jos ei jotain muuta määrätä.
5.  $(komento) paikalle kirjoitetaan arvo joka komento tulostaa.
    lause jaetaan sanoiksi - normaalisti välilyönnin kohdalta.
6.  $((tekstijono)) paikalle kirjoitetaan sen arvo - tämä on siis se matemtiikkamoottori.
   
7.  mikäli käskyssä vielä on ilmaisu: tekstijono* niin tulkki hyväksyy kaiken joka alkaa sanalla joka on sanan tekstijono paikalla ja perässä voi olla mitävaan
    - siis *  voi olla myös tyhjää mutta jos sen korvaa merkillä + niin sen paikalla täytyy olla ainakin yksi merkki
    - jos tekstijono on tyhjä niin * merkitseekin kotikansion sisältöä - esimerkiksi: echo * on melkein sama kuin käsky: ls ~ . Ei tällä mitään käyttöä ole mutta on hyvä asia tietää se ettei ihmettelisi pitkään miksi skriptit joskus riehahtavat.
   
8.  mikäli käskyssä vielä on ilmaisu: tekstijono? niin tulkki hyväksyy kaiken joka alkaa sanalla joka on sanan tekstijono paikalla ja perässä voi olla yksi merkki
9.  mikäli käskyssä vielä on ilmaisu: [tekstijono] niin mikäl tekstijono kuvaa jonkun ryhmän niin tekstijonon paikalle kirjoitetaan sitä vastaava ryhmä - esimerkiksi [0-9] paikalle kirjoitetaan: 0 1 2 3 4 5 6 7 8 9
    - nykyään suositellaan käytettäväksi rakennetta [[tekstijono]] tai [[:ryhmänimi:]] - esimerkiksi [[:num:]]                                                                                                                                                                              
10. tiedostonimet ratkaistaan
11. lopuksi lainausmerkit poistetaan samoin kuin merkki \ elleivät ne ole lainausmerkkien välissä - älä siis hämmästy kun kohtaat ilmaisun "'"
   
   
- ja kaikissa on edellämainituissa on seuraavankaltaisia ehtoja: 'ellei tulkille ole annettu ohjetta fhöj.gåöh'

***

Tämmöistä tein:
Koodia: [Valitse]
Vanhankin .bash_history:n sai käyttöön kun poisti sen duplikaatit ulkoisella käskyllä:
cat ~/.bash_history | tac | awk '!a[$0]++' | tac > ~/koe . Ja koe-tiedoston tutkimisen jälkeen talletin sen .bash_history:n tilalle.

Samalla kirjoitin .bashrc:hen:
export HISTSIZE=100000
export HISTFILESIZE=100000

HISTCONTROL=ignoredups:erasedups
shopt -s histappend
PROMPT_COMMAND="history -n; history -w; history -c; history -r; $PROMPT_COMMAND"

Mutta myöntää täytyy että liian syvillä vesillä liikutaan.

***

 Kukaan ei kykene tekemään yhtäkään skriptiä jossa ei olisi parantamisen varaa - ja usein parantamista on paljon sillä pitkät ja hitaaat skriptit supistuvat nopeiksi muutaman sanan yksirivisiksi - mutta kannattaako sen vaatima työ on kyseenalaista - eikä niiden kehittämiseen kenenkään omat rahkeet riitä. Joten puhumalla skripteistä mitähyvänsä puhuu ilmanmuuta paljon myös palturia.

***

Väite etteivät BASH:in funktiot kykene palauttamaan parametrejaan on vain puolittain tosi, se nimittäin osaa palauttaa nimiparametrin eikä sitäkään automaattisesti. Useimmilla asian merkitys on hakusessa, mutta virtuooseilta on tahallinen ja ilkeämielinen unohdus että BASH osaa tehdä tuosta parametrin palauttamisesta turhaa. Ikuisesti on tunnettu seuraava esimerkki:
a=b;let $a=55;echo b -> tulostaa 55

Funktiomuotoon sovellettuna tämä on:
Koodia: [Valitse]
function koe () { let $1=55 ;}; koe a; echo $a
joka myös tulostaa 55
- siis passataan nimiparametri.
- $1 eteen lisätään joku käsky, esimerkiksi let, read, readarray, test, eval ...
- let toimii vain numeroiden kanssa.
- readarray toimii vähän erilailla kuin muut:
Koodia: [Valitse]
function koe () { readarray $1 < /boot/grub/grub.cfg ;}; koe a; printf "%s\n" "${a[@]}"
Väite kyvyttömyydestä palauttaa parametrejä ontuu muutenkin vähäsen sillä kun joku kirjoittaa näytölle - ei ole väliä  mikä - niin toinen voi käydä lukemassa sen sieltä; toiminta on erittäin nopea eikä se jätä näytölle jälkeä siitä että sitä on käytetty. Tosin se toimii ongelmitta vain kun tulostusta on yksi rivi mutta se riittääkin usein. Esimerkiksi
Koodia: [Valitse]
function koe () { echo töttöröö ;}; a=$(koe); echo $a           # tulostaa: töttöröö
function koe () { cat /boot/grub/grub.cfg ;}; a=$(koe); echo $a # koko teksti menee yhdelle riville ja se on usein ongelmallista.
Siis BASH:in funktiot eivät palauta parametrejään automaattisesti vaan se pitää määrätä.
Ja aina voi funktiossakin kirjoittaa levylle ja pääohjelmassa käydä lukemassa sieltä. Keino on ihan ongelmaton ja se hidastaa toimintaa olemattoman vähän - ajatuskin on kyllä inhottava.

Nopeasti toimiva matriisin sorttausfunktio numero-arvoille
=====================================
- sort-ohjelma sorttaa mitä sille tulostetaan - on aivan sama tulostetaanko sille tiedosto vaiko matriisi.
- matriisia ei muodosteta uudestaan funktiossa, vaan pääohjelmassa käydään näytöltä lukemassa funktioon tulostus ja muodostetaan matriisi uudestaan senperusteella - sentakia kutsu on omituinen mutta toiminta on paljon nopeampaa.
- 1900 jäsenisen matriisin sorttauksessa kuluva aika on noin 60ms
 
#!/bin/bash
export LC_ALL=C # desimaalipiste on tällä sivulla piste eikä pilkku niinkuin normaalilla koodisivulla ->
                # desimaalit sorttautuvat oikein samoinkuin tieteellinen esitysmuoto kytkimellä -g.
Koodia: [Valitse]

function sorttaamatriisi () { declare | grep ^$1= | sed "s/$1=//;s/\s\[/\n/g" | cut -d= -f2 | tr -d \(\)[]\" | sort -g ;} # sort-käskyn parametreja saattaa joutua muuttamaan.

#mat=($(seq 10000;seq 10000))
mat=($(seq -5700 1.5 5700;seq -5700 1.0  5700)) # noin joka neljäs on duplikaatti ja niiden pitää löytyä
#echo matriisi sorttaamatta: ;echo ${mat[*]}
alkuhetki=$(date +%s.%N)
mat=($(sorttaamatriisi mat))
loppuhetki=$(date +%s.%N)
echo matriisi sortattuna: ; echo "${mat[*]}"
echo -n sorttaukseen kulunut aika sekuntia: ; echo $loppuhetki-$alkuhetki | bc
(( ${#mat[*]} > 1 )) && echo kysessä on tosiaan matriisi
# duplikaattien lukumäärä
singlikaatit=$(echo "${mat[*]}" |  tr ' ' '\n' | sort | uniq | wc -w)
kaikki=$(echo "${mat[*]}" | wc -w)
echo duplikaatteja $((2*($kaikki-$singlikaatit)))
export LC_ALL=
- BASH:in sorttausrutiinit syövät usein duplikaatteja ja testatessa on syytä varmistaa että duplikaatteja ei syödä - esimerkiksi laskemalla ne ja vertaamalla siihen paljonko niitä pitäisi olla.
- jos luulee työskentelevänsä matriisin kanssa niin kannattaa testata työskenteleekö matriisin vaiko samannimisen muuttujan kanssa - jossa matriisin koko sisältö on on yhtenä tekstijonona. 
- nimenomaan kannattaa huomioida ettei tässä funktiossa tarvita eval-käskyä.
- tämä kykenee sorttaamaan vain numeroita - ja yksinkertaista tekstiä.
- toki parametrin voisi "palauttaakin". Mutta aikaa se vain tuhraa.

***

Verkkosivuilla esitetystä BASH-koodista ei juuri koskaan skriptin kokeilemisen takia tarvitse tehdä skripti-tiedostoa eikä edes lausetta #!/bin/bash välttämättä tarvita. Riittää kun maalaat koodin kokonaisuudessaan ja painat ctrl-c. Siten avaa pääte painamalla ctrl-alt-t ja sitten paina ctrl-shift-v . Lopuksi kannattaa painaa enter. Näyttö ei ole ihan siisti mutta asia selviää kyllä.

***

BASH:illa oli aikoinaan kirjastot ja niitä alettiin oppia hyödyntämään. Mutta BASH:ista halutaan eroon eikä se passannut ja niinpä tietoturvaan vedoten BASH:in kirjasto-osoitin poistettiin. Se ei tee kirjastojen käyttämisestä mahdotonta mutta koska entiset kirjastoja hyödyntävät skriptit lakkasivat toimimasta niin koko kirjastoidea haudattiin.

Mikäli kieleltä viedään sen kirjastot niin kielestä tulee kelvoton - niinkuin BASH:ista tuli kun siltä vietiin kirjastot. Ja BASH:issa kirjastot ovat vielä tärkeämpiä kuin muissa kielissä sillä BASH:in toiminnoista monet ovat kammottavaa merkkisotkua jota ei saa kasattua kohtuullisella työllä mitään kevollista edes asiaan vihkiytynyt.

Silloin kun BASH:illa oli kirjastot se olikin kiistaton kunkku. Mutta monikin haluaa kunkuksi kunkun paikalle ja sehän ei onnistu mikäli vanha kunkku on edes jotenkin kelvollinen. Siispä tietoturvaan vedoten BASH:in kirjasto-osoitin poistettiin. Se ei tee kirjastojen käyttämisestä mahdotonta mutta koska entiset kirjastoja hyödyntävät skriptit lakkasivat toimimasta niin koko kirjastoidea haudattiin.

Mutta mikäli ohjelmointikielen kirjastoista ei löydy jotain ominaisuutta ja joutuu koodaamaan itse tuon ominaisuuden kestää koodaaminen joskus iäisyyksiä ja koodista tulee suurinpiirtein aina hyvin hidas virhegeneraattori. 

Kuitenkin maailmalla on vieläkin monia BASH-kirjastoja - joitain löytyy netistäkin. Mutta tällähetkellä tilanne on sellainen että jos haluat olla aikaansaapa skriptaaja niin sinun on tehtävä omat kirjastosi - sillä jos hyvästä koodista ei ole kirjastofunktiota rönsyilee sen käyttö ikuisesti.

Seuraava isku BASH:in tuhoamisessa oli eval-käskyn lainsuojattomaksi julistaminen. Siinäkin käytettiin tekosyytä että eval-käsky on tietoturvariski - ja onkin ilmanmuuta selvää että se on tieoturvariski sillä koko ATK on pelkkää tietoturvariskiä. Kirjastofunktioista valtaosa tarvitsee eval-käskyä.

Tosiasiahan on että BASH on ohjelmointiin sopimaton varsinkin kun toiset heittelee kapuloita rattaisiin. Muiden hämärähommien lisäksi huomasin omituisen ilmiön: skripti toimi yhtänopeasti surkealla tabletilla kuin tehokkaalla pöytäkoneella. Millätavoin BASH:in nopeutta rajoitetaan? Sillä rajoittamistahan on pakko harrastaa moniajoympäristössä tai muuten joku´omii kaikki resurssit - mutta BASH:in suhteen koko raudan parantuminen viedään ilmeisesti toisiin käyttöihin.

Pahan iskun BASH:ille ovat antaneet BASH:in kehittäjät itsekin jättämällä opettamatta sen että BASH-kieliset loopit hidastavat BASH:in toiminnan mateluksi.

***
 
Sensijaan että tekee skriptiin BASH-kielisiä looppeja tulisi käyttää BASH:in monessa käskyssä sisällä olevaa looppia - ja on käskyissä hyödynnettäväksi nopeita mariisejakin. Käskyjen sisäisten looppien hyödyntäminen nostaa BASH:in toiminnan nopeammaksi. Nämä esimerkit ovat työskentelyä matriisin kanssa - mutta toisaalta kaikki on matriisia. Muuten looppien hidastava vaikutus kertautuu jos niitä hölvätään jokapaikkaan.

Kokeile itse:
Koodia: [Valitse]
matriisi=({1..1000000}); time printf "${matriisi[*]}"                      # kestää 1.2s
matriisi=({1..1000000}); time for n in {1..1000000}; do printf $n' '; done # kestää 7.5s vaikka looppi on nopein mahdollinen - tuommoiseen looppiin ei voi laittaa muutujaa, mutta loopit jotka hyväksyvät muuttujan ovat parikin sekuntia hitaampia.


- nopeuden muutokset ovat sittenkin pieniä. Mutta koko skriptin osalta nämä pinet erot usein kertautuvat eivätkä summaudu.
- ei BASH:in kanssa pidä toimia kovin suurten matriisien kanssa mutta tämä on vain osoitus että nekin toimivat. Suurten matriisien sivuvaikutukset ovat myös suuria siinä päättessä jossa toimitaan. Ehkä raja on jossain välillä:  20.000 - 200.000. Sen näkee myös liitteistä.

***

Kun käsitellään tiedostoja se ei onnistu kovalevyllä vaan siitä täytyy ensin tehdä käskyn sisällä matriisi RAM:miin. Eihän silloin ole kovin odottamatonta ole että totunnaisesti vain tiedostojen käsittelyyn käytetyt käskyt toimivat matriisienkin kanssa? Esimerkiksi matriisin sorttaus onnistuu ihan hyvin mikäli matriisi tulostetaan sinne sort-rutiiniin:
Koodia: [Valitse]
matriisi=({1..1000000}); time echo "${matriisi[*]}" | tr ' ' '\n' | sort -nr    # kestää 2.5 sek
tai saman tekee "käänteinen cat" tässä erikoistapauksessa
matriisi=({1..1000000}); time echo "${matriisi[*]}" | tr ' ' '\n' | tac 

Sitkeässä elää harhaluulo että funktion parametrin palauttamisesta olisi jotain hyötyä. BASH:issa funktioon lähetetään muuttujan nimi - siis matriisinkin tapauksessa vain nimi. Funktio käsittelee muuttujan. Funktiosta ei tarvitse palauttaa mitään sillä muuttuja on muuttunut BASH:in kirjanpidossa.

Seuraava matrisin sorttausfunktio joka sopii pelkästään numeroille ja tekstille jossa ei ole välilyöntejä ei palauta parametreja sillä se olisi tarpeetonta:
Koodia: [Valitse]
function sorttaamatriisi () { readarray $1 < <(eval echo \${$1[*]} | tr ' ' '\n' | sort -nr) ;}

- 20.000-jäsenisen matrisin sorttaaminen kestää noin 150ms  - täysin uskomatonta BASH:ista.
- eval-käsky mahdollistaa funktion tekemisen - readarray vain yksinkertaistaa koodia.
« Viimeksi muokattu: 05.11.24 - klo:09.50 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #250 : 30.06.19 - klo:13.48 »
Tuossa edellisessä funktiossa on pari puutetta jotka kaipaavat välitöntä korjausta: ensiksi on turha antaa ammuksia BASH:in tuhoajille tuolla eval-käskyllä jos voi olla antamatta - ja toisekseenkin tuo jokeri * on ongelmallinen ja teoriassa pitäisi käyttää viisikertaa hitaampaa jokeria @. Kumpikin korjautuu kun käsitellään BASH:in kirjanpitoa. Nimittäin käsky-yhdistelmä:

declare | grep ^matriisin_nimi=   
tulostaa sen, kuinka BASH pitää kirjaa matriisin_nimi nimisestä matriisista. Sen avulla voi tulostaa matriisin arvot:
Koodia: [Valitse]
mat=(1 kaksi 3 4 "viisi ja puoli" kuusi 7 8 9 0); declare | grep ^mat= | sed "s/mat=//;s/\s\[/\n/g" | cut -d= -f2 | tr -d \)\"
Siitä saa kasattua erittäin nopean sorttausfunktion jossa ei ole eval-käskyä eikä ongelmallista tähteä:
Koodia: [Valitse]
sorttaamatriisi () { readarray $1 < <(declare | grep ^mat= | sed "s/mat=//;s/\s\[/\n/g" | cut -d= -f2 | tr -d \)\" | sort -g) ;
Muistikuvan arvot voidaan erottaa myös jollakin sopivalla regex:ällä, esimerkiksi:
Koodia: [Valitse]
grep -Po '(?U)\".*\"' | tr -d \" tai:grep -Po '(?<==\").*?(?=\")'  tai:grep -Po '(?<=\")[0-9]*'

- nimenomaan huomioi ettei mitään tulosteta - eihän sitä pyydetäkään
- matriisin tekstijäsenet erottuvat näppärästi noilla regex:llä - välilyöntejäkin saa olla. Mutta käytännossä tekstissä saattaa olla kaikenlaista inhottavaa ja tiedostot ovat ihan kauheita. Tekstillä kannattaa käyttää sitä eval-versiota.
- pienin koodimuutoksin sortataan osoitteet. Varsinkin assosiatiivisten matriisien kanssa se on tarpeellista. Osoitteet saa erotettua vaikka käskyllä:
Koodia: [Valitse]
mat=({1..10}); declare | grep ^mat= | grep -Po '(?U)\[.*\]' | tr -d []

***
 
Muistikuvan avulla saa tehtyä matriiseille (=siten myös tiedostoille) monenlaista, esimerkiksi etsittyä maximia, minimiä, keskiarvoa tai jotain muuta odotusarvoa.Tai etsittyä pisimmän jäsenen pituus, suoritettua monenlaisia vertailuja ... samankaltaisia nopeita on lukemattomia.

Kyllähän nämä muistikuva-muutokset jäävät toiseksi kaikille: sed, awk, perl, python - siinä se BASH:in kirous onkin: professorit kehittävät vaikka mitä uudella nimellä koska siten saavat omankin nimensä esiin varsinkin painottaessaan että sitä heidän kehittämäänsä tarvitaan kun BASH ei kykene - tai eihän professorit voi enää tunnustaa että ovat joskus BASH:ia käyttäneetkään.

***

BASH loistaa sellaisten tutkimuksien tekemisessä joista alkuun epäillään että mahtaako niissä mitään tutkittavaa olla. Mutta BASH:illa tekee nopeasti riittäävän hyvän skriptin sen päättelemiseksi kannattaako tehdä tutkimukseen kunnon ohjelma.

Aloin noiden muistikuvaa käsittelevien regex:ien suhteen epäillä että regex:n ajankäyttö kasvaa lievästi exponentiaalisesti datamäärän kasvaessa ja tein siitä käyrän - asia on tärkeä siksi että regex:ää käytetään monissa kielissä. Epäily osottautui vääräksi - mutta sensijaan paljastui että regex:illä on vaikeuksia sisäisten datarakenteidensa kanssa - tai BASH:illa muistinvarauksessa. Siis tarkastelun kohteena on regex: grep -Po '(?<==\").*?(?=\")' ja rinnalla on normaali muistikuvaversio.

Käskyn koodi on tässä (ja se toimii heti kun kopioit sen päätteeseen - muista painaa return. Skriptin toimimisen jälkeen kotikansioon tulee tiedosto regex.eps. Sen saa näkyviin tiedostoselaimella - joskus se toimii vasta kun antaa ensin käskyn: sudo apt-get install evince):
Koodia: [Valitse]
rm -f regex.eps regex;
for (( n=1; n<=1000000; n=n+500 )); do echo $n; unset mat; mat=$(seq -s' ' $n); alkuhetki=$(date +%s.%N); declare | grep mat= | grep -Po '(?<==\").*?(?=\")' ; echo $n' '$(echo $(date +%s.%N)-$alkuhetki | bc ) >> regex; done; gnuplot -p -e 'set terminal postscript eps color enhanced; set xlabel "matriisin koko"; set ylabel "aika sec."; set output "regex.eps"; plot "regex"'
Käsky on pitkäpötkö. Mutta ei sitä kannata paloitella sillä se on suora ja yksioikoinen. Skriptin toiminta kestää viitisen minuuttia sillä dataa liikkuu kymmenkunta gigaa - skriptin toimiessa näytöllä pitää olla kasvava numero osoittamassa että jotain tapahtuu ja missä mennään.

***

BASH:in matriiseja inhotaan ja väheksytään monesta syystä. Yksi väite on se etteivät BASH:in matriisit mitään matriiseja olekaan vaan mitälie vektoreita.

Mutta tavallaan se ei pidä paikkaansa. Kirjoita tiedostoon nimeltä koe:
1 22 333
-4444 5.'$5e55 6666666
7777777 "8888 8888" 9999999999

ja anna sitten käsky:
Koodia: [Valitse]
readarray mat < koe; printf "%s" "${mat[@]}"
jolloin tulostuu:
1 22 333
-4444 5.'$5e55 6666666
7777777 "8888 8888" 9999999999

mutta BASH:in kehittäjät ovat unohtaneet yhden asian: BASH osaa erottaa siitä jäsenen vain kovin hankalasti:
Koodia: [Valitse]
set -- ${mat[rivi_numero]}; echo $jäsenen_numero. Siis esimerkiksi:
set -- ${mat[1]}; echo $2 -> tulostaa: 5.'$5e55
Hidasta ja hankalaahan tuommoinen on, mutta matriisista on silti kysymys.
- matriisin jäsenissä saa olla kaikkia merkkejä paitsi välilyöntejä sillä välilyönnit on varattu erottamaan jäseniä.

Vielä hankalampaa on kirjoittaa tuohon muisti-matriisiin. Esimerkiksi seuravankaltainen toimii:
Koodia: [Valitse]
set -- ${mat[rivi]}; mat[rivi]=${@:1:alkio-1}' uusi '${@:alkio+1}

***

Ei tuollaista "kaksiulotteista" matriisia tarvise editorissa tehdä ja lukea sitten "readarray"-käskyllä vaan onnistuu se ihan BASH:issakin - erikoismerkkejä ei kylläkään hyväksytä, mutta matriisin saa helpommin alkamaan ykkösestä:
Koodia: [Valitse]
rivi1=(1 22 333)
rivi2=(-4444 5.5e55 6666666)
rivi3=(7777777 "8888 8888" 9999999999)
mat=([1]=${rivi1[@]} [2]=${rivi2[@]} [3]=${rivi3[@]})

Siitä saa arvot kahdella tavalla:
set -- ${mat[rivino]}; echo $sarakeno
echo ${mat[rivino]} | awk "{print \$sarakeno}"
- esimerkiksi:echo ${mat[2]} | awk "{print \$2}" tulostaa: 5.5e55

***

Pitää täysin paikkansa että BASH:in matriisioperaatiot ovat niin omituisia että niitä kannattaa välttää mikäli niitä joutuisi koodaamaan. Mutta silti ne toimivat ja mikäli toiminta tapahtuisi kirjastoista noudetuista funktioissa niin koodista saisi selväpiirteistä, koodi nopeutuisi suunnattomasti ja tietoturvaankin saataisiin jonkinlainen ote.

Tai itseasiassa ei siihen kirjastoja tarvita: funktion/funktioita voi toki lisätä koodiinsa alkuun. Pitäisi vain olla paikka josta niitä funktioita voisi kopioida.

Kirjastojen merkitys on helppo todeta: tee millä kielellä hyvänsä jotain monimutkaisempaa ilman kirjastoja.

***
BASH on surkimus. Suurin syy taitaa olla se että BASH on tehty opetustarkoituksiin ja siihen on tosiaan laitettu paljon sitä opetettavaa. Siis sitä ei ole tarkoitettu ohjelmointiin eikä se siihen kunnolla sovikaan.

Jokaisen toiminnan voi koodata lukemattomilla täysin erilaisilla tavoilla - ja vain muutama niistä on lähes virheetön. On liiankin helppoa saada aikaiseksi skriptejä jotka toimivat vain kun niille annetaan dataa josta ne pitävät.

Kirjastot korjaisivat sen että skriptintekijän täytyy oppia BASH:in salat. Ja kirjastothan toimisivat, niitä ei vain ole kirjoitettu - tämä ei pidä täysin paikkaansa sillä netistäkin löytyy aikamonta.

Tämä kirjastojen puute koskee meitä tavallisia tallaajia - roistoilla on kyllä kirjastot: ja katsopas mihin se on johtanut. Olisikohan aika käyttää skripejä puolustukseenkin?
 
Eikä BASH:ia pidä esittää negatiivisessa valossa sillä se olisi oman oksan sahaamista. Nimittäin BASH:ista ei tulla pääsemään eroon ihan pian. Ja jos BASH tuhotaan jossakin distrossa menee tuo distro perässä.

***

Yleensä funktiot ovat tyyppiä: function () { ... }. Silloin funktiolla ja pääohjelmalla on samat muistikuvat - ja kummassakin paikassa on muistikuvaan sekä luku- että kirjoitusoikeus. Jos funktiossa muutetaan muistikuvaa muuttuu se pääohjelmassakin. Parametreja ei siis tarvise palauttaa jos muuttaa parametrin muistikuvaa - joka on yleensä huomaamattoman pieni toimenpide. Funktiossa voi muuttaa niin monen muuttujan tai matriisin muistikuvaa kun haluaa.

Levylle tallettaminen ja levyltä lukeminen kannattaa toteuttaa muistikuvan avulla. Siinä on kahdet menetelmät:

Moitteettomat menetelmät jotka sopivat myös assosiatiivisille matriiseille, noille äsken kuvatuille kaksiulotteisille elikä kun matriisin jäsenet ovat matriiseja:
Koodia: [Valitse]
function matriisilevylle () { declare | grep ^$1= > $2 ;}
# sitä kutsutaan: matriisilevylle matriisin_nimi tiedoston nimi
# "declare -p" on kolmekertaa nopeampi mutta se ei toimi assosiatiivisten matriisien kanssa.   
   
function matriisilevyltä () { . "$1" ;}
# sitä kutsutaan: matriisilevyltä tiedoston_nimi

# siis nämä soveltuvat myös assosiatiivisen matriisin talletukseen ja -lukuun.
# Siinä yhteydessä tarpeellinen funktio:
function tulostamatriisi () { echo "arvo     osoite"; paste <(IFS= eval printf "%s\\\n"  "\${$1[@]}") <(IFS= eval printf "%s\\\n" "\${!$1[@]}") ;}

Nopeat ja likaiset menetelmät jotka sopivat parhaiten yksinrivisille numeromatriiseille silloin kun osoitteesta ei välitetä:
Koodia: [Valitse]
function matriisilevylle () { declare | grep ^$1= | tr ' '  '\n' | cut -d\" -f2 > $2 ;}
# sitä kutsutaan: matriisilevylle matriisin_nimi tiedoston nimi
   
function matriisilevyltä () { readarray $1 < $2 ;}
# sitä kutsutaan: matriisilevyltä matriisin_nimi tiedoston_nimi

- funktioita käytetään kirjoittamalla ne skriptin alkuun - jos ei halua sotkeutua kirjastoihin.
- funktionimissä voi käyttää ä:tä ja ö:tä.

***

Yksinkertaisen tekstijonon saa matriisiksi helposti: matriisi=($tekstirivi). Mutta muunnos muuttuukin todella monimutkaiseksi mikäli tekstijonossa on heittomerkein suojeltuja lauseita joissa on välilyöntejä - käytännössä niitä usein on, ei  tämä ole keksitty erikoistapaus. Koko ratkaisukoodi on tässä sillä funktiokutsukin kaipaa selittämistä:
Koodia: [Valitse]
#!/bin/bash
function tekstijonostamatriisiksi () {
teksti=${@:1:$#-1} # lähtöpään funktiokutsu jakaa tekstijonon välilyöntien kohdalta aina uudeksi parametriksi. Tässä funktioon tulleista parametreista kootaan se munnettava tekstijono takaisin yhtenäiseksi ottaen huomioon että viimeisenä parametrina on sen matriisin nimi johon muunnos suoritetaan.   

# tehdään apumatriisi niistä heittomerkkien välisistä tekstijonoista:
apumatriisi=($(echo " $teksti" | tr \" '\n' | sed 's/^ .*//g;s/.*/\"&\"/g;/\"\"/d;s/\"\\n\"/\" \"/g;s/ /$£/g'))

# korvataan löydettyjen heittomerkkien välissä olevien tekstijonojen välilyönnit $£-merkkiryhmällä:
for (( n=0; n<=${#apumatriisi[@]}; n++ )); do apustring1=${apumatriisi[n]}; apustring2=$( echo ${apumatriisi[n]} | sed 's/$£/ /g'); teksti=$( echo ${teksti//$apustring2/$apustring1}) ; done
 
# muodostetaan tullesta teksijonosta pyydetty matriisi - jossa siis on $£-merkkiryhmät heittomerkkien välisten tekstijonojen välilyöntien paikalla:
apu=("$teksti")

# muodostetaan muistikuva nimellä: ${@:$#} (joka on sen matrisin nimi joka olisi tarkoitus muodostaa) korvaten samalla välilöynneillä merkkiryhmät: $£ :
readarray ${@:$#} < <(printf "%s\n" ${apu[@]} | sed 's/$£/ /g')
}



# kutsuva pääohjelma
echo jokumatriisi:
unset jokumatriisi # varmistaa että mitä tulostuukin niin se tulee tässä kutsutusta funktiosta
teksti='0 "1 2" "2 3" 4 5 "6 7" "8 9" 10 "11 12 13 14 15 16" 17 18 19 "20 21 22 23 24"'
tekstijonostamatriisiksi $teksti jokumatriisi
printf "%s" "${jokumatriisi[@]}"                 # tuloste on sama olipa kyseessä matriisi tai muuttuja
echo matriisissa on jäseniä: ${#jokumatriisi[@]} # joten täytyy testata montako jäsentä siinä on.

echo; echo jokutoinenmatriisi:
teksti='"koira kylmällä kalliolla" "^ _ $ / #" 2 "kissa kuumalla katolla" "Åålannista Öölannin kautta Ääniselle"'
tekstijonostamatriisiksi $teksti jokutoinenmatriisi
printf "%s" "${jokutoinenmatriisi[@]}"
echo matriisissa on jäseniä: ${#jokutoinenmatriisi[@]}

echo; echo jokumatriisi on arvoiltaan edelleen sama:
printf "%s" "${jokumatriisi[@]}"
- varmaankin joku awk- ja regex-velho saisi samasta aikaiseksi nopeasti toimivan version.

***

muistikuvan manipulointia tapahtuu normaaleissakin skripteissä aina kun jollekin muuttujalle tehdään jotakin: olkoonpa kyse tavallisesta muuttujasta tai matriisista - niin pääohjelmassa kuin funktiossakin. Eikä siinäkään ole uutta että funktiossa siirretään muistikuva osoittamaan jotain toista muuttujaa sillä toiminto on tunnettu aina; toiminto vastaa täysin parametrin palauttamista. Kysymys on siitä ettei haluta puhua BASH:ista mitään positiivista.

Se merkitsee vähän että nopeus nousee huomattavasti sillä nopeus on edelleen kelvottoman huono. Vaan nimenomaan se merkitsee että voidaan käyttää totunnaisempia merkintätapoja - ne BASH:in kummalliset merkinnät ovat funktioissa ja teoriassa käyttäjien ei tarvitse tehdä funktioita vaan noutaa ne kirjastoista.

***

Päätteeseen tulostuu usein tekstiä monta sivua - joskus jopa kymmeniä. Mutta kaikki se on tallessa näyttöbufferissa. Näyttöbufferia voi selata rullahiirellä  tai jos hiiri on tavallinen niin oikean reunan "hissillä". Kun on selannut jonnekin historian hämäriin niin kun näpäyttää enter palataan heti sinne mistä lähdettiin. Voit siis käydä katsomassa mitä aikaisemmin tulostettiin.

***

Tässä yksi mitättömän pieni ja merkityksetön funktio mutta kun samantapaisia alkaa olla tarpeeksi monta niin merkitystä alkaa tulla. Kyseessä on funktio joka kääntää matriisin.

Ainahan näitä matriisin kääntöohjelmia on ollut ja jos haluaa pysyä BASH:issa niin esimerkiksi awk-toteutukset ovat paljon nopeampia - minkätakia tämmöistä tehdä? Osittain osoituksena että kyllä BASH osaa kaksiulotteisiakin matriiseja sekä tehdä että käsitellä. 

Matriisissa saa olla tekstiä tai numeroita mutta välilyönnit on tarkoitettu erottamaan alkioita eivätkä lainausmerkit auta tässä yksinkertaisessa toteutuksessa:
Koodia: [Valitse]
#!/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' ' )
done) # > testb # tämä muodostaa levytiedoston käännetystä kun ensimmäisen kommenttimerkin poistaa
}

# Pääohjelma toiminnan kokeilemiseksi:
readarray array < testa
echo "matriisi alunperin:"
printf "%s\n" "${array[@]}"
echo "matriisi käännettynä:"
transpose array
printf "%s" "${array[@]}" | column -t
echo

- siis tiedostoon nimeltä: testa kirjoitetaan esimerkiksi:
kissa ja koira ovat ikuisia vihollisia 0 1 2 3 4 5 6 7 8 9 20 11 12 13 14
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 62 63

niin skripti tulostaa ajettaessa:
kissa       21  42
ja          22  43
koira       23  44
ovat        24  45
ikuisia     25  46
vihollisia  26  47
0           27  48
1           28  49
2           29  50
3           30  51
4           31  52
5           32  53
6           33  54
7           34  55
8           35  56
9           36  57
20          37  58
11          38  59
12          39  60
13          40  62
14          41  63

- toki matriisi voidaan muodostaa BASH:issakin käyttämättä tiedostoa. Mutta ehkä näin on selvempää
- skripti kääntää matriisin samaan matriisiin missä se funktioon toimitettiinkin ja tulostaa sen samalla.
- koodilisäyksillä tulostumisjärjestyksiä voi muuttaa mielinmäärin, suorittaaa sorttaamista, matematikkaa ...
- tuo loppussa oleva: " | column -t " kutsuu automaattiformatointi ohjelmaa.
- BASH:in kehittäjän ratkaisu readarray:n toimintaan aiheutti kylläkin paljon:
1. Tulostusasultaan matriisit ovat melkein normaaleja. Sen aikaansaaminen edellytti varmaan melkoisia ponnistuksia eikä kai seuraavista pitäisi valittaa:
2. readarray lisää näkymättömiä merkkejä - muunmuassa rivinsiirron ja muutenkin se murjoo muistikuvaa.
3. BASH:in koko sparse-matrix-käsitteen tilalle tuli melkein sama "osoitteeton" kuin muissakin kielissä.
« Viimeksi muokattu: 24.07.19 - klo:12.19 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #251 : 25.07.19 - klo:14.06 »
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.
Koodia: [Valitse]
#!/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:
Koodia: [Valitse]
#!/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:
Koodia: [Valitse]
#!/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:
Koodia: [Valitse]
#!/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:
Koodia: [Valitse]
#!/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.
Koodia: [Valitse]
#!/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:
Koodia: [Valitse]
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:
Koodia: [Valitse]
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:
Koodia: [Valitse]
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ää:
Koodia: [Valitse]
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.

Koodia: [Valitse]
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:
Koodia: [Valitse]
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:
Koodia: [Valitse]
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:
Koodia: [Valitse]
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.
« Viimeksi muokattu: 27.08.19 - klo:09.11 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #252 : 05.08.19 - klo:20.41 »
Ja samantapaisia hitaita menetelmiä tulee käytettyä monissa tehtävissä: ei kannata käskeä "cat tiedosto | grep jotakin". On nopeampaa toiminnallisesti ja helpommin käsittävääkin käskeä: "grep jotakin tiedosto" - ja sama koskee useimpia muitakin käskyjä: ei kannata käskeä: "cat tiedosto | tail -1" vaan kannattaa käskeä: "tail -1 tiedosto". 

Miksi noitä putkia on alunperinkään opetettu laittamaan jokapaikkaan? 

- awk ja sed ovat yksinään paljon tehokkaampia kuin BASH, mutta BASH:in apulaisiksi niistä ei aina ole sillä ne ovat ulkoisia käskyjä joten niiden lataaminenkin kestää. Niitä kannattaa käyttää vain kun niille annetaan ratkaistavaksi monimutkainen tehtävä.

***

Skriptien nopeuttamiseksi on miljoonia muitakin silkkaa BASH:ia olevia keinoa - eivätkä ne ole kummallisia niinkuin tuo LC_ALL=C joka ei aina edes tehoa. Ja mikäli jollekin toiminnalle löytyy BASH-käsky niin se on myös nopeampi kuin sed ja awk-viritykset. Toisaalta BASH kyllä kompastelee kaikissa esteissä. Esimerkiksi tämmöisiä löytyy:
Koodia: [Valitse]
apu=kilometri; echo ${apu//[a-l]/x}  # tulostaa: xxxomxtrx. Elikä tehdään BASH:issa se muunnos joka
# yleensä tehdään sed:issä. Ja BASH:kin tuntee regex:än, vaikka varsin rajoitetusti. 

apu=kilometri; echo ${apu//kilo/'~'} # tulostaa: ~metri elikä vaihdetaan sananosa eikä merkkejä.

apu=kilometri; echo ${apu#*ilo}                          # tulostaa:metri elikä toimitaan sananosilla
apu=kilometri; apu=$(echo ${apu%*tri}); echo ${apu#*ilo} # tulostaa:me sieltä välistä
# siis kun echotaan niin käsky cut on pahvia.
Samantapaisia on loputtomasti. Ja yleensä ne toimivat matriisienkin kanssa joten niitä voi soveltaa loopittomiin ratkaisuihin - jolloin jo nopeampi nopeutuu lisää. Tosin nuo BASH:illa tehdyt kompastelevat enemmän kun awk ja sed.

***

BASH:in toiminta virhe- ja poikkeustilanteissa:

Mikäli skiptissä viitataan johonkin jota ei ole olemassakaan oletetaan että kutsutaan funktiota jota ei ole määritelty ja kutsutaan automaattisesti funktiota: command_not_found_handle. Koska tuossa funktiossa voidaan tehdä mitävaan skriptintekijä määrää niin sen muodostaminen on skriptintekijän vastuulla. Käytännössä skriptintekijä haukkuu BASH:in kivenrakoon kun ei tiedä tuota funktiota määritellä. Muiden poikkeustilanteiden käsittelemiseen pätee seuraava:

Tehdäänpä koneessa mitähyvänsä niin käyttöjärjestelmä kyllä havaitsee sen ja asettaa vastaavan signaalin. BASH käy jokaise käskyrivin jälkeen tutkimassa määriteltyjen 78:n signaalin tilan ja tekee mitä trap-käskyllä on määrätty tuossa tilanteessa tekemään. Esimerkiksi seuraavankaltaisia trap-komentoja yleensä tehdään:
Koodia: [Valitse]
trap 'echo virherivi:${LINENO[@]};echo ${FUNCNAME[@]} ; read' ERR
trap 'rc=$?; echo "virhekoodi $rc rivillä $LINENO-${FUNCNAME[0]}" ' ERR
trap ' (( $? )) && echo ${FUNCNAME[0]}" palautti virheen:"$?'  RETURN
trap "echo kutsupino: ${FUNCNAME[@]}" RETURN
trap pomppaaTänne SIGINT                    # mitä tehdään kun painetaan CTRL-c (nimi eikä numero)
trap 'echo "Control-C disabled."' 2
trap jälkiensiivous SIGTERM                 # kun skripti loppuu tai tekee suoritusaikaisen virheen
trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG # muuttujanarvon seuraaminen
set -x; trap "echo hit_return;read x" DEBUG                    # käskyrivien suoritus yksi kerrallaan
- trap:peja voidaan kirjoittaa funktioisakin vaikka ne yleensä määritellään skriptin alussa. Esimrkiksi kun virhettä käsitellään voidaan virhekäsittelyn ajaksi määrätä virhekäsittely pois päältä käskyllä: trap - ERR ja palauttaa se vanhassa muodossaan käsittelyn jälkeen - jottei näyttö sotkeutuisi kovin pahasti.

-samoin trapeissa saa olla funktiokutsujakin.

- jos skriptinsuoritus päätyy virheeseen niin tulostuva virheviesti on BASH:issa normaaliakin kehnompi - mutta ei se virheen tarkka ilmoittaminen paljoa auttaisikaan. Nimittäin skriptintekijälle riittää tieto siitä tapahtuuko virhettä vai ei - ja missä se tapahtuu: silloin virheen syyn pitäisi olla melkoselvä.

- BASH ei lopeta skriptinsuorittamista ihan pienistä virheistä vaikka se asettaakin virheen kohdalla muuttujan $? . Yritetään väittää että tällainen toimintatapa ei ole hyväksyttävää ja neuvotaan laittamaan skriptin alkuun: set -e. Mutta siitä on joskus hyötyä ja joskus haittaa.

BASH-skripteissä ei enää käytetä rakennetta: if..then vaan se korvataan rakenteella:
käsky1 && käsky2 -> käsky2 suoritetaan vain mikäli käsky1 onnistuu, tai:
käsky1 || käsky2 -> käsky2 suoritetaan vain mikäli käsky1 epäonnistuu, tai:
käsky2 && käsky2 || käsky3 -> käskyryhmää käytetään siellä missä ennen oli if..then..else .
- usein käsky1 on ehto, esimerkiksi: [[ -f tiedosto ]], mutta ei sen tarvitse olla. Jokatapauksessa käsky: "set -e" olettaa tuon ensimmäisen olevan ehto ja eikä välitä siitä.
- mieti kaksikertaa mihin || milloinkin viittaa, minkä kanssa se OR-funktion muodostaa.

- useimpia muitakaan tulkille annettavia ohjeita ei kannata antaa muuten kuin virhettä metsästettäessä.

***

Skriptin debuggaamisessa ei ole mieltä - kuvaannollisesti huonompi yrittää korjata parempaansa. Hermotkin kiittää jos heität huonostitoimivan skriptin roskikseen ja teet uuden. Vaikka tuleehan siihen debuggaamiseen joskus syyllistyttyä.

Muutenkin on suositeltavaa välillä "hukata" joku skripti ja tehdä se sitten uudestaan sillä yleensä siitä tulee paljon parempi.

Skriptissä kaikkien käytettävien nimien tulee olla valittu niin ettei kommentointia enää juurikaan tarvita. Nimittäin kommentoidessasi skriptiä tajuat että jos se toimisi toisella tavalla nopeutuisi se paljon. Teet siis uuden version ja kommentointi alkaa alusta ja ennenkuin kommentointi on tehty niin taas huomaat että uusi olisi parempi ja ... Ja koska asiallinen kommentointi vie aikaa enemmän kuin koodaaminen niin hiukka idioottimaista se kommentointi on.

Maailmalla monet skriptit on suurella vaivalla kommentoitu pilalle. Sikäli pilalle että koodia ei ole helppo ymmärtää kun pääosa siitä on kommentointia. Kestää nimittäin kauan ennenkuin ymmärrät toisen kommentointia sillä toiselle ilmanmuuta selvä on toiselle vaikeaa joten sitä ei selitetä tarpeeksi - ja toisaalta toiselle vaikea on toiselle itsestäänselvää joten siitä läärätään turhaan ja loputtomasti.

Ihminen tajuaakin koodin sitä hitaammin mitä useammalla rivillä se on esitetty - ja lisää hitautta tulee jos koodi pursuaa näytön toiselle sivulle - varsinkin raamatun kokoinen skripti on vaikea ymmärtää vaikka siinä vain osoitettaisiin että 1+1=2 vielä tänäänkin. Paras esimerkki on jo vanhentunut käsky if..then..else jota ei pitäisi enää käyttää. Koska sitä kuitenkin käytetään niin minkätakia se täytyy jakaa monelle riville kun sen voi kirjoittaa yhdellekin?

***

Nopein keino skriptin nopeuttamiseksi on poistaa siitä tarpeettomia osia - usein poistaminen johtaa huomattavaan nopeutukseen. Sen havaitseminen mikä on tarpeetonta on valitettavasti kokeellista hommaa ja jotta se onnistuisi järjellisessä ajassa tarvitaan työkaluja - aivankuin niitä työkaluja tarvitaan kaikissa kielissä ja ne ovatkin lähes kaikissa kielissä jo perusversiossa - mutta BASH:ista ne on poistettu ja joudut itse luomaan ne uudestaan.

Samalla BASH:ista on poistunut paljon muutakin. Esimerkiksi BASH on pakotettu käyttämääm merkintätapoja joita ihminen ei kykene käyttämään tehokkaasti. Sama merkintätapojen kummallisuus pätee lopulta kaikkiin kieliin mutta niissä inhottavia merkintöjä ei tarvitse käyttää sillä ne on kätketty kirjastoihin.

Mutta paras keino skriptin nopeuttamiseksi on kirjoittaa skripti kokonaan uudestaan käyttäen parempaa toimintaperiaatetta jolloin nopeutta tulee usein paljon lisää, tulee vakautta ja vaikka mitä - kymmenenkertaa nopeampi on arkipäivää. Kannattaako se on toinen asia sillä BASH on sittenkin niin hidas ettei siitä isompaan käyttöön ole - tai hitaaksi BASH:ia ainakin monet väittävät.

Uudet menetelmät tuovat skripteihin uusia ominaisuuksia - esimerkiksi tässä esitetty muistikuva-menetelmä nopeuttaa oleellisesti useimpia isoja tehtäviä. Jos samalla hyödynnetään kuvattua uutta ja yksinkertaista menetelmää yhden tai useamman erityyppisen parametrin palauttamiseksi funktiosta päädytään johonkin aivan uuteen.

***

- koska tuo parametrien palauttaminen on niin perustavalaatuinen asia - ja koska näin loppuunajettuna en kykene asioita loogisesti esittämään niin tässä parametrien palauttaminen on esitetty niin ettei sitä voi ymmärtää väärin vaikka kuinka tahtoisi :
Koodia: [Valitse]
function pienlukujenkertoma () { let $2=$(echo $(($(seq -s* $1)))) ;}

pienlukujenkertoma 6 kuuskertoma
pienlukujenkertoma 20 kakskytkertoma

echo "6  kertoma: "$kuuskertoma
echo "20 kertoma: "$kakskytkertoma

- parametrien palauttamiseen on muitakin keinoja: esimerkiksi kutsurakenne $( ... ) - elikä palautetaan haluttu arvo näytön kautta; toiminta on erittäin nopeaa eikä jätä näyttöön mitään. Sillä on se puute ettei se toimi oikein kun palautettavaa on montariviä.
- tai peräti niin että funktio kirjoittaa kovalevylle ja kutsuja käy sitten lukemassa kovalevyltä. BASH:in hitauden huomioonottaen tämä on ihan käyttökelpoinen keino.
- nämä kaksi keinoa toimivat silloinkin kun funktiokutsu on muotoa: funktionimi () ( ... ) - siis kaarisulut aaltosulkujen tilalla. Kutsu on hitaampi koska se muodostaa ensin oman prosessinsa - mutta lähes kaikki on sitten ihan omaa joten mikään funktiossa tehty ei näy pääohjelmassa. Paitsi levyoperaatiot.

***

Ikuista ratkutusta:
Missään kielessä ei voi tehdä mitään merkittävää ilman kirjastoja. Olisi mielekästä alkaa uudestaan käyttää BASH:iin  kirjastoja sillä BASH:ista ei tulla pääsemään eroon vaikka halu onkin hirveä - ja kirjastoilla varustettuna se olisi ihan kelvollinen kieli - ja alkaa hiljokseen tuntua ettei se ole mahdottoman hidaskaan mikäli käskee oikein. Kuitenkin BASH:in käskyjä olisi syytä parantaa - ilmeisesti se olisi BASH:in kehittäjille helppoakin, mutta nähtävästi rintakarvoja puuttuu - en minäkään kyllä uskaltaisi käskyjä parannella.

***

Käsky sleep toimii millisekunnista ylöspäin eikä se ole kovin tarkkakaan. Tarkemman viiveen saat käskyllä: read -t .xxxxx  - muuttujaa ei tarvitse määrätä. Aika on sekunneissa joten toimitaan 10:stä mikrosekunnista ylöspäin. Pienillä viiveillä toimimista varten tein sitä testaavan skriptin - sadasta mikrosekunnista yhdeksään millisekuntiin:

Skriptissä on kaksi prosessia pyörimässä yhtäaikaisesti. Ensin omia aikojaan tiedoston lukuarvoa tasaisesti kasvattavava prosessi joka työnnetää taustalle käskyllä: & .

Ja sitten normaalina edusta-ajona toinen nopeampi looppi ottamaan tiedostosta ensin näyte, sitten suoritetaan tasaisesti kasvava viive jonka perään otetaan toinen näyte ja tulos on näyte2-näyte1. Tuloksien tulisi kasvaa tasaisesti, mutta koska BASH on ajallisesti horjahteleva niin eihän näin aina ole. Yleensä tämä ei edes tulosta kaikkia määrättyjä; esimerkiksi jos ensimmäinen prosessi kirjoittaa juuri kun toinen haluaa lukea niin BASH luovuttaa.
 
Suuntaus on kuitenkin selvä joten kyllä pienetkin viiveet toimivat ainakin suurinpiirtein - ehkäpä tarkastikin. Joten vaikka tällainen viive on tällähetkellä tarkoitukseton niin on mukava tietää että tulevaisuudessa tällainenkin viive on käytettävissä. 
Koodia: [Valitse]
#!/bin/bash
for (( n=0; n<=99999999; n++ )); do echo $n > file1; done & # tämän loopin pitää olla hitaampi

apu1=0; apu2=0; for m in {0001..0090}; do read -t 0.$m; apu1=$apu2;apu2=$(cat file1); [[ $apu1 && $apu2 ]] && echo -n $(($apu2-$apu1))' ' ; done
kill $! # tämä tappaa alussa määrätyn taustaprosessin - siis tuon jonka perässä on merkki &
rm -f file1

***

Kevennystä:
Voi sen sinin laskea näinkin:
Koodia: [Valitse]
b='$(echo "s(4*a(1)/180*$a)" | bc -l)'; a=30; eval echo $b
- siis muuttujaan laitetaan se kaava. Tekstijono se on kaavakin.

***

Yhdessä asiassa mistään kielestä ei ole BASH:in kilpailijaksi: yhdenrivin skriptit. Kooltaan ne ovat yleensä alle rivinmittaisia, mutta monet ovat kyllä jopa kolmenrivin mittaisia - mutta aina yksi kokonaisus.

Niitä käytetään usein sellaisenaan sillä päätteessä on tavallaan jo rivi: !#/bin/bash. Eikä noille yhdennrivin skripteille haeta koskaan suoritusoikeutta. Mikäli käytettävissä on kunnollinen varasto yhdenrivinskripejä ja niitä osaa käyttää tuleekin käyttäjästä legendaarinen.

Mutta noita yksirivisiä täytyy olla tuhansia ja ne täytyy olla järjestetty niin että löytää tarvitsemansa. Ja hyvin harvasta on siihen sillä kaikki me hallitsemme epäjärjestyksen.

Näyttää lisäksi siltä ettei ole mitään rajaa sille mitä yksirivisellä voi tehdä. Ja ne ovat hämmästyttävän nopeita.

Mutta on yksirivisillä varjopuolensakin: esimerkiksi ne toimivat usein vain jossakin tietyssä distrossa ja joskus ne tarvitsevat sellaisia järjestelmäasetuksia joita harvassa koneessa on.

***

Aikaisemmin käsityksenä oli että vaikka BASH:illa saakin nopeasti nopeasti tehtyä skriptin mihin vaan, niin siitä selviää tutkittavasta vain senverran että voi päättää kannattaako muilla kielillä tehdä kunnollinen ohjelma. Mutta usein BASH-skripti kelpaa kyllä lopulliseksikin versioksi. Esimerkiksi seuraava joka kelpaa pohjaksi monenlaiseen:

Graafisessa xy-esityksessä akseli voidaan esittää joko lineaarisena tai logaritmisena. Esitettäessä jotakin x-akselin ollessa logaritminen saadaan vähimmillä ponnistuksilla sujuva käyrä jos sen esityspisteet sijaitsevat x-akselilla tasavälein.

Koska pisteiden luomisessa tarvittava laskukaava on BASH:issa mahdoton muistaa tein siitä funktion: tämänjälkeen kun tarvitsee tasavälisiä esityspisteitä ei enää tarvitse laskea niitä vaan voi kutsua funktiota tekemään sen.

Mutta logaritmialueella pisteiden muodostamisessa tuli eteen periaatteellinen ongelma: epäilin että peräkkäisten pisteiden suhde on vakio eikä millääntavalla logaritminen. Tämä täytyi varmistaa tekemällä käyrä jossa peräkkäisten x-akselin pisteiden suhde on vakio - ja mikäli väite pitää paikkansa sijaitsevat pisteet tasavälein koko x-akselilla dekadien vahtuessakin - ja lisäksi exponentiaalisen käyrän kuvaaja on suora y-akselin ollessa lineaarinen:
Koodia: [Valitse]
#!/bin/bash
function luoaskellus () { # lineaarisen  asteikon kutsu: luoaskellus alku loppu askel
                         # logaritmisen asteikon kutsu: luoaskellus alku loppu *suhde 
                        # negatiivisilla arvoilla liikutaan imaginääriluvuissa. Siis nollaa ei saa ylittää
unset $1 # varmistus että matriisissa kaikki on tässä funktiossa muodostettua
[[ $(printf '%d' "'${4:0:1}'") != 42 ]] && apu=$(seq $2 $4 $3 | tr '\n' ' ' | tr , . ) || { echo "for (i = $2 ; i < $3; i *= ${4:1})  i" | bc -l > /tmp/delme; apu=$( cat /tmp/delme | tr '\n' ' ') ;}
apu2=($apu)                                # matriisi x-akselin pisteistä   
readarray $1 < <(printf "%s\n" ${apu2[@]})
let $5=${#apu2[@]} ;}                      # pisteiden lukumäärä   
 
function plotlogx () {
# tämä funktio tulostaa ainoastaan tiedostoon: ~/koe.eps eikä näytölle tule mitääm. Tiedosto avautuu tiedostoselaimella
gnuplot -p -e 'set terminal postscript eps color enhanced; set logscale x ;set xlabel "x"; set ylabel "logaritmi"; set output "~/koe.eps"; plot "~/koe"' ;}

luoaskellus askellus .000001 1000000 *1.5 askelluku
rm -f ~/koe; for (( n=0; n<=$askelluku-1; n++ )); do echo -n ${askellus[n]}' ' >> ~/koe; echo $(echo "l(${askellus[n]})") | bc -l >> ~/koe; done

plotlogx
« Viimeksi muokattu: 27.08.19 - klo:06.31 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #253 : 17.08.19 - klo:11.09 »
Muuttujien vertaaminen ei onnistu matemaattisella vertailulla:
Koodia: [Valitse]
a=2; b=1; (( $1 > $2 )) && echo 'eka on suurempi' # eikä mikään vaihtoehto onnistu tuolle > merkille.
# se onnistuu kylläkin kun kirjoittaa:
(( 2 > 1 )) && echo 'kaksi on suurempi kuin yksi'
Mutta BASH kykenee vertailemaan kokonaisluku- tai desimaalimuuttujiakin ihan peruskäskyillään, ensiksi yhtäsuuruus:
Koodia: [Valitse]
a=2.1; b=2.1; ((${a//./} ^ ${b//./})) || echo yhtäsuuria
# mutta: a=21.1; b=2.11; ((${a//./} ^ ${b//./})) || echo yhtäsuuria  # menee väärin. Korjaus tähän:
a=21.1; b=2.11; ((${a%.*} ^ ${b%.*})) || ((${a//./} ^ ${b//./})) || echo yhtäsuuria
seuraava vääriä tuloksia aiheuttava on .... ja sen korjaus .... Kyllä BASH kykenee kaikkiin korjauksiin mutta kaavat tulevat jatkuvasti pitemmiksi ja mahdottomammiksi muistaa ja loogiset operaatiot saattavat skriptintekijän sekaisin. Nopeus ei laske paljoakaan

***

Sitten varsinainen vertailu. Siitä kannattaa tehdä funktio. Ei se super-nopea ole, mutta eipä kuhnailekaan. Eiköhän siinä ole vielä parsimista, mutta tämmöinen se nyt on:
Koodia: [Valitse]
function onkoekasuurempi () { ((0${1%.*} ^ 0${2%.*})) || (( ${1//./} < ${2//./} )) && echo 0 || echo 1 ;}
#                                                                    ^
#                                            vertailumerkki on tässa | ja sitä voi muuttaa mieleisekseen
# ja sen kutsuesimerkki:
onkoekasuurempi -0.127 -0.126 
Muuten nopeudesta: BASH:in peruskometoja saa olla pitkä rivi ennenkuin  kokonaisus kestää edes yhtäkauan kuin paraskaan ulkoinen ohjelma. Esimerkiksi:
Koodia: [Valitse]
a=-1.01;b=-1.02; echo -e $a'\n'$b | sort -n | tail -1
on hidas ja toimiikin väärin kuten näet kun muutat miinukset plussaksi: plussapuolen suuremman iitseisarvon pitää nimittäin olla negatiivisen puolen pienemmän itseisarvo. Ja tuo pitkä hiviö on montakertaa nopeampikin.

Funktiokutsu vie hyvin vähän aikaa. Funktiokutsuun kuluvan ajan korvaa ylimäärin se että se tekee skriptin rakenteesta selväpiirteisen ja ongelmattoman. Funktiot on syytä tallettaa kirjastoon. Sillä nuo merkinnät ovat tosiaankin niin kummallisia että ellei niitä saa kopioitua jostakin niin käyttämättä jää.
Ja kirjaston toinen nimi on automaattikopioija.

Tämä johtaa aiheseen nimeltään desimaalimatematiikka jota BASH kyllä hallitsee myös. "salamannopeasti" lisäksi; se jää kyllä nähtäväksi pystynkö minä tekemään kaavat sille.

- kaavat ovat tosiaan kauheita vaikka ne suoritetaankin nopeasti. Ja salahautoja olisi runsaasti ja niiden kiertäminen hidasta. Pääsisihän sillä muutamaan millisekuntiin, mutta koska bc toimii ongelmitta jo viidessä milliskunnissa niin toistaiseksi viisainta on sittenkin käyttää seuraavantyylistä:
Koodia: [Valitse]
bc <<< "1.000001+5.27"

***

Vaikka pääohjelmassa ei voi kirjoittaa: (( $1 > $2 )) && ... niin funktiossa se on sallittu:
Koodia: [Valitse]
function max () { (( $1 > $2 )) && echo $1 || echo $2 ;}
 
# ja funktion kutsuesimerkki:
max 5 9
« Viimeksi muokattu: 20.08.19 - klo:06.43 kirjoittanut petteriIII »

nm

  • Käyttäjä
  • Viestejä: 16428
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #254 : 20.08.19 - klo:11.33 »
Muuttujien vertaaminen ei onnistu matemaattisella vertailulla:
Koodia: [Valitse]
a=2; b=1; (( $1 > $2 )) && echo 'eka on suurempi' # eikä mikään vaihtoehto onnistu tuolle > merkille.

Tuossa esimerkissä ei ole määritelty positionaalisten argumenttien 1 ja 2 arvoja, vaan muuttujat a ja b, joten vertailussa on käytettävä niitä:

Koodia: [Valitse]
a=2; b=1; (( a > b )) && echo 'eka on suurempi'

a=2.1; b=2.1; ((${a//./} ^ ${b//./})) || echo yhtäsuuria
# mutta: a=21.1; b=2.11; ((${a//./} ^ ${b//./})) || echo yhtäsuuria  # menee väärin. Korjaus tähän:
a=21.1; b=2.11; ((${a%.*} ^ ${b%.*})) || ((${a//./} ^ ${b//./})) || echo yhtäsuuria
[/code]

Tämä menee metsään ainakin siinä, että vertailuun päätyy oktaalilukuja, jos kokonaislukuosa on 0:

Koodia: [Valitse]
a=0.7; b=0.8; ((${a%.*} ^ ${b%.*})) || ((${a//./} ^ ${b//./})) || echo yhtäsuuria
Koodia: [Valitse]
-bash: �L: 07 ^ 08: liian iso luku lukujärjestelmälle (virheellinen avainsana on ”08”)
yhtäsuuria

En kyllä itse lähtisi toteuttamaan tukea desimaaliluvuille tällä tavalla. Mahdollisia sudenkuoppia on joka mutkan takana.

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #255 : 22.08.19 - klo:18.41 »
Kaikenlaiseen tulee näköjään syyllistyttyä. Mutta en hölmöile ensikertaa joten on helppoa tunnustaa.

Minunmielestäni BASH:in viehätys onkin siinä että kaikessa on miljoona mahdollisuutta virheille - mutta virhetilanteita voi estää muodostumasta. Kaikkia mahdollisia virhepaikkoja ei ole mahdollista huomata mutta minkähyvänsä huomaamansa virhepaikan voi välttää.

Desimaalilukujen vertaaminen on sinälläänkin mukava apu nopeutensa tähden mutta sen varsinainen merkitys on siinä että se avaa oven nopeaan desimaalilaskentaan.

Matematiikkaohjelmat kuten bc viettävät vertailussa aikaa viisikertaa enemmän, mutta sen korvaukseksi niilä monia erikosiominaisuuksia vertailun tueksi. Joten monimutkaiset vertailut täytyy edelleen suorittaa  bc:llä.

Vertailufunktio toimii millisekunnissa vaikka onkin kooltaan hirviö. Funktion pituus ei vaikuta nopeuteen paljoakaan sillä koska käskyjä on vain yksi rivi niin se tulkataan kokonaisuudessan C-kieliseksi ohjelmaksi ja senpäälle aikaa kuluttaa vain yksi funktiokutsu. Tämmöinen vertailufunktio on nyt - mutta sitä täytyy korjata vielä montakertaa: 
Koodia: [Valitse]

function max () { a=${1//+/}; b=${2//+/}; b=${b//-./-0.}; a=${a//-./-0.}; [[ ${a%.*} == $a ]] && a=$a'.0';[[ $b{%.*} == $b ]] && b=$b'.0'; [[ ${a%.*} == '' ]] && a='0'$a; [[ ${b%.*} == '' ]] && b='0'$b; ((0${a%.*} == 0${b%.*})) && { ((${a//[^-]/}1${a##*.}${b##*.} > ${b//[^-]/}1${b##*.}${a##*.})) && echo $a || echo $b ;} || { ((${a%.*} >= ${b%.*})) && echo $a || echo $b ;} ;}

# kutsuesimerkkejä vaikeista vertailuista (muuten kun kummankin luvun etumerkin vaihtaa niin valitunkin pitää vaihtua):
max 2222222222222222.000000000000000001 2222222222222222.000000000000000002
# max .08 +.079
# max -.08 +.079
# max +.99 1

- vertailu tehdään bc:llä yksinkertaisimmillaan näin: bc <<< "$a > $b" joka tulostaa totuusarvon 0 tai 1.
- tekstijonona vertaaminen onnistuu sekin ihan hyvin. Sekin on nopea, hyväksyy desimaalit ja rajattomasti numeroita, ei välitä vaikka desimaalipiste olisikin pilkku eikä ole aina kovin kranttu vieraiden merkkienkään suhteen. Mutta se ei toimi oikein etumerkkien suhteen, ei ole niin nuuka missä se desimaalipiste on ja voi höpertää muutenkin, joten täytyy miettiä kaksikertaa missä sitä käyttää. Siis tarkoitan tämäntyyppisiä vertailuja:
[[ 2.19mW < 2.20mW ]] && echo 'eka on pienempi'

***

Desimaalilaskenta. Ensin yhteelasku, ja alkuunsa vain jotenkin toimiva periaate:
Koodia: [Valitse]
luku1=1.2; kokonaisosa1=${luku1%%.*}; desimaaliosa1=${luku1##*.}
luku2=3.4; kokonaisosa2=${luku2%%.*}; desimaaliosa2=${luku2##*.}

kokonaisosiensumma=$(($kokonaisosa1+$kokonaisosa2))
desimaaliosiensumma=$(($desimaaliosa1+$desimaaliosa2))

echo $kokonaisosiensumma.$desimaaliosiensumma

Mutta desimaalien yhteenlaskemisessa on sekin ongelma että kun yhteenlaskettavien desimaalien määrä on erilainen niin yhteenlaskettaessa tulee melkoinen virhe mikäli desimaalilukumäärät eroavat. Sen korjaukseksi pikkuisen huonolla mutta yksinkertaisella tavalla voi lisätä kummankin desimaaliluvun loppuun niin monta nollaa kuin toisessa on merkkejä sillä silloin ne ovat yhtäpitkiä:
Koodia: [Valitse]
desimaaliosa1pituus=$(echo ${#desimaaliosa1}); desimaaliosa1=$desimaaliosa1$(printf "%0"$desimaaliosa2pituus"d")
desimaaliosa2pituus=$(echo ${#desimaaliosa2}); desimaaliosa2=$desimaaliosa2$(printf "%0"$desimaaliosa1pituus"d")

Seuraava korjattava on desimaaliosan ylivuoto. Ylivuoto on otettu huomioon jo perusasetelmassa, siinähän jo alkujaan on kummankin desimaaliosan alussa suojanumerona 1 joten ylivuotonumerona on 2. Siis kun tapahtuu todellista ylivuotoa niin ylivuotonumeroksi tulee 3. Muuta ei tarvitse tehdä kuin lisätä desimaaliosan ylin numero kokonaisosan lopputulokseen ja vähentää 2. Siis viimeinen lause muutetaan muotoon:
Koodia: [Valitse]
echo $(($kokonaisosiensumma+${desimaaliosiensumma:0:1}-2))."${desimaaliosiensumma:1}"

***

"BASH ei tunne desimaalilukuja ja niitä ei voi käyttää matemaattisissa tehtävissä - niitä ei voi laskea yhteen, kertoa eikä mitään muutakaan matemaattista. Ne ovat vain tekstijonoja vailla matemaattista merkitystä". Se siitä pilkunviilaamisesta sillä myös BASH:issa on käsitelty desimaalilukuja aina - eikun tekstijonoja.

Tein tähän desimaalimatematiikkaan parikin erilaista skriptiä yhteenlaskusta - muuta ei niitä kannata esitää sillä niillä on vielä paljon rajoitteita vaikka ne toimivatkin muuten hyvin. Nuo rajoitteet voisi kiertää paremmalla koodilla, mutta ei taida kannattaa - ei BASH:in matematiikasta hyvää saa tekemälläkään - luultavasti. Mutta loppupäätelmä totisesti kannattaa esittää: tämä on kokonaisuudessaan aluetta joka on tarkkaan suunniteltu BASH:in alkuperäisten kehittäjien toimesta. Siis seuraan joidenkuiden jalanjäljillä ja näitä BASH:in desimaalilaskimia on ollut jo aikoinaan - miksi niistä ei ole kerrottu en käsitä. Vai onko niistä kerrottu mutta on miellyttävämpää kirjoittaa BASH-pitfail:eista?

***

Taas kertaalleen ällistyin sitä kuinka BASH:illa voidaan ratkaista mikähyvänsä ongelma monin eritavoin huolimatta väitteistä ettei onnistu ollenkaan. Esimerkiksi desimaalilukujen vertailussa edellinen nopeuskuningashan oli:
Koodia: [Valitse]
function max () { a=${1//+/}; b=${2//+/}; b=${b//-./-0.}; a=${a//-./-0.}; [[ ${a%.*} == $a ]] && a=$a'.0';[[ $b{%.*} == $b ]] && b=$b'.0'; [[ ${a%.*} == '' ]] && a='0'$a; [[ ${b%.*} == '' ]] && b='0'$b; ((0${a%.*} == 0${b%.*})) && { ((${a//[^-]/}1${a##*.}${b##*.} > ${b//[^-]/}1${b##*.}${a##*.})) && echo $a || echo $b ;} || { ((${a%.*} >= ${b%.*})) && echo $a || echo $b ;} ;}

Sille löytyi koodiltaan ja toimintaperiaatteeltaan erilainen voitaja joka toimii vielä nopeammin ja sallii melkolailla rajoittamattoman desimaalimäärän vertailtaviin lukuihin:
Koodia: [Valitse]
function max () { [[ ${1//[^-]/} ]] && m1=-1 || m1=1; [[ ${2//[^-]/} ]] && m2=-1 || m2=1; [[ ${1//./} == $1 ]] && a=$1".0" || a=$1; [[ ${2//./} == $2 ]] && b=$2".0" || b=$2; a=${a//+/}; b=${b//+/}; b=${b//-./-0.}; a=${a//-./-0.}; ((10#${a%.*} == 10#${b%.*})) && { (($m1*10#${a#*.} >=  $m2*10#${b#*.})) && echo $1 || echo $2 ;} || { ((10#${1%.*} > 10#${2%.*})) && echo $1 || echo $2 ;} ;};
# ja esimerkkikutsu:
max -1111111111111111111111111111111111111111.000000000000000000000000000000002 -1111111111111111111111111111111111111111.000000000000000000000000000000003

- ihan varmasti on edelleen tilanteita joissa funktio toimii väärin sillä koska BASH on opetuskieli on siinä erittäin runsaasti kompastuskiviä - ja semmoinenkin ilmiö on että jos suorittaa korjauksia väärässä järjestyksessä tulevat ne vanhat virheet pahempina takaisin joten loppuelämä kuluu rattoisasti testatessa alusta alkaen jokaisen korjauksen jälkeen.

- samoin on ihan varmaa että parempikin ratkaisu löytyy.

***

Desimaalilaskenta aivan uudestaan - ei ne vanhat väärin ole, kyllä niissä ajatuksentynkää on.

"BASH ei tunne desimaalilukuja ja niitä ei voi käyttää matemaattisissa tehtävissä - niitä ei voi laskea yhteen, kertoa eikä mitään muutakaan matemaattista. Ne ovat vain tekstijonoja vailla matemaattista merkitystä". Tarkoituksena on luoda mielikuva etä BASH ei pysty desimaalilaskentaan, mutta niin ne eivät sanoneet sillä tiesivät itsekin että kyllä pystyy - ja joissain tilanteissa säällisen nopeastikin. Hyvin harvoin desimaalilaskentaa kannattaa silti nykyisellään  käyttää ja saako siitä koskaan kelvollista yleisempään käyttöön on toinen asia - mutta kun esitetään asia niin ettei BASH kykene niin se on törkeää sillä senjälkeen ei edes yritetä. Nimittäin kaikessa on aina parantamisen varaa, ideoista se vain riippuu - ja teoriatiedoistakin.

Ilmanmuuta BASH:iin on aikoinaan tehty desimaalilaskennan funktioita - sillä jokaisessa funktioden tekemisen vaiheessa tuntuu siltä että nyt kuljetaan jonkun jalanjäljissä. Joskus tarvittavat funktiot ovat sangen yksinkertaisia mutta esimerkiksi jakolaskufunktion tekeminen vaatisi matematiikkaneroa - mutta eiköhän joku ole tehnyt jakolaskuunkin funktion. Kukahan on aktiivisesti "unohtanut" että desimaalilaskentakin toimii - ja uskotellut muillekin niin?

Siten käytäntö: koska käyttäjä ei voi määritellä + merkkiä uudestaan on yhteenlaskuun suorittamiseksi käytettävä funktiokutsua: add luku1 luku2  - ja mikäli skriptissään haluaa suorittaa yhteenlaskun niin skriptissä merkintä on tyypiltään: vastaus=$(add luku1 luku2)

Desimaaliyhteenlaskun funktio toimii periaatteessa tällätavoin: laskettaessa yhteen lukuja, esimerkiksi 222 ja 0.123 muokataan luvut siten, että niiden desimaaliosissa on yhtämonta numeroa: 
                         222.000
                           0.123
Tämänjälkeen lasketaan erikseen yhteen kokonais- ja desimaaliosat ja tulostetaan ne desimaalipisteen eroittamina peräkkäin. Desimaaliosia yhteenlaskettaessa etunollat poistettaisiin tulkin toimesta ellei sitä estettäisi: koska desimaaliosat ovat yhtäpitkät niiden molempien eteen lisätään suojanumeroksi 1 ja tulostettaessa jätetään tuo suojanumero pois joten "etunollat" säilyvät mutta noita suojanumeroita ei tulosteta. Desimaaliosan ylivuoto yhteenlaskussa otetaan huomioon siten että kokonaisosaan lisätään desimaaliosan ensimmäinen numero (=se suojanumero - ja ylivuoto kasvattaa sitä) ja vähennetään tuloksesta 2.
- sekä kokonais- että desimaaliosassa kummassakin saa olla korkeintaan 19 numeroa.
- mikäli lukualue ylittyy ei edes tule varoitusta.
- toimintanopeus jää noin 4:ään millisekuntiin - ehkä vähän paremmaksi kuin mitä matematiikkaohjelmat tarjoavat.
Koodia: [Valitse]
function add () {
# reset # testauksen aikana tarpeen mutta käytössä kommentoitava
# [[ ${1//[^-]/} ]] && m1=-1 || m1=1; [[ ${2//[^-]/} ]] && m2=-1 || m2=1 # vähennyslaskun alkulause
luku1=$1
luku2=$2
[[ ${luku1//./} == $luku1 ]] && luku1=$luku1".0" # on tarpeen että luvussa on yksi desimaalipiste
[[ ${luku2//./} == $luku2 ]] && luku2=$luku2".0" # joten lisätään sellaien mikäli ei jo ole
desimaaliluku1=${luku1##*.}
desimaaliluku2=${luku2##*.}
 
(( ${#desimaaliluku2} >= ${#desimaaliluku1} )) &&
{ apu=$desimaaliluku1"0000000000000000000"; desimaaliluku1=${apu:0:${#desimaaliluku2}} ;} || { apu=$desimaaliluku2"0000000000000000000"; desimaaliluku2=${apu:0:${#desimaaliluku1}} ;}
# echo a$desimaaliluku2' 'b$desimaaliluku1 ; read # testatessa tämä on tarpeen

kokonaisluku1=${luku1%%.*}
kokonaisluku2=${luku2%%.*}
desluku=$((1$desimaaliluku1+1$desimaaliluku2)) # 1 on se suojanumero
echo $((10#$kokonaisluku1+10#$kokonaisluku2+${desluku:0:1}-2)).${desluku:1} ;}
koe suurimmalla sallitulla numeromäärällä ja ylivuodolla:
add 1111111111111111111.511111111111111111 1000000000000000000.600000000000000001  # ja se tulostaa:
    2111111111111111112.111111111111111112

***

Päivitin positiivisten desimaalilukujen skriptin. Millisekunnin se enää kestää hakaten 1-4 matematiikkaohjelmia käyttävät laskut. Pitää kai sitten yrittää tehdä siitä versio joka hyväksyy negatiivisetkin luvut.

BASH:in sisäisten käskyjen suorittaminen on ihmeen nopeaa: iso koodiröykkiö vie aikaa vain millisekunnin.

Jatkuva testaaminen puuduttaa ja välillä täytyy ajatella jotakin muuta:

Teoria on että skripti suunnitellaan etukäteen niin hyvin ettei se virheile - sillä korjaamalla ei saa moitteetonta vaan se täytyy synnyttää virheettömänä. Mutta ei skriptiä noin tehdä - harhaluulo tulee siitä että kuvitellaan että skriptinteossa merkitsee jotakin mitä siinä BASH:in kelvottomassa virheviestissä lukee. Niitä ei lueta - se riittää viankorjaukseen että virhe ylipäätään tapahtuu.

En aikaisemmin käsittänytkään että itseasiassa BASH:in mollaajat kaivavat linuxille hautaa - ymmärtämättä että heitä on älytetty.

***

Tällainen tästä BASH:in yhteenlaskusta tuli. Onhan tämä neljäkertaa nopeampi kuin matematiikkaohjelmat, mutta myöntää täytyy että en jaksanut testailla ja silittää ryppyjä joten eihän tämä vielä luotettava ole. Ainoastaan positiivisten desimaalilukujen yhteenlasku on aikahyvin testattu. Mutta tekee naurettavaksi väitteet ettei BASH osaa. 

Tämä kelpaa kaikkiin yhteenlaskuihin - siis kummankin yhteenlaskettavan etumerkki voi olla + tai -. Siis siten tämä kelpaa vähennyslaskuihinkin. Lukualue on 19 numeroa sekä kokonais- että desimaaliosassa.
Koodia: [Valitse]
function add () {
# reset # testauksen aikana tarpeen mutta käytössä kommentoitava
[[ ${1//[^-]/} ]] && m1=- || m1=+; [[ ${2//[^-]/} ]] && m2=- || m2=+
luku1=$1
luku2=$2
luku1=${luku1//-./-0.}; luku2=${luku2//-./-0.}
[[ ${luku1//./} == $luku1 ]] && luku1=$luku1".0" # on tarpeen että luvussa on yksi desimaalipiste
[[ ${luku2//./} == $luku2 ]] && luku2=$luku2".0" # joten lisätään sellaien mikäli ei jo ole
desimaaliluku1=${luku1##*.}
desimaaliluku2=${luku2##*.}
(( ${#desimaaliluku2} >= ${#desimaaliluku1} )) &&
{ apu=$desimaaliluku1"0000000000000000000"; desimaaliluku1=${apu:0:${#desimaaliluku2}} ;} || { apu=$desimaaliluku2"0000000000000000000"; desimaaliluku2=${apu:0:${#desimaaliluku1}} ;}
#echo a$desimaaliluku2' 'b$desimaaliluku1 ; read # testatessa tämä on tarpeen
kokonaisluku1=${luku1%%.*}
kokonaisluku2=${luku2%%.*}
case $m1$m2 in
-- ) desluku=$((1$desimaaliluku1+1$desimaaliluku2)) && apu=$((10#$kokonaisluku1+10#$kokonaisluku2$m2${desluku:0:1}+2)); (( $apu )) && echo $apu.${desluku:1} || echo -0.${desluku:1} ;;
++ ) desluku=$((1$desimaaliluku1+1$desimaaliluku2)); echo $((10#$kokonaisluku1+10#$kokonaisluku2+${desluku:0:1}-2)).${desluku:1} ;;
+- ) desluku=$((1$desimaaliluku1-1$desimaaliluku2)) && apu=$((10#$kokonaisluku1+10#$kokonaisluku2$m2${desluku:0:1})); (( $apu )) && echo $apu.${desluku:1} || echo -0.${desluku:1} ;;
-+ ) desluku=$((1$desimaaliluku1-1$desimaaliluku2)); echo $((10#$kokonaisluku1+10#$kokonaisluku2+${desluku:0:1})).${desluku:1} ;;
esac
}


Kokeita:
add 1111111111111111111.511111111111111111 1000000000000000000.600000000000000001  # ja se tulostaa:
    2111111111111111112.111111111111111112

add 08.08 08.08

***

Desimaalilukujen kertominen suoritetaan BASH:issa niin että desimaalipisteet poistetaan ja kerrotaan muodostunet kokonaisluvut. Syntyvässä numerosarjassa on oikeat numerot ja mikäli ratkaisee ongelman mihin desimaalipiste sijoitetaan niin tulos on oikea. Koodina tämä on:
Koodia: [Valitse]
function multiply () {
luku1=$1
luku2=$2
[[ $luku1 == 0 || $luku2 == 0 ]] && echo 0 && return
[[ ${luku1//[^.]/} ]] || luku1=$luku1".0"
[[ ${luku2//[^.]/} ]] || luku2=$luku2".0"
desimaaliosa1=${luku1##*.}
desimaaliosa1pituus=${#desimaaliosa1}
desimaaliosa2=${luku2##*.}
desimaaliosa2pituus=${#desimaaliosa2}
desimaalejakaikkiaan=$(($desimaaliosa1pituus+$desimaaliosa2pituus ))
tulo=$((${luku1//./}*${luku2//./}))
echo ${tulo:0: -$desimaalejakaikkiaan}.${tulo: -$desimaalejakaikkiaan} ;}

Vertaa itse:
time multiply 9.65438761 -.3723476896
time bc <<< "scale=18;9.65438761*-.3723476896"
- mutta toisaalta bc:ssä merkkiluku on rajoittamaton ja siihen saa helposti mukaan myös sinit, logaritmit ja muut.
- desimaalien maksimimäärä on tässä BASH-toteutuksessa pieni. Se korjaantuu tulevaisuudessa kaksoistarkkudella. Tai nelois ...
« Viimeksi muokattu: 06.09.19 - klo:06.28 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #256 : 07.09.19 - klo:14.38 »
BASH tuntee ainoastaan kokonaisluvut ja niissäkin luvuissa saa olla korkeintaan noin 19 numeroa. Tuo numeromäärä ei riitä mihinkään, mutta BASH sunniteltiinkin käyttämään matematiikkaongelmissaan kunnon matematiikkaohjelmia.

Mutta yksinkertaisissa matematikkaongelmissa BASH on silti nopeampi kuin nuo matematiikkaohjelmat. Ihan ensimmäiseksi rajoitteeksi tulee pieni numeromäärä ja aluksi tehdään kaksinkertaisen numeromäärän summain kaikenmerkkisille kokonaislvuille joten tätä voi käyttää vähennyslskussakin. Toiminta-aika on luokka 1ms.
             
function addd () { # kokonaislukujen summain kaksinkertaisella numeromäärällä
luku1pituus=${#1}
luku2pituus=${#2}
luku1=00000000000000000000000000000000000000$1
luku2=00000000000000000000000000000000000000$2
luku1=${luku1: -36}
luku2=${luku2: -36}
# echo $luku1
# echo $luku2
luku11=${luku1:0:18}
luku12=${luku1:18}; luku12=${luku11//[^-]/}$luku12
luku21=${luku2:0:18}
luku22=${luku2:18}; luku22=${luku21//[^-]/}$luku22
# echo a$luku11' 'b$luku12
# echo c$luku21' 'd$luku22
tokaluku=$((10#$luku12+10#$luku22))  # Kun käy niin että tokaluku vuotaa yli lisätään ekalukuun 1 ja #leikataan tokaluvusta ensimmäinen merkki pois (se on se ylivuotonumero ja sen arvo on silloin 1)
((10#${#tokaluku} > 10#${#luku12} || 10#${#tokaluku} > 10#${#luku22} )) && ylivuoto=1 || ylivuoto=0
(( $ylivuoto )) && tokaluku=${tokaluku:1}
# echo $ylivuoto
[[ ${luku11//[^-]/} == - && ${luku21//[^-]/} == - ]] && ekaluku=$((10#$luku11+10#$luku21-$ylivuoto)) ||ekaluku=$((10#$luku11+10#$luku21+$ylivuoto))
(( $ekaluku )) && echo $ekaluku$tokaluku || echo $tokaluku ;}

tarkistus:
addd 55555555555555555555555555555555555 55555555555555555555555555555555555
pitää tulla: 111111111111111111111111111111111110

tai:
addd 55555555555555555555555555555555555 -55555555555555555555555555555555555
pitää tulla: 0

tai:
addd -55555555555555555555555555555555555 -55555555555555555555555555555555555
pitää tulla: -111111111111111111111111111111111110

tai:
addd 55555555555555555555555555555555555 555
pitää tulla: 55555555555555555555555555555556110

tai:
addd 555 555
pitää tulla: 1110

tämän kaksoistarkkuuden saisi desimaalilaskuihinkin. Niinkuin kertolaskuunkin. Ja voisi siirtyä 3:nkertaiseen numeromäärään ja siitä ylöspäin. Ja skriptit toimisivat edelleen kohtuullisen nopeasti. Ja tämä olisi toiminut jo silloin kun asialla oli jotain merkitystä käytännössäkin.

aivan varmasti skriptissä on vielä korjattavaa - niinkuin missähyvänsä skriptissä.

***

Jakolasku kokonaisluvuille. Tulos voi kuitenkin olla desimaaliluku ja se esitetään 16:lla desimaalilla.

Taas kertaalleen tämän tekemisessä vaivasi "ajatukset jämähdyttävä väärä luulo" - ja kuvittelin kauan että BASH:ille saa vain matematiikkanero tehtyä skriptin jakolaskuun - ja sekin on tuhatrivinen ja hidas - enkä siis uskaltanut yrittääkään. Mutta itseasiassa skriptinteossa ei nokka kauaa tuhissut.

Tämä jakolasku ei toimi oikein kaikissa tilanteissa mutta on tosiyksinkertainen, desimaaleja voi olla paljon ja lisäksi skripti on helppo ymmärtää. Desimaalijakolaskussa virhetoiminta poistetaan, mutta sen skripti on senkintakia paljon sotkuisempi.
Koodia: [Valitse]
function divide () {
luku1=$1
luku2=$2
kokonaiset=$((10#$1/10#$2)); jakojaannos="$((10#$1%10#$2))"
desimaalit="$(((1000000000000000*10#$jakojaannos)/10#$2))"
echo $kokonaiset.$desimaalit ;}
tarkistus:
divide 9000000000000001 3
Pitää tulla: 3000000000000000.33333333333333333

***

Jakolasku desimaaliluvuille. Kokeile, tämä on virtuooseille niin nyöryyttävää että tekevät parhaansa tämän torpeedoimiseksi.

Tämä täydentää desimaalilaskut sillä yhteenlaskua voi käyttää myös vähentämiseen. Kaikkien desimaalilaskujen suoritusnopeus on luokkaa 1ms kun matematiikkaohjelmia käytettäessä desimaalilaskut kestävät luokkaa 4ms - melkein koko 4ms menee siihen kun matematiikkaohjelmaa luetaan levyltä.

Desimaaliluvulla voi jakaa toisen desimaaliluvun kokonaisluvuille sopivalla menetelmällä kun poistaa luvuista desimaalipisteet - numerot tulevat silloin edelleen oikein.

Mutta kokonaisosa tulee heti aluksi oikein vain kun desimaaliosat ovat yhtäpitkät - siis jos ne eivät ole yhtäpitkät niin lyhyemmän desimaaliosan omaavan perään kirjoitetaan tarpeeksimonta nollaa senjälkeen kun on varmistettu että niissä molemmissa on desimaalipiste ainakin lopussa.
 
Tällä desimaaliosien yhtäpituuden varmistamisella on toinenkin syy: desimalit lasketaan muuten väärin.

Jokatapauksessa desimaaleja laskettaessa on vaikeutena se että etunollat katoavat eikä matematiikka kerro montako niitä etunollia on ollut. Mutta kun kirjoittaa desimaalien laskemisen jälkeen niiden eteen paljon nollia ja katkaisee muodostuneen desimaalijonon perästälukien siihen mittaan joka desimaaleille on varattu ovatkin etunollat näennäisesti tallella.

Sitten vain tulostetaan kokonaisosa <desimaalipiste> desimaaliosa

Skriptin loppuhionta on iäisyyskysymys: esimerkiksi desimaalien määrää saisi varmasti lisättyä ja oikeellisuuden tarkistaminen kestää vuositolkkua.

Tämä olisi toiminut aina - tässä ei ole mitään uutta tai keksinnöllistä - suuret virtuoosit ovat "unohtaneet" ratkaisun sillä itseasiassa tämä on noviisi-tasoa ja sopivien käskyjen löytyminen osoittaa että tämä on tehty jo. Mutta tämä kaipaisi kipeästi parantamista varsinkin desimaalimäärän suhteen - suuret ulkomaiden virtuoosit pystyisivät siihen ja toivottavasti nyt yrittävät päästä eroon narrinhatusta jonka ne tässä sai sillä tämä suorittaa "mahdotoman tehtävän" huippunopeasti.
Koodia: [Valitse]
function jaa () {
luku1=$1
luku2=$2
[[ ${luku1//[^.]/} ]] || luku1=$luku1"."
[[ ${luku2//[^.]/} ]] || luku2=$luku2"."
desimaaliluku1=${luku1##*.}
desimaaliluku2=${luku2##*.}
(( ${#desimaaliluku2} >= ${#desimaaliluku1} )) &&
{ apu=$desimaaliluku1"0000000000000000000"; desimaaliluku1=${apu:0:${#desimaaliluku2}} ;} || { apu=$desimaaliluku2"0000000000000000000"; desimaaliluku2=${apu:0:${#desimaaliluku1}} ;}

kokonaisosa1=${luku1%%.*}
kokonaisosa2=${luku2%%.*}

luku1ilmandesimaalipistetta=$kokonaisosa1$desimaaliluku1 #; echo $desimaaliluku1
luku2ilmandesimaalipistetta=$kokonaisosa2$desimaaliluku2 #; echo $desimaaliluku2
 
kokonaiset=$((10#$luku1ilmandesimaalipistetta/10#$luku2ilmandesimaalipistetta))
jakojaannos=$((10#$luku1ilmandesimaalipistetta%10#$luku2ilmandesimaalipistetta))
desimaalit=$(((1000000000*$jakojaannos)/$luku2ilmandesimaalipistetta))
desimaalit=000000000000000$desimaalit
echo $kokonaiset.${desimaalit: -9} ;}
Tarkistus:
jaa 6666666666.250000002 2
täytyy tulla: 3333333333.125000001

tai:
jaa 1 1000000000
täytyy tulla: 0.0000000001

tai:
jaa 1233457890123.23 .123456
pitää tulla: 9991072852864.421332296
- lähellä lukualueen ylärajaa
- matematiikkaohjelma: bc <<< "scale=9;1233457890123.23/.123456"  tulostaa: 9991072852864.421332296

***

Desimaalijakolasku-skriptin uuden version suoritusnopeus on 0.4ms vaikka se muodostuu 21 lauseesta. Oikeantyyppisistä käskyistä muodostuvat BASH-skriptit voivat olla todella nopeita, viisikymmentäkertaa nopeampia kuin normaalisti.

Syy uuden version tekemiseen olikin se että halusin varmistaa että tuollaisten käskyjen suorittamisessa BASH on nopea.

Jokatapauksessa desimaalijakolaskun tuloksessa esitettävien desimaalien lukumäärä on vanhassa versiossa vain 9. Uudessa versiossa nostin desimaalien lukumäärän 17:ään. Lisäämisen vaatima laskenta ei hidastanut havaittavasti.

Nyt alkaa varmistua että BASH on täysin erilainen kuin miksi se esitetään. Minkähäntakia sitä roimitaan? Antaako se liikaa vapauksia eivätkä isoiset pidä siitä?

***

Saapa nähdä kuinka neliöjuuren laskeminen onnistuu - kovin rajoitettua se kyllä on, luokkaa 7 desimaalia. Veikkaanpa ettei se montaakaan millsekuntia kestä. Ehkä se on vain toive.

Täytyy tosiaan vetää länkiä kaulaan. Kyllä se lopulta onnistui mutta nopeudesta en enää puhu mitään sillä koodi on kammottavan suuri - mutta toisaalta desimaalipilkkujen raahaaminen matkassa tekee koodista pitkän - eiköhän se koodi tänne kohta ilmesty mutta välillä muuta.

***

BASH on piikki isojen putiikkien ahterissa sillä BASH antaa koneen käyttäjänsä leikkikaluksi. Kyseessä eivät ole nuo tekstinkäsittely tai numeronmurskaus-tehtävät vaan se että BASH:illa on syvällinen liitto käyttöjärjestelmän kanssa. BASH:ia käyttäessäsi hallitset omaa konettasi ja isojen putiikkien mielestä koneesi hallinta kuuluu yksinomaan heille - nimenomaan sinulla itselläsi ei saa olla mitään oikeuksia eikä edes kykyjä tarkistaa mitä koneessasi tehdään. 

Ja BASH:in kehittäjät auttavat noita putiikkeja ansiokkaasti: oletkos koskaan tullut miettineeksi miksi noita deb:bejä ja PPA:oita harrastetaan niin kiihkeästi? Olisikohan sillä jotain tekemistä senkanssa että BASH:illa menee tosihuonosti? Ja isot putiikit nauravat partaansa kun saivat distrot älytettyä yrittämään tuhota BASH.

Sillä nuo hienot palikat tuhoavat yritteliäisyyden koska kaikki mitä aikaansaat ovat noiden palikoiden rinnalla todella hitaita virhepesiä. Ja kun yritteliäisyys kuolee niin käy juuri niin kuin on käynyt BASH:ille: perustaidotkin katoavat. Ja niinpä ollaan päädytty tilanteeseen josta nouseminen on todella vaikeaa - ja isot putiikit yrittävät estää BASH:ia saamasta entistä asemaansa.

BASH:ia lyödään kuin vierasta sikaa virtuoosienkin taholta. Tosiasia kyllä on että BASH on hidas ja opetustarkoituksiin tehtynä siinä on niinmonta kompastuskiveä ettei kukaan selviä niistä kaikista. Silti BASH:in kanssa tässä nyt eletään eikä siitä päästä eroonkaan.

Mihinkä todelliset virtuoosit pystyisivätkään jos eivät kokoajan vain toistaisi hypnoosiin vaipuneina ettei BASH kykene?

Ilmeisesti montakin läpimurtoa on tehty näiden "BASH ei kykene" väittämien suhteen. Vaikka useimmat väitteet lienevät vain omaakehua niin jotkut taitavat pitää paikkansa. Mutta alkaapäälle nuo ratkaisut ovat ilmanmuuta hitaita, reikäisiä ja vaikeasti käsitettäviä hirviöitä eikä niitä kehdata esittää eikä jakseta jalostaa - sillä jalostaminen on niin rankkaa että terveys menee. Tuo kehtaaminen on erittän käsitettävää: esittämääsi raadellaan säälittä ja narrinhattua ei kukaan halua kantaa.
« Viimeksi muokattu: 15.09.19 - klo:05.16 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #257 : 15.09.19 - klo:13.43 »
Desimaaliluvun neliöjuuren määritys pelkästään BASH:in peruskäskyillä.

Mikäli x:n neliöjuuri on y niin silloin: y*y=x. Tätä tosiasiaa soveltaen voi tehdä binäärihaun neliöjuuren selvittämiseksi.
- tämän ohjelman pääasiallisin tarkoitus ei ole neliöjuuren etsiminen vaan desimaalilaskennan toimimisen testaaminen: jos yksikin palikka höpertää edes joskus niin metsään mennään. Silti binäärilaskentakin on merkityksellinen.

Binäärihaussa tässätapauksessa tarvittavat ohjelmat - mutta kaikissa toteutuksissa ne ovat samantapaisia:
y*y (luvun kertominen itsellään)
kahdellajako
yhteenlasku
vähennyslasku ( voidan käyttää yhteenlaskua kun yhteenlaskettavan eteen laitetaan miinusmerkki)
lukujenvertailu       
binäärihaun ohjaus.

- desimaalipisteiden kuljettaminen mukana kuluttaa paljon aikaa koskei Bash desimaalipistettä edes tunne. Mutta tässävaiheessa desimaalipisteiden mukanaolo tekee toiminnasta käsitettävämmän. Ainoastaan desimaaliluvuista väliltä 2-6 saa neliöjuuren käytettyjen toteutuksien rajoitteiden takia. Näidenkin lukujen neliöjuurissa on vain noin 8 desimaalia ja aikaakin kuluu luokkaa 80ms johtuen osittain siitä että toiminta vaatii noin 2100:n lauseen suorittamisen. Desimaalilaskennassakin on loputtomasti parannettavaa nopeuden, kykyjen ja koodin koon suhteen.  Toiminnankuvaus:

1. Muodostetaan alkuarvot: y=x/2 ja delta=x/4. Haun alkuarvoksi otetaan y.   
2. aloitetaan esimerkiksi 30:een hakuun rajoitettu binäärihaku. Teoriassa lopputulos tarkentuu 3:lla desimaalilla jokaista kymmentä binäärihakua kohden.
3. lasketaan y*y
4. mikäli y:ssä desimaalipisteen kanssa on yli 11 merkkiä niin tulostetaan y ja lopetetaan. Tätä kohtaa ei välttämättä kannata toteuttaa.
5. verrataan tulosta x:ään ja vertailun perusteella lasketaan uusi y kaavalla: y=y+delta tai y=y-delta
6. delta jaetaan kahdella ja palataan binäärihaun alkuun.
7. kun binäärihaku on loppunut niin tulostetaan y ja lopetetaan.

Koodia: [Valitse]
function y*y () {
luku1=$1
luku2=$1
[[ ${luku1//[^.]/} ]] || luku1=$luku1"."
[[ ${luku2//[^.]/} ]] || luku2=$luku2"."
desimaaliosa1=${luku1##*.}
desimaaliosa1pituus=${#desimaaliosa1}
desimaaliosa2=${luku2##*.}
desimaaliosa2pituus=${#desimaaliosa2}
desimaalejakaikkiaan=$(($desimaaliosa1pituus+$desimaaliosa2pituus ))
tulo=00000000000$((10#${luku1//./}*10#${luku2//./}))    # mikäli kokonaisia ei ole ja desimaaliosan alussa on nollia katoavat ne tässä laskussa joten lisätään tuloksen eteen paljon nollia.
luku=${tulo:0: -$desimaalejakaikkiaan}                  # etunollia otetaan lopputulokseen kumminkin vain niin monta kuin niitä on kadonnut. Siis yleensä ei yhtään.
echo $((10#$luku)).${tulo: -$desimaalejakaikkiaan:8} ;} # $((10#$luku)) poistaa kokonaisosasta etunollat


function lukujenvertailu () {
luku1=$1
luku2=$2
[[ ${luku1//[^.]/} ]] || luku1=$luku1".0"
[[ ${luku2//[^.]/} ]] || luku2=$luku2".0"
kokonaisosa1=${luku1%%.*}
kokonaisosa2=${luku2%%.*}
desimaaliosa1=${luku1##*.}
desimaaliosa2=${luku2##*.}
(( ${#desimaaliosa2} >= ${#desimaaliosa1} )) &&
{ apu=$desimaaliosa1"00000000"; desimaaliosa1=${apu:0:${#desimaaliosa2}} ;} || { apu=$desimaaliosa2"00000000"; desimaaliosa2=${apu:0:${#desimaaliosa1}} ;}
luku1=$kokonaisosa1$desimaaliosa1
luku2=$kokonaisosa2$desimaaliosa2
(( $luku1 >= $luku2 )) && echo 0 || echo 1 ;} 


function kahdellajako () {
[[ ${1//[^.]/} ]] && luku1=$1 || luku1=$1".0"
luku2=2.0
desimaaliosa1=${luku1##*.}
desimaaliosa2=${luku2##*.}
(( ${#desimaaliosa2} >= ${#desimaaliosa1} )) &&
{ apu=$desimaaliosa1"00000000"; desimaaliosa1=${apu:0:${#desimaaliosa2}} ;} || { apu=$desimaaliosa2"00000000"; desimaaliosa2=${apu:0:${#desimaaliosa1}} ;}
kokonaisosa1=${luku1%%.*}
kokonaisosa2=${luku2%%.*}
luku1ilmandesimaalipistetta=$kokonaisosa1$desimaaliosa1 #; echo $desimaaliosa1
luku2ilmandesimaalipistetta=$kokonaisosa2$desimaaliosa2 #; echo $desimaaliosa2
kokonaiset=$((10#$luku1ilmandesimaalipistetta/10#$luku2ilmandesimaalipistetta))
jakojaannos=$((10#$luku1ilmandesimaalipistetta%10#$luku2ilmandesimaalipistetta))
desimaalit=$(((1000000000*$jakojaannos)/$luku2ilmandesimaalipistetta))
desimaalit=000000000000000$desimaalit
echo $kokonaiset.${desimaalit: -9} ;}


function yhteenlasku () { # voi käyttää vähennyslaskuunkin
[[ ${1//[^-]/} ]] && m1=-1 || m1=+1; [[ ${2//[^-]/} ]] && m2=-1 || m2=+1
luku1=$1
luku2=$2
luku1=${luku1//[-+]/}; luku2=${luku2//[-+]/}
luku1=${luku1//-./-0.}; luku2=${luku2//-./-0.}
[[ ${luku1//./} == $luku1 ]] && luku1=$luku1".0" # on tarpeen että luvussa on yksi desimaalipiste
[[ ${luku2//./} == $luku2 ]] && luku2=$luku2".0" # joten lisätään sellaien mikäli ei jo ole
desimaaliosa1=${luku1##*.}; desimaaliosa1pituus=${#desimaaliosa1}
desimaaliosa2=${luku2##*.}; desimaaliosa2pituus=${#desimaaliosa2}
(( ${#desimaaliosa2} >= ${#desimaaliosa1} )) &&
{ apu=$desimaaliosa1"00000000"; desimaaliosa1=${apu:0:${#desimaaliosa2}} ;} || { apu=$desimaaliosa2"00000000"; desimaaliosa2=${apu:0:${#desimaaliosa1}} ;}
#echo a$desimaaliosa2' 'b$desimaaliosa1 ; read # testatessa tämä on tarpeen
desimaaleja=${#desimaaliosa1} #; echo $desimaaleja
kokonaisluku1=${luku1%%.*}
kokonaisluku2=${luku2%%.*}
kokoluku1=$kokonaisluku1$desimaaliosa1 #; echo $kokoluku1
kokoluku2=$kokonaisluku2$desimaaliosa2 #; echo $kokoluku2
(( $m2 +1 )) && luku=$((10#$kokoluku1+10#$kokoluku2)) || luku=$((10#$kokoluku1-10#$kokoluku2))
echo ${luku:0: -$desimaaleja}.${luku: -$desimaaleja} ;}


time { juurrettava=3 # tämä on binäärihakua ohjaava pääohjelma
y=$(kahdellajako $juurrettava)
delta=$(kahdellajako $y)
for ((i=0;i<30;i++)); do
  x=$( y*y $y)
  (( $(lukujenvertailu $juurrettava $x) )) && y=$(yhteenlasku $y -$delta) || y=$(yhteenlasku $y $delta)
  delta=$(kahdellajako $delta)
done
echo $y ;}

***

Virtuoositkin haluavat korvauksen ponnisteluistaan eivätkä siten kerro saavutuksistaan julkisesti. Mutta eivät malta olla vihjailematta.

Esimerkiksi aikoinaan luin erään virtuoosin kirjoituksia ja niissä oli ohje: ei pidä käyttää sed:iä tai awk:ia ellei se ole ihan välttämätöntä sillä ne hidastavat erittäin paljon. Ihmettelin kirjoitusta silloin sillä sed ja awk ne vasta nopeita ovatkin. Kuinka päteväntuntuinen virtuoosi voi tuommoisia kirjoittaa?

Mutta desimaalilaskenta opetti että virtuoosi puhui aivan totta sillä yksikin awk tai sed laskee nopeuden noin kymmenenteen osaan - kyse on siitä muutaman millisekunnin ajasta joka kuluu sed:iä tai awk:ia kutsuttaessa ja jos niille antaa vain jonkun pienen tehtävän niin BASH itse olisi suorittanut tehtävän paljon nopeammin.

Esimerkiksi BASH:ista löytyy käsky sille kuinka sana vaihdetaan toiseen elikä siihen hommaan johon sed:iä melkein aina käytetään - tosin BASH:in käsky tunnistaa vain yksinkertaiset regex:ät. Samoin moniin awk:in yksinkertaisiin tehtäviin löytyy BASH:ista vastaava toiminta.

Kuljenko siis desimaalilaskennassakin toisten jalanjäljillä?

*** 

Uudet versiot desimaalilukujen tuplaamiseen ja puolittamiseen - ja niille yksi testaus:
Koodia: [Valitse]

function tuplaa () { [[ ${1//[^.]/} ]] && luku=$1"0"|| luku=$1".0"; desimaaliosa=${luku##*.}; desimaaliosanpituus=${#desimaaliosa}; luku=${luku//./}; let "luku <<=1";  apu=${luku: -$desimaaliosanpituus:$desimaaliosanpituus-1}; (( $apu )) && echo ${luku:0: -$desimaaliosanpituus}.$apu || echo ${luku:0: -$desimaaliosanpituus} ;}

function puolita () { [[ ${1//[^.]/} ]] && luku=$1"0"|| luku=$1".0"; desimaaliosa=${luku##*.}; desimaaliosanpituus=${#desimaaliosa}; luku=${luku//./}; let "luku >>=1"; echo ${luku:0: -$desimaaliosanpituus}.${luku: -$desimaaliosanpituus:desimaaliosanpituus-$((${luku: -1} == 0)) } ;} 

# Tässä se testaus:
puolita $(tuplaa 555.555) # arvo 555.555 ja sen tupla elikä 1111.110 ovat muunnoksille haasteellisia. Tarkoituksena on myös selvittää tuleeko ulos sama luku kuin meni sisään.

***

Koskei BASH desimaalilukuja tunne niin desimaalilaskenta suoritetaan BASH:in matematiikkaoperaatioilla tuo desimaalipiste poistettuna. Siitä seuraa nopeus mutta myös se että luvuissa saa olla korkeintaan 19 numeroa laskematta mukaan tuota desimaalipistettä - myös laskujen tuloksen pitää olla 19 numeroa pienempi.

Desimaalilaskennan skriptit ovatkin liian nopeita jotta niiden suoritusajan saisi mitattua. Esimerkiksi uusien versioiden desimaaliluvun tuplaamiseen tai puolittamiseen pitäisi olla nopeampia kuin edellisten versioiden. Mutta kuinka todeta se selvin numeroin? Nimittäin time-käsky antaa nopeudeksi yleensä nollan ja harvemmin 0.001 sekuntia. Keskiarvo monesta mitauksesta on arviolta jotain .2ms - varsin epämääräistä. Tarkempi arvo saadaan näin:

- ensin mitataan pohja-aika: time { for n in {1..1000}; do : ; read -t .01 ; done ;}
joka on noin 10.300
- sitten mitataan ohjelman suoritusaika: time { for n in {1..1000}; do tuplaa 1 ; read -t .01 ; done ;}
joka on noin 10.750. Joten kokonaisaika on noin 0.450ms
- kokonaisaika on: (aika2-aika1)/1000
- onko aika sittenkään yhtään tarkempi on vain arvattavissa - ehkä se on moninumeroista potaskaa.
- käsky: read -t 1 on tarkka 1 sekunnin viive. Tarkkuus lienee mikrosekunteja.
- käsky : on käsky olla tekemättä mitään joten ei se oikein mitään kestäkään.
- ilmeisesti enin osa ajasta matematiikalaskennan skripteissä kuluu tulkin kutsumiseen sillä sillä ei tunnu olevan suurtakaan väliä montako lausetta niissä on. Siis ilmeisesti isonkin ohjelman suorittaminen olisi nopeaa jos sen lauseet kirjoittaa tulkille mieleisiksi.

tässä yhteydessä on syytä kertoa mitä time-käskyn real, user ja system ajat ovat:
- real on ohjelman käyttäjätilassa (user) viettämä aika ja siinä on lisänä kaikki ne ajat jotka prosessi joutuu odottamaan isokenkäisempien prosessien tunkiessa väliin. Tämä on se aika jonka käyttäjän kannalta on kuluu.
- user on se aika joka kuluu käyttäjätilassa työskentelyyn laskematta mukaan odotusaikoja.
- system on se aika joka kuluu systeemipalveluissa.
- ja kun soppaan lisätään linuxin kyky multiprosessointiin (system-tilassa) niin varsinkin nopeilla skripteillä user+system voi olla suurempi kuin real.

- oikeintehdyllä skriptillä real-aika on mahdollisimman pieni, user-aika siitä vain kotuullisesti pienempi ja system-aikakin jotain vähän suurempaa kuin 0. Eri suorituskerroilla arvot saattavat vaihdella paljonkin.

- käyttöjärjestelmä myöntää jokaiselle prosessille aikaviipaleita ja päättää myös koska niitä myönnetään. Käyttöjärjestelmä voi kyllä keskeyttää jo alkaneen prosessin mutta niin ei tapahdu usein vaan odotusajat ovat niitä aikoja kun toiset tunkee väliin. Jos siis aika on tarpeeksi lyhyt niin siihen tuskin kukaan kerkiää väliin ja silloin real-aika on pieni ja user-aika siitä vain vähän pienempi.

*** 

Kertolasku jossa sekä kertoja että kerrottava voivat olla 16-numeroisia kokonaislukuja ja tulos 32 numeroinen.
 
Desimaalilaskenta on nopeaa niinkauan kuin tyydytään toimimaan alle 19-numeroisilla luvuilla. Myös kaksinkertaisen numeromäärän desimaalisummain on nopea. Mutta "kaksinkertaisen numeromäärän" kertolasku kestää 8ms sillä sen täytyy käyttää neljää normaalia kertolaskua ja kolmea kaksinkertaisen numeromäärän yhteenlaskua.

Ei etumerkin käsittelyä eikä desimaalipistettä ole toteutettu vaikka se ei olisi vaikeaa. Sillä tämä on liian hidas yleisempään käyttöön mutta sopii desimaalilaskennan testaamiseen.
Koodia: [Valitse]
function summaa36 () { # kokonaislukujen summain kaksinkertaisella numeromäärällä
luku1pituus=${#1}
luku2pituus=${#2}
luku1=00000000000000000000000000000000000000$1
luku2=00000000000000000000000000000000000000$2
luku1=${luku1: -36} # echo $luku1
luku2=${luku2: -36} # echo $luku2
luku11=${luku1:0:18}
luku12=${luku1:18}; luku12=${luku11//[^-]/}$luku12 # echo a$luku11' 'b$luku12
luku21=${luku2:0:18}
luku22=${luku2:18}; luku22=${luku21//[^-]/}$luku22 # echo c$luku21' 'd$luku22
tokaluku=$((10#$luku12+10#$luku22))  # Kun käy niin että tokaluku vuotaa yli lisätään ekalukuun 1 ja #leikataan tokaluvusta ensimmäinen merkki pois (se on se ylivuotonumero ja sen arvo on silloin 1)
((10#${#tokaluku} > 10#${#luku12} || 10#${#tokaluku} > 10#${#luku22} )) && ylivuoto=1 || ylivuoto=0
(( $ylivuoto )) && tokaluku=${tokaluku:1} # echo $ylivuoto
[[ ${luku11//[^-]/} == - && ${luku21//[^-]/} == - ]] && ekaluku=$((10#$luku11+10#$luku21-$ylivuoto)) ||ekaluku=$((10#$luku11+10#$luku21+$ylivuoto))
(( $ekaluku )) && echo $ekaluku$tokaluku || echo $tokaluku ;}


function kerro16 () { # kertolasku kun jäsenet ovat 16 numeroisia kokonaislukuja.
luku1=000000000000000000$1
luku2=000000000000000000$2
a=${luku1: -16:8}; b=${luku1: -8}
c=${luku2: -16:8}; d=${luku2: -8} # echo $a' '$b; read # echo $c' '$d
ala=$((10#$b*10#$d))
keski1=$((10#$d*10#$a))"00000000"
keski2=$((10#$c*10#$b))"00000000"
yla=$((10#$a*10#$c))"0000000000000000"
summa=$(summaa36 $ala $keski1)
summa=$(summaa36 $summa $keski2)
summa=$(summaa36 $summa $yla)
echo $summa ;}

kerro16 1111111122222222 3333333344444444  # tulokseksi pitää tulla 3703703753086418641975301234568

***

Rekursio on ohjelmointirakenne jossa funktio kutsuu itseään.

BASH:issa rekursio on hidas joten sitä ei kannata käyttää kuin joissain erikoistapauksissa. Teoria on kuitenkin hyvä tuntea.

Kullakin rekursiokierroksella kutsun parametrit ovat kutsun omia - siis muuttuja $1 viittaa jokaisella kierroksella senkertaisen kutsun $1:een.

BASH-tulkki pitää kirjaa funktiokutsuista matriisissa nimeltä: FUNKNAME.

Koodia: [Valitse]
#!/bin/bash
function kertoma () {
if [ "$1" -gt "1" ] # rekursiossa pitää ehdottomasti olla relkursion lopetusehto.
then
i=$(expr $1 - 1)
j=$(kertoma $i)
kertoma= echo "$1*$j" | bc | tr -d '\\\n'
echo $kertoma   
else
echo 1
fi
}
read -p 'mistä luvusta se kertoma lasketaan: ' x
kertoma $x

vertaapa nopeutta otettaessa kertoma luvusta 500: echo 500 | dc -e '?[q]sQ[d1=Qd1-lFx*]dsFxp' | tr -d '\\\n'

***

Murheellista tuo BASH:in kirjastojen vieroksuminen, sillä funktion tekeminen melkein mihinvaan on suorastaan rikollisen yksinkertaista - nimittäin tehdä ne tavalla joka on kymmeniäkertoja turhan hidas, virheellinen, sisältää turhia rajoituksia ja henkilökohtaisia luuloja. Joten nuo pienet funktiot ovat koneen kanssa taistelua mikä ei tosiaan kuulu yhtään mihinkään ja masentaa vain. Niinpä BASH kaipaisi ehdottomasti kirjastoja - tai siis että niitä kirjastoja olisi kirjoitettu ja että niitä käytettäisiin.

Kaikenlaista  kirjastokamaa löytyy eikä erkkikään muista edes näitä yksinkertaisia. Koska funktioita ei haeta kirjastosta niin tapahtuu seuraavaa: teet skriptiä ja kun saat sen toimimaan menet ja esittelet ylpeänä sen kavereillesi - jolloin se kaatuu heti alussa sillä jokaisessa vähänkin suuremmassa funktiossa on montakin salahautaa -> pienikin muutos dataan ja heti kosahtaa.
Koodia: [Valitse]
function int () { echo ${1%%.*} ;}

function fract () { desimaaliosa=${1##*[,.]}; (( ${#desimaaliosa} == ${#1} )) && echo 0 || echo $desimaaliosa ;}

function abs () { echo ${1//-/} ;}
 
function floor () { [[ ${1//[^-]/} ]] && m1=-1 || m1=0; kokonaisosa=${1%%[,.]*}; echo $(($kokonaisosa+(1*$m1))) ;}

function ceil () { [[ ${1//[^-]/} ]] && m1=0 || m1=-1; kokonaisosa=${1%%[,.]*}; echo $(($kokonaisosa-(1*$m1))) ;}

« Viimeksi muokattu: 25.09.19 - klo:08.47 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #258 : 25.09.19 - klo:15.31 »
BASH:ista haastellaan potaskaa kaikkialla: osoitus siitä että BASH hallitsee ohjelmarakenteen nimeltään: first-class function
Koodia: [Valitse]
function koe () { local numero; $1 $2 numero; echo $numero ;}   
function joo () { readarray $1 < <(echo 12345) ;}
a=joo
koe $a

echo $numero # numerolla on arvo täälläkin vaikka se onkin määritelty local:iksi funktiossa: "koe"
#                      sillä se on funktiossa: "joo" tehty globaaliksi

***

Anonymous function on funktio jolla ei ole nimeä. Tarkastiottaen se ei ole totta, sillä anonymous funktiolla  on muuttujan nimi. Siis tässä tapauksessa tuo a - kaavassa: a=x**2+x-1
Koodia: [Valitse]
function koe () { apu=($@); for n in $(seq 1 $((${#@}-1))); do x=${apu[$n]}; echo $(( $1 )) ; done ;}; mat=(1 2 3);a=(x**2+x-1); koe $a ${mat[@]}

- tuon matriisin palautus sotkisi muutenkin hankalasti ymmärrettävää joten se jäi pois

***

Missään näissä desimaalimaematiikan laskuista ei ole käytetty bc:tä tai jotain muuta matematiikkaohjenlmaa sillä näillä pinillä hommilla ne hidastavat..

Neliöjuuri luvuista 1.0000001-99.999999. Välin ulkopuolisiin lukuihin pätee: "kerro tai jaa sadalla kunnes pääset tälle välille. Pilkku siirtyy aina yhdellä.". Desimaaleja tuloksessa on 8. Aikaa kuluu noin 2ms.
Koodia: [Valitse]
function neliöjuuri () {
juurrettava=${1//./}"000000000000000000"
juurrettava=${juurrettava:0:17}
y=2110000000
delta=1005000000
case ${1%%.*} in
    [1-9])    { for ((i=0;i<31;i++)); do
                  x=$(($y*$y))
                  (( $x>=$juurrettava )) && y=$(($y-$delta)) || y=$(($y+$delta))
                  delta=$(($delta/2))
                done ;} ;;
    [10-99]*) { for ((i=0;i<31;i++)); do
                x=$(($y*$y/10))
                 (( $x>=$juurrettava )) && y=$(($y-$delta)) || y=$(($y+$delta))
                 delta=$(($delta/2))
               done ;} ;;
    *) echo "kerro tai jaa sadalla kunnes pääset välille 1-100. Pilkku siirtyy aina yhdellä." ;;
esac
echo ${y:0:1}.${y:1} ;}

time neliöjuuri 2.43

***

BASH:in matematiikka toimii noin 50 mikrosekunnissa mutta se toimii vain kokonaisluvuilla. Mutta itseasiassa kaikki matematiikka tietokoneissa on kokonaislukumatematiikkaa, liukulukulaskut vain pitävät kirjaa mihin desimaalipiste kuuluu - mutta tämä kirjanpitokin voidaan tehdä joko vaikeasti tai helposti,  ja se helpompitapa on että käsiteltävien desimaaliosat muodostetaan yhtäpitkiksi lisäämällä lyhyempään desimaaliosaan riitävästi peränollia. BASH:ista tämä kirjanpito puuttuu ja se on itse lisättävä. Ja koska kirjanpito on toteutettava korkeatason käskyillä hidastaa se melkolailla. Desimaalimatematiikan laskujen nopeus onkin BASH:issa luokkaa 0.2-0.5ms.

Ja näiden desimaalilaskujen numeromäärä sillon kun haetaan suurta nopeutta rajoittuu kokonaislukulaskujen 19 numeroon. Tämä on usein täysin epätyydyttävä merkkimäärä.

***

Desimaalimatematiikan peruskiviin kuuluu desimaalilukujen vertaaminen. Silloin kun joudutaan vertailemaan lukuja joissa saattaa olla yli 19 numeroa kannattaa käyttää tekstijono-vertailua sillä salahautojen väistelemisestä huolimatta se on yhtänopea kuin aritmeettinen desimaalivertain. Eikä merkkimäärällä ole rajoituksia tekstijonovertailussa. Myös kaikki lukujärjestelmät toimivat haxadesimaalit mukaanlaskien.

Tekstijonovertailuahan on käytetty aina sillä se on erinomainen numeroitakin vertailtaessa. Mutta noinvain käytettynä se höpertää monessa tilanteessa. Seuraavilla korjauksilla siitä tulee kelvollinen:

1. verrattavien lukujen etumerkit täytyy erottaa omiin muutujiinsa ja poistaa senjälkeen.
2. luvuissa täytyy aluksi olla desimaalipiste ja jos ei ole niin sellainen liitetään lukujen loppuun.
3. desimaaaliosista täytyy tehdä yhtäpitkät, elikä katkaista pitempi desimaaliosa lyhyemmän mittaiseksi.
4. lopulliset verrattavat ovat KokonaisosaDesimaaliosa; pelkkiä numeroita ilman desimaalipistettä
5. varsinainen suuruuden testaaminen täytyy suorittaa eritavoin eri etumerkkikombinaatioilla.

Ja näiden korjausten jälkeen se on hidas ellei sitä kirjoita oikein:
Koodia: [Valitse]
function onkoekasuurempi () { # rajoittamaton numeromäärä kokonais- ja desimaaliosissa
[[ ${1//[^-]/} ]] && m1=- || m1=+; [[ ${2//[^-]/} ]] && m2=- || m2=+
luku1=${1//[-+]/}; luku2=${2//[-+]/}    # etumerkki pois, myös +.
[[ ${luku1//./} == $luku1 ]] && luku1=$luku1"." # on tarpeen että luvussa on yksi desimaalipiste
[[ ${luku2//./} == $luku2 ]] && luku2=$luku2"." # joten lisätään sellaien mikäli ei jo ole
desimaaliosa1=${luku1##*.}; desimaaliosa1pituus=${#desimaaliosa1}
desimaaliosa2=${luku2##*.}; desimaaliosa2pituus=${#desimaaliosa2}
kokonaisosa1=${1%%.*}; kokonaisosa2=${2%%.*}
(( ${#desimaaliosa2} <= ${#desimaaliosa1} )) && { apu=$desimaaliosa1; desimaaliosa1=${apu:0:${#desimaaliosa2}} ;} || { apu=$desimaaliosa2; desimaaliosa2=${apu:0:${#desimaaliosa1}} ;}
luku1=$kokonaisosa1$desimaaliosa1; luku2=$kokonaisosa2$desimaaliosa2
case $m1$m2 in 
+- )  echo 1 && return ;;
-+ )  echo 0 && return ;;
++ )  [[ $luku1 > $luku2 ]] && echo 1 || echo 0 && return ;;
-- )  [[ $luku1 > $luku2 ]] && echo 0 || echo 1 ;;
esac ;}

time onkoekasuurempi 11111.2 11111.19999 # kestää 1ms

***

Lukujen yhtäsuuruus neuvotaan toteamaan näin:

((10#$a == 10#$b)) && <tehdään jotakin>

Tuo 10# poistaa luvuista etunollat joten vertailu selviää "oktaaliansasta" mutta useimmat neuvojat jättävät tuon tarpeellisen osan neuvostaan pois. Jokatapauksessa esitetyllä tavalla yhtäsuuruutta testattaessa testattavien numeromäärä on rajoitettu siihen noin 19 numeroon joka on BASH:in matematiikassa suurin hyväksytty numero eikä se riitä alkuunkaan sillä tottakai BASH joutuu tekemisiin ulkomaailman kanssa ja sietähän tulee 64 numeroisia lukuja ja suurempiakin.

Mutta yhtäsuuruutta ei kannatakaan testata matematiikkamoottorissa vaan käyttää tekstijonovertailua sillä se on joustavinta ja nopeinta. Tekstijonovertailussa on lukujen yhtäsuuruuden vertailussa vain vähän asioita jotka voisivat viedä harhaan:
1. etunolla
2. + merkki
sillä kumpikaan ei vaikuta luvun numeeriseen arvoon mutta tekstijonoarvoon kyllä joten ne molemmat poistetaan. Mutta sensijaan kaikki lukujärjestelmät käyvät ja vertailtavissa saa olla mitähyvänsä vakiotekstiäkin.

Asiasta on viisainta tehdä funktio:
Koodia: [Valitse]
function yhtäsuuruustesti () {
luku1=${1//+/}; [[ ${luku1:0:1} == 0 ]] && luku1=${luku1:1} 
luku2=${2//+/}; [[ ${luku2:0:1} == 0 ]] && luku2=${luku2:1}
[[ $luku1 == $luku2 ]] && echo "arvoltaan luvut ovat samoja" || echo "luvut eivät ole samoja" ;}
# ja sen testaus:
a=1234567890123456789012345678901234567890.1234567890123456789012345678901234567890; b=+01234567890123456789012345678901234567890.1234567890123456789012345678901234567890;
yhtäsuuruustesti $a $b

***

Aikoinaan BASH:issa harrastetiin muotoiluohjelmia tulosteiden muotoilemiseen - mutta muotoiluohjelma kykenee muotoilemaan vain koko sivun eikä yksittäistä numeroa. Siitä on menty toiseen äärimmäisyyteen ja muotoillaan ainoastaan yksittäisiä numeroita ja tekstikenttiä ja oletetaan että sehän johtaa siihen että koko sivukin on muotoiltu.

Mutta numeron tulostamiseen ei ole keinoa joka ei joskus pursuisi yli ja silloin joko tulosteen ulkoasu tai numero kärsii. Tieteellinen esitysmuotokin on vain osaratkaisu. Printf tekee hyvää työtä, mutta jotkin asiat ovat sillekin liian vaikeita.

Esimerkiksi lukua tulostettaessa desimaalit on pakko katkaista. Mutta nitä ei saa raa-asti katkaista vaan pyöristää ottaen huomioon mitä numeroita on katkaisukohdan perässä.

printf pyöristäisikin muotoilemisen ohella. Mutta pyöristettäessä sen toiminta tökkii koska laskuissa on usein desimaalipisteenä piste ja Suomalaisen koodisivun desimaalipiste on pilkku. Senvuoksi kun yrittää pyöristää käyttäen printf-käskyä se välillä epäonnistuu. Printf:ää pitää käskeä omalla tavallaan sekä pisteelle että pilkulle.

Kunnolliseen muotoilemiseen tarvitaankin sekä numeroiden muotoilemista tulostettaessa että muotoiluohjelmaa - ja nuo jo unohdetut BASH:in muotoiluohjelmat kyllä toimivat edelleen. "paras" muotoiluohjelma on column -t ja se putkitetaan tarvittaessa tulostukseen. Eiväthän nuo muotoiluohjelmat nykyisiä vaatimuksia täytä - mutta silti niistä on joskus hyötyä. Muotoiluohjelman sijoitaminen oikeaan kohtaan ei ole aina helppoa.

Seuraava tulostusfunktio ei muuta tulostettavan luvun kokonaiosaa mutta pyöristää desimaaliosan niinkuin halutaan - ja sille kelpaa sekä piste että pilkku. Se on lisäksi nopeampikin kuin printf: 
Koodia: [Valitse]
function round_in_decimals () { (( $# == 2 )) && decimals=$2 || decimals=0; kokonaisosa=${1%%[,.]*}; desimaaliosa=${1##*[,.]}; (( $decimals == 0 )) && echo $(($kokonaisosa+$((${desimaaliosa:0} >= 50)))) && return; desimaaliosa=$desimaaliosa"0000000000000000000000";echo $kokonaisosa.$(( ${desimaaliosa:0:$decimals}+$(( ${desimaaliosa:$decimals+1} >= 50)) )) ;}
# kutsu:
round_in_decimals 1.51515773 2  # tulostaa 1.52

# lisätestaaminen:
round_in_decimals .5   # tämän pitää tulostaa 0 # parametria ei tarvitse kirjoittaa kun sen arvo on 0
round_in_decimals .51  # tämän pitää tulostaa 1
round_in_decimals 1.51 # tämän pitää tulostaa 2

Muut viritelmät lukujen tulostamiseksi alkavat nopeudeltaa yleensä 4ms nurkilta - mutta ei voi ihan yksiselitteisesti tuomita niitä huonoiksi sillä esimerkiksi sovellukset jotka käyttävät bc:tä voivat samalla tehdä matemaattisia ihmetekoja.

***

Se että tuo 6-rivinen funktio jossa on monimutkaista toimintaakin voi olla nopeampi kuin prinf-käsky sai jälleen kerran ajattelemaan legendaa BASH:in hitaudesta.

Nopeudessa taitaa olla kysymys seuraavasta: BASH:issa on nopeudeltaan kahdentyyppisiä käskyjä - siitä toki manuaaleissa ohimennen kerrotaan muttei sitä että niiden nopeudet eroavat monikymmenkertaisesti:
 
1. sellaisia käskyjä joiden koodi on BASH:iin liitetty. Tällaiset käskyt ovat tosinopeita - no nopeita BASH-yksiköissä. Näitä saa olla viitisenkymmentäkin peräkkäin ennenkuin suoritus kestää millisekunnin.

2. sellaisia käskyjä jotka täytyy tulkita ennen linkittämistä. Nämä ovat niitä hitaita joista useimmat kestävät luokkaa millisekunnin.

Noissa nopeissa käskyissä on paljon vaikeasti muistettavaa digisotkua jonka kirjoittaminen vaatii lisäksi näppäin-akrobatiaa - ja koska noiden käskyjen nopeudesta ei puhuta niin niitä ei opetella käyttämään vaan totutaan käyttämään yksinomaan helppoja ja hitaita ulkoisia käskyjä - jotka lisäksi kaipaavat apua sed:iltä ja awk:ilta jotka ovat pikkuhommissa vielä hitaampia.

Koskei ole täysin tajuttu että nopeitakin käskyjä löytyy niin on luultu ettei BASH:ista ole ollenkaan numeronmurskaamiseen ilman bc:n apua. Tätä on vahvistanut luulo ettei BASH kykene desimaalilaskentaan. Lisäksi luullaan että matematiikan 19:sta merkkin lukumäärää ei voisi mitenkään lisätä. Mutta pienissä "nelilaskin" laskuissa desimaaliluvuillakin BASH on neljäkertaa nopeampi kuin bc.

BASH:in vadtustajia täytyy ihailla: väitetään hieman väärin että BASH ei osaa palauttaa funktioparametreja, eval-käskyn tuomitseminen - mutta vain BASH:issa, kirjasto-osoittimen poistaminen, väärät väitteet ensimmäisenluokan funktioita ja niinpoispäin. Mikähyvänsä näistä riittää tuhoamaan melkein mitavaan. BASH halutaan tuhota.   

Toki BASH:issa on paljon ihan oikeaakin kritisoitavaa. Ja jotakin täytyisi tehdä jotta sen kritisoitavan voisi skripteissään tehdä oikealla tavalla - esimerkiksi palauttaa kirjasto-osoitin ja koota muutama kirjasto.

Sehän selvisi jo aikoinaan että sed, awk ja bc täytyy unohtaa kun halutaan nopeutta yksinkertaisiin tehtäviin. Mutta se täytyi yrittää selvittää mitä kaikkea muuta kuin BASH:in nopeita käskyjä voi nopeaan skriptiin ottaa mukaan: ilmeisesti ainakin funktiot ja looppit ovat nopeita - täytyy korjata pahaa puhettani looppien hitaudesta: eivät loopit ole hitaita vaan se minkä ne suorittavat.

Käytännön skripteistä näkee parhaiten mikä kannattaa:

Ensin rajoittamattoman monen rajoittamattoman moninumeroisten lukujen yhteenlaskin jossa on pari funktiota ja neljä looppia. Tätä ei ole suoranaisesti tarkoitettu käytettäväksi vaan se on tehty nopeiden käskyjen löytämiseksi. Lopussa kuvatulla laskulla skriptin läpikäyminen vaatii noin 150:n monipuolisen käskyn suorittamisen elikä noin 20 mikrosekuntia per käsky -> siis kaikki sen käskyt ovat nopeita.
Koodia: [Valitse]
function radd () { 

function desimaalisumma () { for((j=1;j<=$2;j++)); do summa=$((summa+${desimaaliosa[j]:$1:1})); done; [[ $muistinumero ]] || muistinumero=0;summa=$((summa+$muistinumero)); muistinumero=0 ;}

function kokonaissumma  () { for((j=1;j<=$2;j++)); do summa=$((summa+${kokonaisosa[j]:$1:1})); done; [[ $muistinumero ]] || muistinumero=0; summa=$((summa+$muistinumero)); muistinumero=0 ;}

i=1; maxdesimaalit=0; maxkokonaiset=0; muistinumero=0; for n in $@; do desimaaliosa[i]=${n##*.}; desimaaliosanpituus[i]=${#desimaaliosa[i]}; (( ${desimaaliosanpituus[i]} >= $maxdesimaalit )) && maxdesimaalit=${desimaaliosanpituus[i]}; kokonaisosa[i]=${n%%.*};  kokonaisosanpituus[i]=${#kokonaisosa[i]}; (( ${kokonaisosanpituus[i]} >= $maxkokonaiset )) && maxkokonaiset=${kokonaisosanpituus[i]}; (( i++ )); done

i=1; for n in $@; do desimaaliosa[i]=${n##*.}"000000000000000000"; desimaaliosa[i]=${desimaaliosa[i]:0:$maxdesimaalit};
kokonaisosa[i]="000000000000"${kokonaisosa[i]}; kokonaisosa[i]=${kokonaisosa[i]: -$maxkokonaiset};
echo ${kokonaisosa[i]}.${desimaaliosa[i]}; (( i++ )); done 

desimaalit=''; for((i=maxdesimaalit-1;i>=0;i--)); do summa=0; desimaalisumma i $# ; desimaalit=${summa: -1}$desimaalit; muistinumero=${summa:0: -1}; done; # echo $desimaalit' '$muistinumero; read

kokonaiset=''; for((i=maxkokonaiset-1;i>=0;i--)); do summa=0; kokonaissumma i $# ; kokonaiset=${summa: -1}$kokonaiset; muistinumero=${summa:0: -1}; done; echo $muistinumero$kokonaiset.$desimaalit
}

time radd 1.17 22.222 3.33 4.4




« Viimeksi muokattu: 09.10.19 - klo:10.26 kirjoittanut petteriIII »

petteriIII

  • Käyttäjä
  • Viestejä: 693
    • Profiili
Vs: Ohjeita shell-skriptaukseen (bash)
« Vastaus #259 : 08.10.19 - klo:04.13 »
BASH:ista haastellaan potaskaa kaikkialla: osoitus siitä että BASH hallitsee ohjelmarakenteen nimeltään: first-class function
Koodia: [Valitse]
function koe () { local numero; $1 $2 numero; echo $numero ;}   
function joo () { readarray $1 < <(echo 12345) ;}
a=joo
koe $a

echo $numero # numerolla on arvo täälläkin vaikka se onkin määritelty local:iksi funktiossa: "koe"
#                      sillä se on funktiossa: "joo" tehty globaaliksi

***

Anonymous function on funktio jolla ei ole nimeä. Tarkastiottaen se ei ole totta, sillä anonymous funktiolla  on muuttujan nimi. Siis tässä tapauksessa tuo a - kaavassa: a=x**2+x-1
Koodia: [Valitse]
function koe () { apu=($@); for n in $(seq 1 $((${#@}-1))); do x=${apu[$n]}; echo $(( $1 )) ; done ;}; mat=(1 2 3);a=(x**2+x-1); koe $a ${mat[@]}

- tuon matriisin palautus sotkisi muutenkin hankalasti ymmärrettävää joten se jäi pois

***

Missään näissä desimaalimaematiikan laskuista ei ole käytetty bc:tä tai jotain muuta matematiikkaohjenlmaa sillä näillä pinillä hommilla ne hidastavat..

Neliöjuuri luvuista 1.0000001-99.999999. Välin ulkopuolisiin lukuihin pätee: "kerro tai jaa sadalla kunnes pääset tälle välille. Pilkku siirtyy aina yhdellä.". Desimaaleja tuloksessa on 8. Aikaa kuluu noin 2ms.
Koodia: [Valitse]
function neliöjuuri () {
juurrettava=${1//./}"000000000000000000"
juurrettava=${juurrettava:0:17}
y=2110000000
delta=1005000000
case ${1%%.*} in
    [1-9])    { for ((i=0;i<31;i++)); do
                  x=$(($y*$y))
                  (( $x>=$juurrettava )) && y=$(($y-$delta)) || y=$(($y+$delta))
                  delta=$(($delta/2))
                done ;} ;;
    [10-99]*) { for ((i=0;i<31;i++)); do
                x=$(($y*$y/10))
                 (( $x>=$juurrettava )) && y=$(($y-$delta)) || y=$(($y+$delta))
                 delta=$(($delta/2))
               done ;} ;;
    *) echo "kerro tai jaa sadalla kunnes pääset välille 1-100. Pilkku siirtyy aina yhdellä." ;;
esac
echo ${y:0:1}.${y:1} ;}

time neliöjuuri 2.43

***

BASH:in matematiikka toimii noin 50 mikrosekunnissa mutta se toimii vain kokonaisluvuilla. Mutta itseasiassa kaikki matematiikka tietokoneissa on kokonaislukumatematiikkaa, liukulukulaskut vain pitävät kirjaa mihin se desimaalipiste kuuluu. BASH:ista tämä kirjanpito puuttuu ja se on itse lisättävä. Ja koska kirjanpito on toteutettava korkeatason käskyillä hidastaa se melkolailla. Desimaalimatematiikan laskujen nopeus onkin BASH:issa luokkaa 0.2-0.5ms.

Ja näiden desimaalilaskujen numeromäärä sillon kun haetaan suurta nopeutta rajoittuu kokonaislukulaskujen 19 numeroon. Tämä on usein täysin epätyydyttävä merkkimäärä.

***

Desimaalimatematiikan peruskiviin kuuluu desimaalilukujen vertaaminen. Silloin kun joudutaan vertailemaan lukuja joissa saattaa olla yli 19 numeroa kannattaa käyttää tekstijono-vertailua sillä salahautojen väistelemisestä huolimatta se on yhtänopea kuin aritmeettinen desimaalivertain. Eikä merkkimäärällä ole rajoituksia tekstijonovertailussa. Myös kaikki lukujärjestelmät toimivat haxadesimaalit mukaanlaskien.

Tekstijonovertailuahan on käytetty aina sillä se on erinomainen numeroitakin vertailtaessa. Mutta noinvain käytettynä se höpertää monessa tilanteessa. Seuraavilla korjauksilla siitä tulee kelvollinen:

1. verrattavien lukujen etumerkit täytyy erottaa omiin muutujiinsa ja poistaa senjälkeen.
2. luvuissa täytyy aluksi olla desimaalipiste ja jos ei ole niin sellainen liitetään lukujen loppuun.
3. desimaaaliosista täytyy tehdä yhtäpitkät, elikä katkaista pitempi desimaaliosa lyhyemmän mittaiseksi.
4. lopulliset verrattavat ovat KokonaisosaDesimaaliosa; pelkkiä numeroita ilman desimaalipistettä
5. varsinainen suuruuden testaaminen täytyy suorittaa eritavoin eri etumerkkikombinaatioilla.

Ja näiden korjausten jälkeen se on hidas ellei sitä kirjoita oikein:
Koodia: [Valitse]
function onkoekasuurempi () { # rajoittamaton numeromäärä kokonais- ja desimaaliosissa
[[ ${1//[^-]/} ]] && m1=- || m1=+; [[ ${2//[^-]/} ]] && m2=- || m2=+
luku1=${1//[-+]/}; luku2=${2//[-+]/}    # etumerkki pois, myös +.
[[ ${luku1//./} == $luku1 ]] && luku1=$luku1"." # on tarpeen että luvussa on yksi desimaalipiste
[[ ${luku2//./} == $luku2 ]] && luku2=$luku2"." # joten lisätään sellaien mikäli ei jo ole
desimaaliosa1=${luku1##*.}; desimaaliosa1pituus=${#desimaaliosa1}
desimaaliosa2=${luku2##*.}; desimaaliosa2pituus=${#desimaaliosa2}
kokonaisosa1=${1%%.*}; kokonaisosa2=${2%%.*}
(( ${#desimaaliosa2} <= ${#desimaaliosa1} )) && { apu=$desimaaliosa1; desimaaliosa1=${apu:0:${#desimaaliosa2}} ;} || { apu=$desimaaliosa2; desimaaliosa2=${apu:0:${#desimaaliosa1}} ;}
luku1=$kokonaisosa1$desimaaliosa1; luku2=$kokonaisosa2$desimaaliosa2
case $m1$m2 in 
+- )  echo 1 && return ;;
-+ )  echo 0 && return ;;
++ )  [[ $luku1 > $luku2 ]] && echo 1 || echo 0 && return ;;
-- )  [[ $luku1 > $luku2 ]] && echo 0 || echo 1 ;;
esac ;}

time onkoekasuurempi 11111.2 11111.19999 # kestää 1ms

***

Lukujen yhtäsuuruus neuvotaan toteamaan näin:

((10#$a == 10#$b)) && <tehdään jotakin>

Tuo 10# poistaa luvuista etunollat joten vertailu selviää "oktaaliansasta" mutta useimmat neuvojat jättävät tuon tarpeellisen osan neuvostaan pois. Jokatapauksessa esitetyllä tavalla yhtäsuuruutta testattaessa testattavien numeromäärä on rajoitettu siihen noin 19 numeroon joka on BASH:in matematiikassa suurin hyväksytty numero eikä se riitä alkuunkaan sillä tottakai BASH joutuu tekemisiin ulkomaailman kanssa ja sietähän tulee 64 numeroisia lukuja ja suurempiakin.

Mutta yhtäsuuruutta ei kannatakaan testata matematiikkamoottorissa vaan käyttää tekstijonovertailua sillä se on joustavinta ja nopeinta. Tekstijonovertailussa on lukujen yhtäsuuruuden vertailussa vain vähän asioita jotka voisivat viedä harhaan:
1. etunolla
2. + merkki
sillä kumpikaan ei vaikuta luvun numeeriseen arvoon mutta tekstijonoarvoon kyllä joten ne molemmat poistetaan. Mutta sensijaan kaikki lukujärjestelmät käyvät ja vertailtavissa saa olla mitähyvänsä vakiotekstiäkin. Desimaalipistekään ei ole ongelma.

Asiasta on viisainta tehdä funktio:
Koodia: [Valitse]
function yhtäsuuruustesti () {
luku1=${1//+/}; [[ ${luku1:0:1} == 0 ]] && luku1=${luku1:1} 
luku2=${2//+/}; [[ ${luku2:0:1} == 0 ]] && luku2=${luku2:1}
[[ $luku1 == $luku2 ]] && echo "arvoltaan luvut ovat samoja" || echo "luvut eivät ole samoja" ;}
# ja sen testaus:
a=1234567890123456789012345678901234567890.1234567890123456789012345678901234567890; b=+01234567890123456789012345678901234567890.1234567890123456789012345678901234567890;
yhtäsuuruustesti $a $b

***

Aikoinaan BASH:issa harrastetiin muotoiluohjelmia tulosteiden muotoilemiseen - mutta muotoiluohjelma kykenee muotoilemaan vain koko sivun eikä yksittäistä numeroa. Siitä on menty toiseen äärimmäisyyteen ja muotoillaan ainoastaan yksittäisiä numeroita ja tekstikenttiä ja oletetaan että sehän johtaa siihen että koko sivukin on muotoiltu.

Mutta numeron tulostamiseen ei ole keinoa joka ei joskus pursuisi yli ja silloin joko tulosteen ulkoasu tai numero kärsii. Tieteellinen esitysmuotokin on vain osaratkaisu. Printf tekee hyvää työtä, mutta jotkin asiat ovat sillekin liian vaikeita.

Esimerkiksi lukua tulostettaessa desimaalit on pakko katkaista. Mutta nitä ei saa raa-asti katkaista vaan pyöristää ottaen huomioon mitä numeroita on katkaisukohdan perässä.

printf pyöristäisikin muotoilemisen ohella. Mutta pyöristettäessä sen toiminta tökkii koska laskuissa on usein desimaalipisteenä piste ja Suomalaisen koodisivun desimaalipiste on pilkku. Senvuoksi kun yrittää pyöristää käyttäen printf-käskyä se välillä epäonnistuu. Printf:ää pitää käskeä omalla tavallaan sekä pisteelle että pilkulle.

Kunnolliseen muotoilemiseen tarvitaankin sekä numeroiden muotoilemista tulostettaessa että muotoiluohjelmaa - ja nuo jo unohdetut BASH:in muotoiluohjelmat kyllä toimivat edelleen. "paras" muotoiluohjelma on column -t ja se putkitetaan tarvittaessa tulostukseen. Eiväthän nuo muotoiluohjelmat nykyisiä vaatimuksia täytä - mutta silti niistä on joskus hyötyä. Muotoiluohjelman sijoitaminen oikeaan kohtaan ei ole aina helppoa.

Seuraava tulostusfunktio ei muuta tulostettavan luvun kokonaiosaa mutta pyöristää desimaaliosan niinkuin halutaan - ja sille kelpaa sekä piste että pilkku. Se on lisäksi nopeampikin kuin printf: 
Koodia: [Valitse]
function round_in_decimals () { (( $# == 2 )) && decimals=$2 || decimals=0; kokonaisosa=${1%%[,.]*}; desimaaliosa=${1##*[,.]}; (( $decimals == 0 )) && echo $(($kokonaisosa+$((${desimaaliosa:0} >= 50)))) && return; desimaaliosa=$desimaaliosa"0000000000000000000000";echo $kokonaisosa.$(( ${desimaaliosa:0:$decimals}+$(( ${desimaaliosa:$decimals+1} >= 50)) )) ;}
# kutsu:
round_in_decimals 1.51515773 2  # tulostaa 1.52

# lisätestaaminen:
round_in_decimals .5   # tämän pitää tulostaa 0 # parametria ei tarvitse kirjoittaa kun sen arvo on 0
round_in_decimals .51  # tämän pitää tulostaa 1
round_in_decimals 1.51 # tämän pitää tulostaa 2

Muut viritelmät lukujen tulostamiseksi alkavat nopeudeltaa yleensä 4ms nurkilta - mutta ei voi ihan yksiselitteisesti tuomita niitä huonoiksi sillä esimerkiksi sovellukset jotka käyttävät bc:tä voivat samalla tehdä matemaattisia ihmetekoja.

***

Se että tuo 6-rivinen funktio jossa on monimutkaista toimintaakin voi olla nopeampi kuin prinf-käsky sai jälleen kerran ajattelemaan legendaa BASH:in hitaudesta.

Nopeudessa taitaa olla kysymys seuraavasta: BASH:issa on nopeudeltaan kahdentyyppisiä käskyjä - siitä toki manuaaleissa ohimennen kerrotaan muttei sitä että niiden nopeudet eroavat monikymmenkertaisesti:
 
1. sellaisia käskyjä joiden koodi on BASH:iin liitetty. Tällaiset käskyt ovat tosinopeita - no nopeita BASH-yksiköissä. Näitä saa olla viitisenkymmentäkin peräkkäin ennenkuin suoritus kestää millisekunnin.

2. sellaisia käskyjä jotka täytyy tulkita ennen linkittämistä. Nämä ovat niitä hitaita joista useimmat kestävät luokkaa millisekunnin.

Noissa nopeissa käskyissä on paljon vaikeasti muistettavaa digisotkua jonka kirjoittaminen vaatii lisäksi näppäin-akrobatiaa - ja koska noiden käskyjen nopeudesta ei puhuta niin niitä ei opetella käyttämään vaan totutaan käyttämään yksinomaan helppoja ja hitaita ulkoisia käskyjä - jotka lisäksi kaipaavat apua sed:iltä ja awk:ilta jotka ovat pikkuhommissa vielä hitaampia.

Koskei ole täysin tajuttu että nopeitakin käskyjä löytyy niin on luultu ettei BASH:ista ole ollenkaan numeronmurskaamiseen ilman bc:n apua. Tätä on vahvistanut luulo ettei BASH kykene desimaalilaskentaan. Lisäksi luullaan että matematiikan 19:sta merkkin lukumäärää ei voisi mitenkään lisätä. Mutta pienissä "nelilaskin" laskuissa desimaaliluvuillakin BASH on neljäkertaa nopeampi kuin bc.

BASH:in vastustajia täytyy ihailla: väitetään väärin että BASH ei osaa palauttaa funktioparametreja, eval-käskyn tuomitseminen - mutta vain BASH:issa, kirjasto-osoittimen poistaminen, väärät väitteet ensimmäisenluokan funktioita ja niinpoispäin. Mikähyvänsä näistä riittää tuhoamaan melkein mitavaan. Tuntuu siltä että BASH halutaan tuhota ja nimenomaan niin etteise koskaan palaisi.   

Sehän selvisi jo aikoinaan että sed, awk ja bc täytyy unohtaa kun halutaan nopeutta yksinkertaisiin tehtäviin. Mutta se täytyi yrittää selvittää mitä kaikkea muuta kuin BASH:in nopeita käskyjä voi nopeaan skriptiin ottaa mukaan: ilmeisesti ainakin funktiot ja looppit ovat nopeita - täytyy korjata pahaa puhettani looppien hitaudesta: eivät loopit ole hitaita vaan se minkä ne suorittavat.

Käytännön skripteistä näkee parhaiten mikä kannattaa:
« Viimeksi muokattu: 08.10.19 - klo:04.28 kirjoittanut petteriIII »