31. tammikuu 2010, 17:58

Vim-skriptaus on helppoa ja mukavaa

Vim on erityisen hieno editori, ja mikä siitä tekee jokseenkin erityisen hienon, on se seikka, että sitä on hyvin helppo hallita. Siis todella! Moni muuten kiva ohjelma jumiutuu siihen seikkaan, että niiden konfigurointi tai koodaaminen on todella vaivalloista (esimerkiksi vaikka rtorrent). Vim on samanaikaisesti sekä hyvin sutjakka ja monipuolinen että todella helposti säädettävä. Tämä on ilmiömäistä. Voisin sanoa, että samanlaista tasoa säädön helppoudessa tarjoilee joku Microsoft Word lähimpänä kumppanina.

Edellinen ei siis ollut sarkasmia, eikä siinä solvattu ketään. Sekä Word että Vim sattuvat molemmat olemaan hyvin monipuolisia ohjelmia ja samanaikaisesti helposti konfiguroitavissa. Ken ei ole Wordissa nauhoitellut ja viilaillut makroja ja sitten liittänyt niitä joko työkalupalkkiin tai pikanäppäimeen, ei ole kunnon tekstejä Wordilla tehnytkään. Onkin todella pahkeista, että Microsoft ei suostu linux-versiota porttaamaan siitä suurenmoisesta ohjelmasta. No, heidän häpeänsä.

Takaisin Vimiin. Suorastaan ylitsepursuavat määrät toimintoja näppäinhattujen päässä tietysti vaatii vähän aikaa sisäistämistä, mutta periaatteessa riittää, kun oppii käyttämään sisäänrakennettua ohjejärjestelmää, pääsee pitkälle ilman erityisempiä muistamisia.

Pari viikkoa sitten olisin voinut sanoa vim-skriptauksen helppoudesta vähän toisin sanoin, mutta kun havaitsin sieltä löytyvän kaksi sellaista komentoa, joiden avulla saa kirjoiteltua "makrot" sisään funktioiksi aivan helposti.

Makrot Vimissä

Tietysti on ensin selvitettävä, mitä tarkoittikaan termi "makro" Vimissä. Se tarkoittaa rekisteriin tallennettavaa komentosarjaa, joka ei varsinaisesti vastaa täysin tämän kirjoituksen asiasisältöä. Makrot ovat useimmiten kertakäyttöisiä, siis heitetään luonnostaan isoa tiedostoa varten joku lyhyt rimpsu, ja sitten suoritetaan sitä vaikkapa tiedoston jokaiselle riville. Makroja ei varsinaisesti taida olla suunniteltu tiedostoon tallennettavaksi ja sieltä ladattavaksi, mutta toki moinen operaatio onnistuukin.

Makron nauhoitus aloitetaan komentamalla q<rek>, missä <rek> on jokin aakkonen, rekisteri. Samoja rekistereitä muuten käytetään kopypasteiluunkin, mistä syystä makroja voi nauhoitella ja muokkailla nauhoituksen jälkeen. Nauhoitus lopetetaan komentamalla 'q'. Nauhoitetun makron voi suorittaa komentamalla @<rek>. Makron sisältöä voi tutkia komennolla :registers, tai sen voi liittää tekstiin tavalliseen tapaan (kuten muitakin rekistereitä) komentamalla "<rek>p. Lainausmerkki kuuluu komentoon mukaan. Esimerkiksi jos nauhoitan jotain pientä seikkailua rekisteriin a, tapahtuisi sen nauhoitus komennolla qa, toistaminen komennolla @a, tekstiin liittäminen komennolla "ap, sen takaisin kopioiminen rekisteriin "ayy (tai siirtäminen "add). Voi esimerkiksi pieniä mokia leikata pois. Rekisterin sisältö on täysin samanlaista kuin mitä kirjoittaisit näppäimistöltä sisään.

Makrot saa siirrettyä talteen em. mainittuja kopypastemenetelmiä hyväksikäyttäen, tai vaikkapa tallentamalla oman viminfo-tiedoston (kts. komennot :rviminfo ja :wviminfo).

No miten se skriptaus luonnistuu helposti?

