24. marraskuu 2009, 19:32

GNU Readline

GNU Readline on ohjelma, jonka yksinkertainen tehtävä on kysyä käyttäjältä syötettä, kenties pienen kysymyksen kera. Ja sen se yrittää tehdä niin hyvin kuin suinkin osaa. Ja mielestäni erittäin hyvin se suoriutuu hommistaan. Ohjelmaa kehittelevä Chet Ramey on myös bashin isiä. Ajattelisin, jos vaikka kertoilisin vähän ohjelmasta, sen käytöstä ja koodaamisestakin voisi turista.

Mikäs tämmöisen simppelin ohjelman idea sitten on? Sen sijaan, että se yksinkertaisesti käärisi getline()-kutsun omaksi ohjelmakseen, se tukee valtavia määriä erilaisia hyödyllisiä näppäinkomentoja (olkoonkin Emacsista peräisin) ja kaikkea maan ja taivaan väliltä: bash yksinomaan hyödyntää vain readline-toimintoja komentorivillään, ja se kertoo osaltaan potentiaalista. On täydennystä, historiaa, hakuja takaa ja edestä, kopypaste, sanojen välillä hyppimistä. Nämä kaikki toiminnot on helppo saada omaankin CLI-ohjelmaan vain muutamalla kikkailulla ja käärimällä tarpeeksi toiminnallisuutta simppeleiksi funktioiksi; C:n ja C++:n tapauksessa kannattaa vähintään tehdä oma funktio sille, ja mieluummin ehkä luokka. PHP tukee tätä varsin sulavasti, kunhan kyseinen integraatio on käännetty mukaan.

Moni *NIX-käyttäjä varmasti tuntee monia ohjelmia, jotka käyttävät Readlineä, vaan eivät tiedä ohjelmien nimiä. Erilaiset shellit ovat etunenässä tämmöisiä. Toisaalta nimet, kuten vi, toimivat omien virittelyjensä päällä. Wikipediassa mainitaan, että Readlinen integrointi omiin sovelluksiin olisi työlästä, ja toisaalta sen GPL-lisensointi karkottaa muita toimijoita, joten en osaa kyllä heittää järkevää arvausta Readlinen levinneisyydestä ja käytöstä. Integroinnin työläydestä en tiedä, tein pari kokeilusoftaa, ja helpoksi minä tuota enimmälti sanoisin.

Käyttö

Koska jokainen tehobashisti epäilemättä hallitsee komentorivinsä lisäksi komentorivillä liikkumisen salat hyvin, käyn vain lyhyesti muutamia helpottavia asioita täältä sun sieltä. Ja eikä edes niitäkään tarvitse käydä, täällä katetaan hyvien selostusten kanssa kaikki tarpeellinen.

Sen sijaan voin kertoa, mitä itse käyttelin kovasti, ennen kuin löysin readlinen kaikki piilotetut erikoisuudet. Ctrl-L on kovassa käytössä: se siis tyhjentää ruudun. Vastaa siis clear-komentoa, mutta komentoriviä ei tarvitse tyhjentää sen vuoksi. Lisäksi käyttelen paljon ehkä Windows-ajoilta tuttuja hyppelykäskyjä, eli Ctrl-nuolinäppäimet, jotka vastaavat sanojen välillä pomppimista. Annetut esimerkit, C-a, C-e, M-f ja M-b ovat ihan jees ratkaisut sille, kellä kyseiset kombinaatiot toimivat. Copypastetoiminnot olivat hämärässä minullakin, niistä löytyy tuolta osiosta 1.2.3. Olen paljon käytellyt historiahaussa PGup ja -down -komentoja siten, että kirjoitan rivin alkua, ja sitten PGup polkee kyseisillä merkeillä alkavia rivejä. (1.2.5 kuvailee näiden komentojen "oikeiksi" näppäinyhdistelmiksi C-r ja C-s.) Hyvin näppärää esimerkiksi g++- ja fg-rivien seassa; kun ylänuoli on vain liian työläs painaa kahdesti.

Jatketaan koodauksella.

Koodaus

PHP

Ensimmäinen paikka, jossa Readlineä piruuttani kokeilin käyttää, oli PHP-CLI -sovellus, kun en tiennyt muitakaan välineitä riviltälukemiseen. Koodi on petollisen simppeliä:

