Komentorivillä annettavien optioiden käsittely

Komentorivillä tomivien ohjelmien toimintaan voi yleensä vaikuttaa optioilla. Esim. ls -l, ls-komennon suorittaminen optiolla l tulostaa hakemiston sisällön pitkässä muodossa. Komentorivioptiot ovat yleensä yksimerkkisiä, esim. ls:llä l ja annetaan ohjelmille muodossa -l. Jos ohjelmalle annetaan useampia optioita, voidaan ne antaa erikseen ls -l -a tai yhdistettynä ls -la. Optiot voidaan antaa missä järjestyksessä tahansa ja välissä saa olla normaaleja parametrejä.

getopt

Linuxissa löytyy valmis funktio getopt, jonka avulla on helppo selvittää ohjelmalle annetut komentorivioptiot. Funktion esittely:
NAME
       getopt - Parse command line options

SYNOPSIS
       #include < unistd.h>

       int getopt(int argc, char * const argv[],
                  const char *optstring);

       extern char *optarg;
       extern int optind, opterr, optopt;
Komennolle annetaan parametrina mainin saamat argc- ja argv-muuttujat sekä merkkijono, joka kertoo mitä komentorivioptioita ohjelma voi saada. getoptin käyttö vaikuttaa myös automaattisesti ohjelmaan mukaan liitettäviin globaaleihin muuttujiin optarg, optind, opterr ja optopt. Seuraavassa ote ohjelmasta 3-1.c:
  int optio;

  while( 1 ){
    optio = getopt(argc, argv, ":abc"); 
    if ( optio == -1 ) break;

    switch ( optio ){
    case 'a':
      printf(" optio a\n");
      break;
    case 'b':
      printf(" optio b\n");
      break;
    case 'c':
      printf(" optio c\n");
      break;
    case '?':
      printf(" tuntematon optio %c \n", optopt);
      break;
    }
  }
Optioiden käsittely kannattaa suorittaa toistuvasti getoptia kutsuvassa while-loopissa. getoptin kolmantena parametrinä on ":abc", joka tarkoittaa sitä, että ohjelman mahdolliset optiot ovat a, b ja c. Alussa oleva : laittaa getoptin toimimaan siten, että se ei tulosta automaattisesti virheilmoituksia.

getopt toimii siten, että se skannaa läpi ohjelman komentoriviparametrit ja etsii niiden joukosta halutut optiot. getopt palauttaa arvonaan -1 siinä vaiheessa kun kaikki optiot on etsitty tai jos optioita ei ole. Jos löytyy tuntematon optio, on paluuarvo '?', muuten paluuarvo on löytynyt optio.

Jos löydetään tuntematon optio, saa globaali muuttuja optopt arvokseen tuntemattoman option. Yllä oleva ohjelmanpätkä printtaa virheilmoituksena tuntemattoman option käyttäen optopt-muuttujaa.

Optiot saa syöttää ohjelmalle missä järjestyksessä tahansa, ja väliin saa sijoittaa muita komentoriviparametrejä. getopt toimii joka tapauksessa

Muut komentoriviparametrit

Kun getopt palauttaa -1, se on järjestellyt ohjelman komentoriviparametrit siten, että normaali parametrit (eli ne jotka eivät ala -:lla) on siirretty komentoriviparametritaulukon argv loppuun alkaen kohdasta jonka kertoo globaali muuttuja optind.

Seuraavassa tilanne, missä annettu komento ls testi -l -a sekä heti ohjelman käynnistyessä, että getopt-loopin suorituksen jälkeen.

Seuraavassa ohjelman 3-1.c loppuosa, joka tulostaa optioiden käsittelyn jälkeen ohjelman muut komentoriviparametrit.

 
  if ( optind < argc){
    printf("\nloput komentoriviparametrit:\n");
    int i;
    for ( i=optind; i < argc; i++)
      printf("% s\n",argv[i]);
  }
  else {
    printf("\nei komentoriviparametrejä\n");
  }
Ohjelman on yleensä muistettava mitä optioita sille on annettu. Yksi keino hoitaa optioiden muistaminen on yksinkertaisten muuttujien käyttö siten, että esim. muuttujan aflag arvo on 1 jos optio a on annettu ja muuten aflag:n arvo on 0. Ohjelma 3-2.c demonstroi flagien asettamista.

Ohjelman "normaalia laskentaa" ei koskaan kannata suorittaa getopt-loopissa. Eli järkevintä on toimia siten, että getopt-loopissa kerätään tieto siitä mitä optioita ohjelmalla on. Tämän jälkeen vasta aloitetaan ohjelman normaali toiminta.

Optioiden argumentit

Optioon voi liittyä myös argumentti. Esim. jos halutaan kirjautua ssh-komennolla käyttäen jotain muuta käyttäjätunnusta kuin nykyistä, uusi tunnus annetaan optiota -l käyttäen, esim:
[luuma@telinux1 vko3]$ ssh 192.168.181.18 -l mluukkai
mluukkai@192.168.181.18's password:
Option ja argumentin välissä ei ole pakko olla välilyöntiä.