Makrot olivat tämmöinen sivujuoni tässä tarinassamme, sillä en itse niitä ole paljoakaan käytellyt. Ehkäpä nokkela lukija saa kuitenkin osviittaa siitä, mitä tuleman pitää. En aio syvällistä teoriaa teille käydä läpi, sitä varten on hyvää kirjallisuutta olemassa. Käydään kuitenkin alussa hehkuttamani komentokaksikko läpi.

Vimskriptiä on monenlaista sorttia. Skriptailuksi voidaan kuvailla erilaiset komennot (set, abbr, map tai vaikkapa colorscheme). Varsinaiset komentorakenteetkin toki löytyvät, on silmukkaa, muuttujaa ja sanakirjarakennetta. Yksinkertaiset rakenteet eivät monesti suuria tarvitse. Pelkkä tuollainen makron laajentaminen omaksi funktiokseen on usein riittävää. Ja usein funktion kirjoittelu lähtee tarpeesta. Minulla oli sellainen tarve, että LaTeX-tiedostoa kirjoitettaessa olisi mukava saada avoinna olevat ympäristöt suljetuksi nopsakkaan, kun se muuten pitäisi tympeästi tehdä itse aina, jokainen kerta. Olenkin itse asiassa kirjoitellut varsin huomattavia määriä LaTeXia, ennen kuin huomasin, miten mukavaa olisi. Koodaus lähtee omista visioista! Muistakaa se. Jos te pystytte haaveilemaan jostain, te voitte sen vimillä tehdä. Tai koodaamalla muuten, jos haave on liian suuri. Mutta kyllä Vim yllättävän paljon kestää.

Siis tapauksena on saada aikaiseksi valmis näppäinyhdistelmä, jolla voin sulkea avoinna olevan LaTeX-ympäristön. Esimerkkikoodi näyttänee ongelman laadun, jos ko. kieli ei ole tuttua:

\begin{theorem}
   Olkoon $a$ se ja se, tämä tuota ja muuta kivaa:
   \begin{displaymath}
       \int_a^b f(x)g'(x) {\rm d}x = \ldots
   \end{displaymath}
\end{theorem}

Eli isoja lohkoja joudutaan sulkemaan hyvin vaivalloisin lopetustagein. Ne eroavat aloitustagista hyvin vähän. Minäkin kirjoittelin nuo tagit monasti käsin, kun kopypaste olisi sotkenut kirjoitus-flow'n kokonaan, mutta nyt viikonloppuna innostuin muuttamaan näppäinyhdistelmiä makroksi, ja makron funktioksi. Funktion voi sitten sitoa johonkin mukavaan näppäimeen. Itse laitoin yhdistelmään Ctrl-E. Vastaavanlainen lopetusviritelmä tekee gutaa myös HTML:ää ja erityisesti XML-kuvauksia kirjoitellessa, mutta se onkin jo tehty valmiiden Vim-kirjastojen toimesta. Se toimii, jos teillä on mukana tuorehko "omnicompletion" -paketti (X)HTML:lle.

Takaisin meidän ongelmaamme: voimme helposti havaita jo joitakin askelia, joita tulee ottaa kun kirjoitellaan automaatiota. Parhaassa tapauksessa riittää, kun löytää jonkun suoran menetelmän hakea oikea tagi. Mitenkä tehdä moinen? Tulee osata ajatella kuin kone. Otetaanpa tuo edellinen LaTeX-koodi nyt sellaisessa vaiheessa, kun pikanäppäimelle tulisi käyttöä:

\begin{theorem}
   Olkoon $a$ se ja se, tämä tuota ja muuta kivaa:
   \begin{displaymath}
       \int_a^b f(x)g'(x) {\rm d}x = \ldots
   ♦