$prompt = "What's your name? ";
$input  = readline($prompt);    // ja näin se sujuu PHP:ssä

Jos halutaan lisäillä historiaa, tai sen sellaista mukaan, PHP:n dokumentaatio auttaa. Esimerkiksi tiettyjen avainrivien lisäily siten, että niitä voi täydennellä tabilla (siis tyhjälle riville) on helpohko lisätä apufunktion avulla (ja tätä apufunktiota voi tietenkin täydentää antamaan mitä tahansa kontekstiriippuvaisestikin):

public static function readlinetaydennys($str, $i) {
  return array("tässä on",
     "lista erilaisia",
	 "täydennettäviä",
	 "rivejä");
}
// ... myöhemmin alustuksessa
readline_completion_function('readlinetaydennys');

Ja tämä PHP-ohjelma readlineä kutsuessaan voi täydentää annettuihin vaihtoehtoihin. Esimerkkifunktio on kovin staattinen, mutta mikään ei pakota sitä olemaan. Mielestäni jo yksin tämä Readline-integraatio toteutettuna näppärästi PHP:hen innostaa kokeilemaan PHP:n CLI-ominaisuuksia skriptiohjelmissa. Ei kotisivumoottoria tarvitse nettiin jättää. Tosin PHP:n muut ongelmat tekevät siitä tympeän muuhun käyttöön.

C++

Pitäähän omalla suosikillanikin kokeilla tätä. Tässä tulee opeteltua samaa tahtia kun kirjoitan tuotosta. Oppaat sanovat, että Readline varaa malloc():lla merkkijonolle tilaa, ja se pitää itse vapauttaa tarpeen tullessa. Usein keosta nappaaminen ei ole tarpeen, tai ainakaan ei pitäisi jättää sitä ohjelmoijan harteille, joten tehdään jotain asialle C++-toteutuksessamme.

/* Testailen GNU Readlinen integroimista C++-ohjelmaan */
#include <iostream>
#include <string>
#include <cstdlib> // free()
namespace std {
    #include <readline/readline.h>
}

/* C++-tyyliä kunnioittaen vain varmat ratkaisut. */
std::string readline(std::string prompt) {
    char* answer_c = std::readline(prompt.c_str());

    // Kopioidaan sisältö
    std::string answer_cpp (answer_c);
    std::free(answer_c);
    return answer_cpp;
}

int main() {
    std::string foo = readline("Hei! ");
    std::cout << foo << ' ' << foo << std::endl;
    return 0;
}

Kuten aavistelin, käyttö on melko selkeätä. Readlinen kirjastot saa mukaan -lreadline -pätkällä. Kuten PHP-pätkässäkin oli, kokeillaan lisätä myös tuota tabitäydennystä sekaan. Se näyttääkin jo enemmän purkalta johtuen kielten eroista. Readline vähän rumasti käyttelee globaaleja muuttujia, vaan mitenkäs muuten se kykenisi C:n kanssa toimimaan? Esitän muutokset edelläkäytyyn ohjelmaan. Tämä onkin jo vaativampi, ja monta asiaa pitää tehdä itse.

Esimerkiksi ei-kovin-c++:maisesti on tehty merkkijonojen pyörittely. Ellei ole keosta haalittu merkkijonoille tilaa, readlinessä tehtävä free()-kutsu aiheuttaa segfaultin. C++:ssa kun ei niin ole tarvetta aina ottaa dynaamista muistia, kun on std::string ja vektorit. Jouduin ottamaan mallia eräästä hyvästä lähteestä.

Noniin, lisäkoodia: nämä funktiot ovat uusia:

static char** taydennys(const char* str, int begin, int end) {
    return std::rl_completion_matches(const_cast<char*>(str), &gen);
}

// Generaattorimme tutkii, milloin on oikean rivin alkua täydennelty.
char *gen(const char* str, int state) {
    static int length = 0, index;
    char* name;

    // Olemmeko alussa?
    if(state == 0) {
        index = 0;
        length = std::strlen(str);
    }

    // Etsimme kunkin komennon kohdalta, täsmääkö rivin alku mihinkään.
    // Jos tekee, varaamme muistia ja palautamme readlinelle
    while(name = const_cast<char*>(cmdlist[index].c_str())){
        index++;
        if(std::strncmp(name, str, length) == 0) {
            char * rumajuttu = static_cast<char*>(malloc(std::strlen(name) + 1));
            if(rumajuttu != 0) {
                std::strcpy(rumajuttu, name);
                return rumajuttu;
            }
        }
    }
}

