Kirjoittaja Aihe: Mitkä edistyneemmät shell scripting -manuaalit kannattaa kahlata läpi? [Ratkaist  (Luettu 4655 kertaa)

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Olen vuosia tehnyt kaikki jutut perlillä, mutta nyt kun olen virittelemässä komentotiedostoja omalle Ubuntu-koneelleni, haluaisin tietää mitkä työkalut nykyään ovat ne joita suositellaan. Huomasin jo että dash on bashin sutjakampi vaihtoehto, mutta dash-hakuja on vaikea tehdä netistä, kun dash on myös englannin kielen sana. Mistä löytyy hyvä tai ainakin asiallinen dash-manuaali? Ja millä kielellä kannattaa tehdä merkkijono- ja regexec-manipulointia silloin kun sitä tekee satunnaisesti jossain komentotiedostossa, mutta sed ei silti riitä? Oletan että mawk tai gawk, mutta onko jotain muutakin mistä pitäisi tietää?

Esimerkki 1: käytän yhdessä scriptissä bashin [[ ]]-testiä, jota dash ei tunne. Mistä löydän dash-manuaalin jonka avulla saan tämäntapaiset testit muunnettua dash-muotoon? Vai kannattaako dashin sijaan käyttää jotain muuta?

Koodia: [Valitse]
[[ $TITLE == *"$1"* ]]
Esimerkki 2: tämän pikku muunnoksen sain koodatuksi siistin tiiviiseen muotoon perlin avulla, mutta mitä manuaaleja mun pitäisi vertailla että löydän sille kevyemmän vaihtoehdon?

Koodia: [Valitse]
cryptmount -l | perl -pe '/^\S+ .* "(.+)" .*/
&& qx(mountpoint $1) =~ / is a mountpoint$/
&& s/ \[to mount on / \[is mounted on /'

En ole siis etsimässä ketään koodaamaan näitä uusiksi (kyllä ne näinkin toimivat ihan hyvin), vaan etsin nimenomaan linkkejä manuaaleihin yms. sivuihin netissä. Suorituskyvyllä ei ole näissä esimerkeissä mitään käytännön merkitystä. Haluan vain opetella uutta siltä varalta että joskus on.
« Viimeksi muokattu: 18.05.14 - klo:19.33 kirjoittanut AimoE »

nm

  • Käyttäjä
  • Viestejä: 16429
    • Profiili
Huomasin jo että dash on bashin sutjakampi vaihtoehto, mutta dash-hakuja on vaikea tehdä netistä, kun dash on myös englannin kielen sana. Mistä löytyy hyvä tai ainakin asiallinen dash-manuaali?

Dash noudattaa POSIX.1-2008-standardia: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html

Perusneuvoja Bashiin tottuneille:
http://mywiki.wooledge.org/Bashism
https://wiki.ubuntu.com/DashAsBinSh#Dash_as_.2BAC8-bin.2BAC8-sh


Ja millä kielellä kannattaa tehdä merkkijono- ja regexec-manipulointia silloin kun sitä tekee satunnaisesti jossain komentotiedostossa, mutta sed ei silti riitä? Oletan että mawk tai gawk, mutta onko jotain muutakin mistä pitäisi tietää?

Awk on ihan hyvä ratkaisu shell-skriptien sisällä. Seuraava askel onkin jo joku varsinainen ohjelmointikieli, jolloin koko skripti kannattaa yleensä kirjoittaa tällä kielellä. Perl ja Python ovat yleisimmät vaihtoehdot järjestelmien ylläpitoon liittyvässä skriptiohjelmoinnissa, eikä kumpikaan ole erityisen kevyt tai nopea.


Esimerkki 2: tämän pikku muunnoksen sain koodatuksi siistin tiiviiseen muotoon perlin avulla, mutta mitä manuaaleja mun pitäisi vertailla että löydän sille kevyemmän vaihtoehdon?

Koodia: [Valitse]
cryptmount -l | perl -pe '/^\S+ .* "(.+)" .*/
&& qx(mountpoint $1) =~ / is a mountpoint$/
&& s/ \[to mount on / \[is mounted on /'

Perl on kyllä veikeä kieli. Välillä ajattelen, että sitä parjataan turhaan koodin heikosta luettavuudesta, mutta sitten tulee vastaan tällaisia esimerkkejä. ;) Jos tuota joutuu joskus tulkitsemaan joku toinen, Perliä vähemmän tunteva henkilö, suosittelisin ehkä laveampaa ohjelmointityyliä. Säännölliset lausekkeet ovat vielä ok (joskin ne asettavat jo ensimmäisen kynnyksen ymmärtämiselle), mutta muun syntaksin voisi kirjoittaa auki.

Tässä tapauksessa sama onnistunee dash-skriptillä grepin ja sedin avulla. Bash-skriptillä onnistuu ilman niitäkin. Ei ehkä yhtä tiiviissä muodossa, mutta se ei ole välttämättä huono ominaisuus.


AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Dash noudattaa POSIX.1-2008-standardia

Pelkäsinkin että POSIX olisi ainoa lähde kun tieten tahtoen rajataan vai siihen. Bash vs dash -kuvauksia onnistuin itsekin löytämään, mutta jotenkin elin silti toivossa että dashistä olisi  jotain muutakin kuin vaan pelkkä perus-POSIX.

Awk on ihan hyvä ratkaisu shell-skriptien sisällä. Seuraava askel onkin jo joku varsinainen ohjelmointikieli, jolloin koko skripti kannattaa yleensä kirjoittaa tällä kielellä. Perl ja Python ovat yleisimmät vaihtoehdot järjestelmien ylläpitoon liittyvässä skriptiohjelmoinnissa, eikä kumpikaan ole erityisen kevyt tai nopea.

Niinpä. Yleensä tosiaan käytän perliä kaikkeen.

Tässä tapauksessa sama onnistunee dash-skriptillä grepin ja sedin avulla. Bash-skriptillä onnistuu ilman niitäkin. Ei ehkä yhtä tiiviissä muodossa, mutta se ei ole välttämättä huono ominaisuus.

Katsopa tarkemmin; siellä on yksi shell-komento seassa (ehkä se erottuisi helpommin jos olisin qx()-syntaksin sijaan käyttänyt backtikkejä). Awkilla sen saa suoritettua, mutta minä en ainakaan keksi miten tuon saman tekee pelkästään dashin ja grepin ja sedin avulla, ainakaan kohtuullisella koodimäärällä.

Itse asiassa komentotiedosto on muutenkin niin lyhyt että sen postaaminen kokonaisena voisi valaista asiaa hieman paremmin.

Koodia: [Valitse]
#!/bin/sh
list=`cryptmount -l`
[ -z "$list" ] && {
    # Fired from Launcher in terminal window.
    printf "Salakansioita ei ole." ; read any ; exit 1 ;
}
while :
do
    menu=`echo "$list" | perl -pe '/^\S+ .* "(.+)" .*/
&& qx(mountpoint $1) =~ / is a mountpoint$/
&& s/ \[to mount on / \[is mounted on /'` # Mark mounted entries.

    printf "\nSalakansiot:\n$menu\n\nHakusana [poistu]: "
    read word
    [ -z "$word" ] && exit                        # Exit when none selected.

    line=`echo "$menu" | grep "$word"`
    [ `echo "$line" | wc -l` = 1 ] || continue    # Loop until unique.

    eval `echo "$line" | perl -pe '
s/^(\S+) .* "(.+)" .*/secret=$1 ; public=$2/
    '`
    if   mountpoint -q $public
    then cryptmount -u $secret && echo "Salakansio $public on irrotettu."
    else cryptmount -m $secret && echo "Salakansio $public on liitetty."
    fi
done

Kuten huomaat, siinä on kaksi perl-komentoa, ja toisen niistä voi erittäin helposti korvata sedillä. Mutta ekaa en lähtisi sedillä virittämään. Awk on siis seuraava vaihtoehto.

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Kyllä kai ne tärkeimmät sivut jo tulivat esiin, joten merkkasin ratkaistuksi.

Minusta perlin lakonisuus ei kyllä yhtään häpeä awkin rinnalla. Kun muutan säännölliset lausekkeet mahdollisimman samanlaisiksi, vertailtavana on:

Koodia: [Valitse]
cryptmount -l | perl -pe '/^[^ "]+ [^"]* "([^ "]+)" .+/
    && qx(mountpoint $1) =~ / is a mountpoint$/
    && s/ \[to mount on / [is mounted on /
'
ja
Koodia: [Valitse]
cryptmount -l | mawk '/^[^ "]+ [^"]* "[^ "]+" .+/ {
    split($0, mp, /\"/);
    sprintf("mountpoint %s", mp[2]) | getline mpstat;
    if (mpstat ~ / is a mountpoint/)
    then sub(/ \[to mount on /," [is mounted on ");
    print;
}'

Koska POSIX-awk ei osaa poimia osumia, jouduin keksimään sille kiertotien. Onneksi lainausmerkit olivat tässä valmiiksi tarjolla splittiä varten.

Lisäksi,

     `mountpoint $1`

(tai sama qx-syntaksilla) on minusta paljon selvempi kuin

    sprintf("mountpoint %s", mp[2]) | getline mps


Esimerkkitapauksen toisessa muunnoksessa sed ja perl -komennot ovat lähes samat:

Koodia: [Valitse]
perl -pe 's/^([^ "]+) [^"]* "([^ "]+)" .+/secret=$1 ; public=$2/'
sed 's/^\([^ "]*\) [^"]* "\([^ "]*\)" .*/secret=\1 ; public=\2/'

Toivottavasti en hirveästi tarvitse awkia.

nm

  • Käyttäjä
  • Viestejä: 16429
    • Profiili
Katsopa tarkemmin; siellä on yksi shell-komento seassa (ehkä se erottuisi helpommin jos olisin qx()-syntaksin sijaan käyttänyt backtikkejä). Awkilla sen saa suoritettua, mutta minä en ainakaan keksi miten tuon saman tekee pelkästään dashin ja grepin ja sedin avulla, ainakaan kohtuullisella koodimäärällä.

Huomasin kyllä qx:n ja ymmärsin perl-skriptin toiminnan pienen pähkäilyn jälkeen.

Tässä sama pelkällä sh:lla ja sedillä.

Koodia: [Valitse]
#!/bin/sh
list=`cryptmount -l`
[ -z "$list" ] && {
    # Fired from Launcher in terminal window.
    printf "Salakansioita ei ole." ; read any ; exit 1 ;
}

while :
do
    menu=$(
        # Process cryptmout -l output line by line
        echo "$list" |
        while IFS= read -r line; do
            # Parse mountpoint
            mp=`echo $line | sed -e 's/^.* "\(.*\)" as .*$/\1/'`
            # Check if a file system is mounted there
            if mountpoint -q "$mp"; then
                line=`echo "$line" | sed -e 's/to mount on /is mounted on /'`
            fi
            echo "$line"
        done
    )

    printf "\nSalakansiot:\n$menu\n\nHakusana [poistu]: "
    read word
    [ -z "$word" ] && exit                        # Exit when none selected.

    line=`echo "$menu" | grep "$word"`
    [ `echo "$line" | wc -l` = 1 ] || continue    # Loop until unique.

    secret=`echo "$line" | sed -e 's/[[:space:]]*\[.*$//'`
    # Onnistuu myös cut-komennolla:
    #secret=`echo "$line" | cut -d " " -f1`

    public=`echo "$line" | sed -e 's/^.* \[.* "\(.*\)" .*$/\1/'`
    
    if   mountpoint -q $public
    then cryptmount -u $secret && echo "Salakansio $public on irrotettu."
    else cryptmount -m $secret && echo "Salakansio $public on liitetty."
    fi
done

Vaikeinta tässä on merkkijonon käsittely rivi kerrallaan silmukassa, mikä on POSIX-shellissä tarpeettoman monimutkaista. Yleisin ratkaisu on tuo read-komennon soveltaminen. Vaihtoehtoisesti voisi määritellä IFS:ksi rivinvaihdon ja käyttää for-silmukkaa.
« Viimeksi muokattu: 18.05.14 - klo:20.57 kirjoittanut nm »

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Perl-komennilla tämän saa pysymään sen kokoisena että se mahtuu yhteen ikkunalliseen editorissa.

Tämä perl vs shell alkaa itse asiassa tuntua pikemminkin functional vs procedural -jutulta. Vaikka perl ei ole millään muotoa funktionaalinen kieli, niin se tekee helpoksi lausekkeita rakentamalla välttää proseduraalisia rakenteita. Joitakin se viehättää, joitakin ei. Minua funktionaalinen tyyli on aina houkutellut, minusta se on selkeä. Se on ehkä tärkein syy siihen että käytän perliä niin mielelläni. Mutta jos siihen ei ole tottunut eikä siihen ole viehtymystä, niin silloin se tietty tuntuu vaikealta lukea. Kyllä perlilläkin voi tehdä kaiken puhtaan proseduraalisesti, se ei vaan tunnu mielekkäältä jos on funktionaalisen makuun päässyt.

Nyt pitäisi varmaan tehdä jotain suorituskykyvertailuja näiden kanssa.

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Mawk voitti kisan tässä tapauksessa.

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Pakko vielä palata tähän nopeusvertailuun. Eilen testasin vain sitä ekaa perl-komentoa verrattuna muihin toteutuksiin, mutta tänään sain päähäni kokeilla myös sen toisen perl-komennon vaihtoehtoja. Tulos on minulle käsittämätön.

Kirjoitin ensinnäkin tällaisen testitapaussarjan:

Koodia: [Valitse]
#!/bin/sh

case=$1
repeat=$2
list=`cryptmount -l | grep Piilo`
echo "
=== test$case ($repeat rounds) ==="

testEvalPerl () {
    eval `echo "$line" | perl -pe '
s/^([^ "]+) [^"]* "([^ "]+)" .+/secret=$1 ; public=$2/'`
}
testEvalSed () {
    eval `echo "$line" | sed '
s/^\([^ "]*\) [^"]* "\([^ "]*\)" .*/secret=\1 ; public=\2/'`
}
testCutSed () {
    secret=`echo "$line" | cut -d " " -f1`
    public=`echo "$line" | sed -e 's/^[^"]* "\([^ "]*\)" .*$/\1/'`
}
testTwoSed () {
    secret=`echo "$line" | sed -e 's/ *\[.*$//'`
    public=`echo "$line" | sed -e 's/^[^"]* "\([^ "]*\)" .*$/\1/'`
}
i=0 ; while [ $i -lt $repeat ] ; do test$case ; i=$(($i+1)) ; done

Sitten suoritin testin näin:

Koodia: [Valitse]
repeat=2000
for test in EvalPerl EvalSed CutSed TwoSed
do
    time -p ./test-case $test $repeat
done

Vaikka kuinka toistan testiä, niin EvalSed on aina nopein. Kaiken lisäksi EvalPerl on toiseksi nopein, ja TwoSed on kaikkein hitain.

Onko tässä testitavassa jotain mätää? Oletin ilman muuta että EvalPerl on hitain ja että CutSed ja TwoSed olisivat nopeimmat; halusin vain tietää kuinka suurella erolla. Olen aina luullut että eval-komento on niin raskas että sitä kannattaa välttää. Nyt ei näytäkään siltä?
« Viimeksi muokattu: 19.05.14 - klo:20.30 kirjoittanut AimoE »

nm

  • Käyttäjä
  • Viestejä: 16429
    • Profiili
Onko tässä testitavassa jotain mätää? Oletin ilman muuta että EvalPerl on hitain ja että CutSed ja TwoSed olisivat nopeimmat; halusin vain tietää kuinka suurella erolla. Olen aina luullut että eval-komento on niin raskas että sitä kannattaa välttää. Nyt ei näytäkään siltä?

Tässä tapauksessa ratkaisevaa näyttäisi olevan se, että testCutSed ja testTwoSed suorittavat ulkoisen ohjelman kahdesti, kun testEvalPerl ja TestEvalSed suorittavat sen vain kerran. TestTwoSed onkin melkein täsmälleen kaksi kertaa niin hidas kuin testEvalSed.

Muuttujien asetus tällä tavalla eval-komennon avulla on kuitenkin tietoturvariski, koska se mahdollistaa koodi-injektion. Tässä nimenomaisessa tapauksessa riski on vähäinen, kun liitospisteiden nimet ovat pääkäyttäjän hallinnassa, mutta yleisemmin tätä olisi syytä välttää.

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Olen jotenkin kuvitellut että eval on lähes samanveroinen kuin ulkoinen komento. Näköjään kannattaa silloin tällöin testata luulojansa.

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Kun testasin sen ekan perl-komennon vaihtoehtoja (eli mawk-komentoa jonka itse koodasin, ja shell-komentoja jotka poimin tääältä), niin tulos oli että mawk on nopein, perl hitain. Mutta kun vaihdoin dashin tilalle bashin, niin tilanne perlin ja shellin välillä tasoittui. Ulkoisten komentojen määrä ei kuitenkaan muuttunut; sisäisiä komentoja vain oli enemmän. Yksinkertaiset säännöt taitavat olla aika pettäviä.

Mistofelees

  • Käyttäjä
  • Viestejä: 661
    • Profiili
Olen vuosia tehnyt kaikki jutut perlillä,

Hih !
Itse käytän sekaisin Perliä ja Bashia tilanteen mukaan. Joskus ihmettelen, miksei ole tarjolla Basic-shelliä. Ennen VIC-20 -aikaa ensimmäiset ohjelmansa Forth:lla, ADA:lla ja Cobolilla kirjoittaneelle Basic oli mukava, koska kirjoitetut ohjelmat olivat helposti luettavia, kunhan ohjelmoija piti tiukasti suitsista kiinni eikä alkanut kikkailla.

Millä tahansa ohjelmointikielellä pystyy kirjoittamaan täysin lukukelvotonta koodia. Siihen ei tarvita GOTO -komentoa, regexp riittää. m=~/[\/abc]/./g tai jotain sinne päin.

Itse en anna arvoa scriptin nopeudelle, enkä siirrettävyydelle, koska useimmat scriptit, joita kirjoitan, ovat kertakäyttöisiä. Itselle on tärkeintä se, että koodi on itse-dokumentoivaa, eli luettavaa.
Luettavaa koodia on helpompi muokata ja ylläpitää.

AimoE

  • Käyttäjä
  • Viestejä: 2782
    • Profiili
Luettavuus tarkoittaa eri ihmisille eri asiaa. Joillekin se tarkoittaa 100% proseduraalista, joillekin sitä että mitättömän pientä juttua ei venytetä niin pitkäksi että koodia joutuu editorissa scrollaamaan sivukaupalla, ja joillekin sitä että noudatetaan tiettyä tyyliä vaikka se haittaisi lukemista.

Itse olen aloittanut opiskellessa pascalilla, työelämässä fortranilla ja cobolilla. Paras koulu oli kesätyö, jossa jouduin tekemään ison cobol-ohjelmiston ylläpitoa. Siinä hommassa opin kantapään kautta sen koodinlukutaidon jota Martin Fowler kuvaa kirjassa "Refactoring". Koodin kokonaisuuden hahmottamiseen tarvitaan editori, jolla koodiin voi koko ajan lukemisen aikana tehdä pieniä selvennyksiä, ja siihen kuuluu debuggaus ja testitapausten kirjoittaminen jokaisen pienen muutoksen jälkeen. Koodin lukeminen ilman editoria ja debuggeria ja toistuvia testitapausten ajoja on teeskentelyä, ja ohjelmistosuunnittelun taidon voi oppia vasta kun lukutaito on hanskassa.

Näin minä koodin luettavuuden ymmärrän, kielestä ja tyylistä riippumatta.

Mutta minusta on ihan terveellistä välillä pysähtyä tutkimaan myöskin sitä mikä vaikutus eri ratkaisutavoilla on suorituskykyyn, vaikka nopeus ei aina olekaan tärkein asia.