Kursorimme on siis ruudun kohdalla kirjoitusvaiheessa. Oikeaan suuntaan oleva ajatus on, että etsimme taaksepäin ensimmäistä mahdollista ympäristönavausta, joka esiintyy hakusanalla \begin. Vim-haku "?\\begin" tekee kyllä näin. Aivan hyvä. Seuraava vaihe on kopioida se ylös. Tarvitsemme periaatteessa vain \beginiä seuraavan tekstin aaltosuluissa, mutta otamme kaiken ylös komentamalla "ayf}. Tässä komennossa yhdistyy kolme asiaa: rekisterimääre (ottaisimme mielellämme kopion rekisteriin 'a', emmekä oletusrekisteriin, koska se voi sotkea muita suunnitelmia), sitten kopiointikomento, ja lopuksi liikekomento. Komento f} tarkoittaa, että siirretään kursoria rivillä niin pitkälle, kunnes löytyy }-merkki. Tämä komento ei kuitenkaan pompi seuraavalle riville, joten tämä menetelmä on vaarassa epäonnistua pahasti, jos komento \begin ja sitä vastaava ympäristömääre ovat omilla riveillään.

Sitten pitäisi päästä takaisin aloituspaikkaan. Tämä onnistuu markkereilla, ja vieläpä siten, että ensin merkitään senhetkinen olinpaikka ylös komennolla m<rek>, ja sitten siihen voi hypätä komentamalla `<rek>. Liitämme sitten sen koodin tuohon tavallisella pastella: "ap. Lopuksi korvaamme sanan "begin" sanalla "end". Sen voi tehdä monella tavalla, yksi tapa on hypätä siihen hakukomennolla "?\\begin" ja antaa komentoa cwend (change word). Aivan lopuksi sujuvoittamaan kirjoitustyötä pomppaamme rivin loppuun.

Tiedän, tämä ei vaikuta helpolta, jos Vimin käyttö ei noin muuten ole tuttua. Mutta se onkin oikeastaan juuri siinä: jos käytät Vimiä, tunnet suurinpiirtein komentosuunnittelun takana piilevän filosofian (joka on jakaa toiminta ja liike erikseen: action and movement. Esimerkiksi "change" "word") ja osaat plärätä tarvittaessa helppejä, kaikki sujuu hienosti.

No, kohta tuhat sanaa tekstiä takana, emmekä ole vieläkään hehkuttamassani jutussa, eli normal-komennossa. Normal on sellainen komento, jolla saadaan Vim emuloimaan näppäinpainalluksia skriptikoodista käsin. Koska kaikki tuossa yllä on periaatteessa normaalimoodin komentoja, voitaisiin koko funktio kirjoittaa muotoon function Foo() normal normal normal … endfunction. Normal ei yksinään kaikkeen riitä, koska esimerkiksi näppäinpainallukset (vimissä emuloidaan niitä muodossa <C-N> tai esimerkiksi <ESC> tai <CR>) eivät välity oikein. Siksi komennolla execute voidaan kääriä normal merkkijonoksi, ja kaikki toimii sitten nätisti.

On vähän ilkeä nyt tässä vaiheessa huomauttaa, että kovin täydellistä skriptiä ei kuvailemallani tavalla saa aikaiseksi, mutta sentään jotenkin toimivan. Kuitenkin juuri tällä menetelmällä loin pohjan skriptilleni ja loput hain Vimin skriptausohjeista, josta löytyy pari valmista proseduuria helpottamaan detaljeissa. Katsellaan valmista koodia, ehkä huomaat, miten se vastaa edellistä kuvaustani:

function! CloseEnvironment()
   " searchpair flags b: search backwards,
   " W: don't wrap around the file, s: leave a mark here
   call searchpair('\\begin', '', '\\end', 'bWs')
   exe "normal \"pyf}"
   exe "normal ``"
   exe "normal \"pp"
   call search('\\begin', 'bW')
   exe "normal lcwend"
   exe "normal f}"
endfunction

Ah, kyllä. Kovin simppeli ja melkein täydellinen. Käyttäjäfunktioiden tulee alkaa isolla kirjaimella. Sinisellä merkkasin skripteissä helpottavat menetelmät ja muuten koodi on normal-komentojen sarjaa. Minusta tämä on todella mukava tapa koodailla, sillä todellakin kaikki intuitiiviset komennot siirtyvät makron tavoin koodiksi lähes sellaisenaan. Sitä voi sitten viilailla rauhassa kuten mielii, ja lopputuloksen voi sujauttaa vaikka .vimrc -tiedostoon, jos haluaa.

Analysoidaan hieman vielä muutoksia: ensimmäinen sininen komento searchpair() on suoraan suunniteltu ja tehty koodikieliä varten. Se nimittäin toimii kuten tavallinen search(), mutta osaa havaita sisäkkäiset rakenteet. Jos meillä on useita LaTeX-ympäristöjä sisäkkäin, ja vain viimeisin on sulkematta, osaa komento kyseisen proseduurin avulla tehdä asian oikeaoppisesti. Haulle annetaan siis kolme hakutermiä, joiden oletetaan olevan keskenään kimpassa. Tyypillisin tapaus on if..else..endif -rakenne. Meillä ei ole ympäristöissä mitään else-lohkoa, joten sen voi jättää tyhjäksi. Neljäs parametri antaa pari flagia, jotka kuvataan koodin kommenteissa. Esimerkiksi asetuksen "s" ansiosta emme aseta erikseen markkia aloituskohtaan. Ja muuten hyvä huomio voisi olla, että tämä proseduuri hyppää meidät löytökohtaan kuten tavallinen hakukin.

Sitten keltaisella merkityt komennot tekevät, kuten kuvailin jo: tässä versiossa käytetään rekisteriä "p apunamme. Komento " (kaksi takapilkkua) vastaa kuvailumme `a:ta. Liitämme löytämämme tuloksen lähtöpaikkaan. Seuraava proseduuri search() käytännössä vastaa normaalimoodin komentoja / ja ?, mutta koodina sen kai olettaisi toimivan luotettavammin, eikä sotke rekistereitäkään niin nopsaan.