Tämä globaali komentolista voisi olla erittäin hyvin myös funktionsa suojissa, mutta olkoon nyt tässä. Kaiken tilpehöörin keskellä kannattaa harkita käärimistä luokaksi. Seurasin tiiviisti opasta, ja samalla tuli myös itkettävän huonoa koodia (siis C-mäistä koodia).

std::string cmdlist[] = {
    "First",
    "tayd2", "kolmas taydennys", "neljas vaihtoehto"};

Ja lopuksi pääohjelman alkuun tarvitaan kutsu liittää täydennysfunktio readlineen.

    // Kytketään täydennysfunktiomme readlineen.
    std::rl_attempted_completion_function = taydennys;

Koodista tuli nopeasti puuroa, ja const_cast:n käytöstä en ole ylpeä. Nähtävästi näin pitää kuitenkin toistaiseksi tehdä. Järkevämpää olisikin ollut oikeastaan etsiä C++-wrapperia tälle kirjastolle. Nyt tuli kuitenkin tehtyä tämäkin. Valgrindillä ajelin ympäriinsä, ei näytä vuotavan muistia (kumpikaan tapaus). Toivottavasti jollekulle on iloa tästä esimerkkikoodista. Hyvä malli on tuo lähteenä käytetty artikkeli ylempänä.

Virittely

No, nyt päästään tähän päivän varsinaiseen aiheeseen, eli kuulin, että Readlinen näppäinbindaukset voi muuttaa vastaamaan vi:tä! Tämä on riemastuttava ajatus, vaikka en usko ottavani sitä käyttöön (kuka tietää?)

Readline lukee asetuksia /etc/inputrc:stä ja vaihtoehtoisesti lisäksi käyttäjän kotihakemistosta, tosin tämä meni mutuiluksi. Readlinen dokumentaatiossa kuvaillaan tätä asetustiedostoa hyvin (ja mutuiluni sai faktaperää, jippii). En olekaan ennen ajatellutkaan, että readlinen näppäinbindit ovat todellakin varsin emacsmaiset. Sinänsä en koe asiaa ongelmaksi, sillä emacsin näppäimet ovat ihan kivat kyllä. Pitkissä hommissa modulaarinen asettelu on vain vähän mukavampi. (Tämänkin artikkelin kirjoittelin gvimillä.)

Vi-näppäimiin päästään nopeasti ja lupsakasti käsiksi lisäämällä vaikka sinne ~/.inputrc-tiedostoon rivi set editing-mode vi. Tämä onkin ensimmäinen kerta, kun itsekin koittelen sitä. Selvästi toimii! Esimerkiksi kaikki liikekomennot ovat nätisti toiminnassa, ja erityisesti bash-koodaajia voi kiinnostaa komento `v', jolla readline avaa vi-instanssin (tosin nykydistroilla aina vimin) ja komentonsa saa rauhassa naputella. Moniriviset for-loopit on helppo kirjoitella ylös!

Lisäksi edellisiä komentoja voi selailla luontevasti j- ja k-näppäimillä, kuten vi:ssä muutenkin selaillaan rivejä. Monia asioita pitää vain tosin etsiskellä, sillä olen tottunut readlinen käyttöön noiden erikoiskikkailujen kanssa: ^L toimii toki edelleen ruudun tyhjentämisessä, mutta nyt se pitää prefiksata escillä, jotta oltaisiin normaalimoodissa. Joitain asioita pitäisi etsiä, mutta hyvältä näyttää. Ja vaikka vi-mode ei kiinnostaisikaan, on dokumenteissa hyvät ohjeet uusien näppäinbindausten tekoon. Emacsin lähestymistapa on otettu kokonaan käyttöön, joten kustomoitavuutta on helppo saada aikaan. Saattaa olla, että jätän käyttöön, saattaa olla, että en. Joku indikaatio siitä, ollaanko lisäys- vai normaalimoodissa olisi paikallaan ainakin alustavasti.

Tageja: , , ,

---
---

---

Aiheen vierestä