Myös argumentillisten optioiden tunnistaminen hoituu getioptilla. Ohjelmalla 3-3.c voi olla optiot a ja b. Optiolla b on argumentti. Ote optiot käsittelevästä koodista:

  char *b_arg;		// osoitin b option argumenttiin

  while( optio = getopt(argc, argv, ":ab:")  ){
    if ( optio == -1 ) break;   // ei enää optioita, jatketaan 
    switch ( optio ){
    case 'b':
      bflag = 1;                // optarg osoittaa argumenttiin
      b_arg = optarg;           // otetaan osoitin talteen
      break;
    case ':':                   // option argumentti puuttuu
      printf(" optiolla %c on oltava argumentti\n", optopt);
      break;
    ...
optargin kolmas parametri on muotoa ":ab:", kaksoispiste option b jälkeen kertoo, että b:llä on argumentti. getopt:in palautaessa b, globaali muuttuja obtarg osoittaa option argumenttia (eli käytännössä jotain merkkijonoista argv[1], argv[2], ... ). Osoitin argumenttiin otetaan talteen b:n case-haarassa. Jos b annetaan ilman argumenttia, palauttaa optget arvon ':', tälle tehdään oma case-haara joka tulostaa virheilmoituksen.

Terminaalin toimintamoodit

Oletusarvoisesti terminaali toimii siten, että käyttäjän syöte lähtee ohjelmalle esim. komennon gets yhteydessä vasta kun käyttäjä painaa enter. Tätä terminaalin oletustoimintamoodia, missä syöte käsitellään riveittän, sanotaan kanooniseksi moodiksi. Oletustoimintana terminaalissa on myös se, että kaikki näppäimistöltä kirjoitettu syöte kaiutetaan ruudulle.

Terminaalin toimintaan voi vaikuttaa käyttämällä termios.h-kirjaston tarjoamia funktioita, meille näistä riittävät seuraavat:

  #include < termios.h>
  #include < unistd.h>
  
  int tcgetattr(int fd, struct termios *termios_p);

  int tcsetattr(int fd, int optional_actions, struct termios *termios_p);
Funktiot käyttävät tietuetta struct termios. Tietueella on useita kenttiä, mutta yleensä kentät c_lflags ja c_cc riittävät. struct termios on siis määritelty suunnileen seuraavasti:
struct termios{
   tcflag_t c_lflag;      
   cc_t c_cc[NCCS];
   ...			// jotain muita kenttiä
};       
Ohjelma 3-4.c ottaa hetkeksi merkkien ruudulle kaiuttamisen pois päältä. Ote ohjelmakoodista:
int main(){
  struct termios vanha, uusi;
  char buf[80];

  // otetaan terminaalin toimintamoodin tiedot muuttujaan vanha
  // fileno(stdin) tarkoittaa standardisyötettä eli näppäimistöä        
  // vastaavaa tiedostokuvaajaa      
  tcgetattr( fileno(stdin), &vanha );

  // kopioidaan oletusasetukset muuttujaan uusi
  uusi = vanha;

  // kytketään kirjoitettujen merkkien kaiutus pois päältä
  uusi.c_lflag = uusi.c_lflag & ~ECHO;

  // asetetaan terminaalille uusi toimintamoodi
  tcsetattr( fileno(stdin), TCSANOW, &uusi );
  printf("anna syöte: ");
  fgets(buf, 80, stdin);
  printf("\nkirjoitit: %s", buf);

  // palautetaan vanha toimintamoodi
  tcsetattr( fileno(stdin), TCSANOW, &vanha );
struct termios:in c_lflag-kenttä koostuu joukosta terminaalin toimintamoodia ohjaavista flageja jotka joko ovat päällä tai poissa. Ohjelmassa otetaan ECHO-flagi pois päältä. Tämä tapahtuu tekemällä and-operaatio (eli &) flagien arvolle sekä ECHO-flagin negaatiolle ~ECHO:
  uusi.c_lflag = uusi.c_lflag & ~ECHO;
Yleisemmin, flagin XXX saa päälle tekemällä or-operaation (eli |):
  uusi.c_lflag = uusi.c_lflag | XXX;
ja pois päältä and-operaatiolla ja negaatiolla:
  uusi.c_lflag = uusi.c_lflag & ~XXX;
Ohjelma 3-5.c ottaa kaiutuksen pois ja siirtyy merkeittäin tapahtuvaan terminaalin käsittelyyn. Ote ohjelmasta:
  // otetaan terminaalin toimintamoodin tiedot muuttujaan vanha
  tcgetattr( fileno(stdin), &vanha );

  // kytketään kirjoitettujen merkkien kaiutus pois päältä
  uusi.c_lflag = uusi.c_lflag & ~ECHO;

  // lopetetaan syötteen käsittely riveittäin
  uusi.c_lflag = uusi.c_lflag & ~ICANON;
 
  // luetaan merkkejä yksitellen 
  uusi.c_lflag = uusi.c_cc[VMIN] = 1;
  // odotetaan jokaista merkkiä niin kauan kunnes merkki syötetty
  uusi.c_lflag = uusi.c_cc[VTIME] = 0;
  // VTIME-arvo tarkoittaa aikaa, miten kauan yhtä merkkiä odotetaan
  // arvo 0 tarkoittaa, että merkkiä odotetaan niin kauan kunnes merkki on syötetty

  // asetetaan terminaalille uusi toimintamoodi
  tcsetattr( fileno(stdin), TCSANOW, &uusi );

  printf("paina jotain, niin aloitetaan\n");
  getchar();

Ohjelma aloittaa asettamalla uuden terminaalimoodin, jossa kaiutus ja ICANON 
ovat poissa päältä. ICANON tarkoittaa riveittäistä käsittelyä.

Ohjelma lukee yhden merkin käyttäjältä käyttäen getchar-funktiota.
Tämän jälkeen ohjelma menee loopiin, jossa luetaan merkkejä kunnes 
annetaan enter:
i = 0; printf("syötä tekstiä, lopetus enter\n"); while( 1 ){ c = getchar(); if ( c == '\n' ) break; buf[i] = c; i++; } buf[i] = '\0'; printf("syöte oli: %s\n", buf);
Syötettyjen merkkien tallentaminen on huolehdittava nyt itse, eli syötetyt merkit kootaan char-taulukkoon buf, johon lopuksi laitetaan \0-merkki merkkijonon päättymisen merkiksi.