Sitten lopuksi siirrymme yhden merkin verran oikealle ja vaihdamme sanan beginistä endiin. Viimoisena hyppäämme seuraavaksi löytyvään }-merkkiin, kuten kuvailimme. Funktio on valmis, se on melko täydellinen ja täysin sulava lyhyissä testauksissani.

Kun se sidotaan näppäinyhdistelmään, homma on valmis. Sitähän voisi käyttää myös ilman pikanäppäintä, mutta se olisi mielestäni kovin vaivalloista. Insert-moodin pikanäppäimenä komennon voi suorittaa ilman pomppaamista pois kirjoitusprosessista (vaikka se tekeekin sen salaa):

imap <C-e> <ESC>:call CloseEnvironment()<CR>a

Imap: insert mode mapping. <C-e>: control+e. Sitten seuraa näppäilysarja, jonka kuvaus antaa kirjoitettavan. Tulee ensin paluu insert-modesta normaalimodeen, kutsutaan ex-moodissa meidän funktiotamme, painetaan enteriä <CR> ja sitten palaamme takaisin insert-moodiin siten, että teksti soljuu sulavasti siitä eteenpäin. Mahtavaa.

Mutta periaatteessa nyrkkisääntönä voidaan ajatella, että normal-komennon käyttäminen ei ole hyvän ohjelmointitavan mukaista, koska se jättää liikaa arvausten varaan. Kuitenkin on syytä huomioida, että mitä kaikkea jouduttaisiinkaan tekemään ilman paria helpottavaa komentoa tuossa välissä. Joutuisimme pahassa tapauksessa tekemään koko hakuriville jotain merkkijonomagiaa ja muunnoksia. Silloin voisimme myös käyttää oikeita muuttujia rekisterien sijaan, mutta minusta nyt ei tarvita sellaista jämäkkyyttä. Yksi asia on kuitenkin varmaa: normal-komennolla korvataan ensisijaisesti aina valmiita ja "oikeaoppisia", sisäänrakennettuja prosedureja. Se vain olisi niin työlästä hakea kaikki, vaikka pääsimme esimerkissämmekin yllättävän hyvin alkuun. Search() on hyvä paikka aloittaa, ja ehkä jatkossa voisi ollakin triviaalia kirjoittaa vastaavan toiminnan suorittava funktio ilman yhtäkään "haitallista" normal-suoritusta.

Tageja: ,

---
---

---

Aiheen vierestä