Termios functions, the unix terminal canonical mode and other settings.

A few days ago I was rewriting in C an old piece of script originally coded in BASH. Although the code was pretty simple, I found something that was worth sharing.

Basically I had to get a confirmation from the user (y/n) – ie. read char from the standard input – and process it accordingly. However, rather than waiting for the use to press Return, I wanted to read the char as soon as typed. Besides that, I also wanted to have a timeout to unblock my program and assume a default answer after a given period of time.

In BASH that would be as simple as:

[code lang=”bash”]

read -t 5 -n 1 MYVAR

[/code]

In C this is not straightforward because, by default, UNIX terminals are line-oriented rather than byte-oriented. What does that mean? That means that your C program will only be aware of what was typed after the user presses <Enter>. And we still have the timeout issue. So how to do it?

The Canonical Mode

In Unix systems, the aforementioned line-oriented mode is called “The Canonical Mode”. This means that whatever you type is kept in an editable buffer managed by the terminal until you believe what you typed is right and press <Enter>. This is why you can use backspace, auto-complete, arrow keys, etc… And this is why getchar() or read() will block until <Enter> is pressed. :-/

Termios functions

Changing this behavior is easy with the termios set of functions, namely tcgetattr() and tcsetattr(), the documentation of which is available in man format – see termios(3).

These functions read or write terminal settings from or to struct termios arguments, these arguments (as explained in the same man page) have five components being that four bit masks and one integer array, each of these defining a group of related settings. In this post I focus on the ICANON and a few other related flags, however you can always read about the others in the man page.

Example 1 (Download it!):

[code lang="C"]
/*************************************************************************
 * Author: Eduardo M. Fleury - talkto (at) eduardofleury.com              *
 * Site  : http://eduardofleury.com                                      *
 * Desc. : Example 1 from post: http://blog.eduardofleury.com/?p=16      *
 *                                                                       *
 *************************************************************************
 *                                                                       *
 * This program is free software: you can redistribute it and/or modify  *
 * it under the terms of the GNU General Public License as published by  *
 * the Free Software Foundation, either version 3 of the License, or     *
 * (at your option) any later version.                                   *
 *                                                                       *
 * This program is distributed in the hope that it will be useful,       *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 * GNU General Public License for more details.                          *
 *                                                                       *
 * You should have received a copy of the GNU General Public License     *
 * along with this program.  If not, see . *
 *                                                                       *
 *************************************************************************/

#include 
#include 
#include 
#include 
#include 

#include 
#include 

int main() {

  int our_opt = 0;                   /* Our secret number */
  char user_opt = 0;                 /* User guess        */

  struct termios tios, orig_tios;    /* terminal settings */


  /* Seed random number gen */
  srandom(time(0));

  /* Get our number */
  our_opt = random() % 10;

  /**********************************************************************/
  /* Ok, that's what we're here for... Lets play with terminal settings */
  /**********************************************************************/

  /* Get current terminal settings */
  if (tcgetattr(0, &orig_tios)){
    printf("Error getting current terminal settingsn");
    return 3;
  }

  /* Copy that to "tios" and play with it */
  tios = orig_tios;

  /* We want to disable the canonical mode */
  tios.c_lflag &= ~ICANON;

  /* And make sure ECHO is enabled */
  tios.c_lflag |= ECHO;

  /* Apply our settings */
  if (tcsetattr(0, TCSANOW, &tios)){
    printf("Error applying terminal settingsn");
    return 3;
  }

  /* Check whether our settings were correctly applied */
  if (tcgetattr(0, &tios)){
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("Error while asserting terminal settingsn");
    return 3;
  }

  if ((tios.c_lflag & ICANON) || !(tios.c_lflag & ECHO)) {
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("Could not apply all terminal settingsn");
    return 3;
  }

  printf("Choose a number [0-9]: ");
  fflush(stdout);

  read(0, &user_opt, 1);
  user_opt -= 48;

  /* Restore terminal settings */
  tcsetattr(0, TCSANOW, &orig_tios);

  if (user_opt == our_opt){
    printf("nCool, you just won a helluva car!n");
    return 0;
  }

  printf("nThe right answer is %d. Take a walk, alright?n", our_opt);
  return 1;

}
[/code]

The code above should be straightforward to follow, note that we:

  1. Get the current terminal settings with tcgetattr()
  2. Disable the ICANON flag (Canonical mode) and enable the ECHO flag.
  3. Apply the modified termios struct using tcsetattr().
  4. Get the current terminal settings again and check it against the desired ones. This is important to do because tcsetattr() will report success whenever at least one setting was successfully changed, so, to make sure all of them where changed, we must manually assert it.
  5. Call read.
  6. Restore settings.

This works fine, read() will unblock as soon as the first char is typed. However we don’t have a timeout yet.

Timeout

In C you can use poll(2) to implement the timeout, this works fine and is efficient. In this example I could set Poll to wait for an event of type POLLIN on the file descriptor STDIN_FILENO and also set a timeout value so it would block until something is available for reading or the time is up. After poll exits, its return code will let we know whether we had a timeout or success. This can be seen below.

Example 2 (Download it!)

[code lang="C"]
/*************************************************************************
 * Author: Eduardo M. Fleury - talkto (at) eduardofleury.com             *
 * Site  : http://eduardofleury.com                                      *
 * Desc. : Example 2 from post: http://blog.eduardofleury.com/?p=16      *
 *                                                                       *
 *************************************************************************
 *                                                                       *
 * This program is free software: you can redistribute it and/or modify  *
 * it under the terms of the GNU General Public License as published by  *
 * the Free Software Foundation, either version 3 of the License, or     *
 * (at your option) any later version.                                   *
 *                                                                       *
 * This program is distributed in the hope that it will be useful,       *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 * GNU General Public License for more details.                          *
 *                                                                       *
 * You should have received a copy of the GNU General Public License     *
 * along with this program.  If not, see . *
 *                                                                       *
 *************************************************************************/

#include 
#include 
#include 
#include 
#include 

#include 
#include 

int main() {

  int our_opt = 0;                   /* Our secret number */
  char user_opt = 0;                 /* User guess        */

  struct pollfd pfd = {0,0,0};       /* poll() settings   */
  int pr;                            /* poll() result     */
  struct termios tios, orig_tios;    /* terminal settings */


  /* Seed random number gen */
  srandom(time(0));

  /* Get our number */
  our_opt = random() % 10;

  /* Get current terminal settings */
  if (tcgetattr(0, &orig_tios)){
    printf("Error getting current terminal settingsn");
    return 3;
  }

  /* Copy that to "tios" and play with it */
  tios = orig_tios;

  /* We want to disable the canonical mode */
  tios.c_lflag &= ~ICANON;

  /* And make sure ECHO is enabled */
  tios.c_lflag |= ECHO;

  /* Apply our settings */
  if (tcsetattr(0, TCSANOW, &tios)){
    printf("Error applying terminal settingsn");
    return 3;
  }

  /* Check whether our settings were correctly applied */
  if (tcgetattr(0, &tios)){
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("Error while asserting terminal settingsn");
    return 3;
  }

  if ((tios.c_lflag & ICANON) || !(tios.c_lflag & ECHO)) {
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("Could not apply all terminal settingsn");
    return 3;
  }


  printf("Choose a number [0-9]: ");
  fflush(stdout);

  /*************************************************************
   * This is new                                               *
   *************************************************************/

  /* Wait 5000 miliseconds for an POLLIN event on STDIN. */
  pfd.fd = STDIN_FILENO;
  pfd.events = POLLIN;
  pr = poll(&pfd, 1, 5000);

  if (pr > 0){
    /* We have got something to read */
    read(0, &user_opt, 1);
    user_opt -= 48;
  }
  else if (!pr) {
    /* We got a timeout */
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("nSorry, time is up.n");
    return 2;
  }
  else{
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("nPoll() error.n");
    return 3;
  }

  /*************************************************************/

  /* Restore terminal settings */
  tcsetattr(0, TCSANOW, &orig_tios);

  if (user_opt == our_opt){
    printf("nCool, you just won a helluva car!n");
    return 0;
  }

  printf("nThe right answer is %d. Take a walk, alright?n", our_opt);
  return 1;

}
[/code]

Timeout using only read()

Ok, poll() is nice but this post is not about it; so why not to implement a timeout using only read() and termios functions?

When ICANON is enabled the terminal has no difficulties to determine when the text input is ready to be sent to our application, just wait for an <Enter>. This task becomes more complicated when ICANON is disabled though. Should the terminal wait for more than one char before sending the text? Should it wait for a timeout?

Instead of a single answer for these questions, Linux lets the programmer tune the terminal to the best of his convenience, and this is what the variables c_cc[VMIN] and c_cc[VTIME] are for.

In a nutshell, if VMIN > 0, it determines the number of received chars upon which read() calls will return immediately. If it’s Zero than read() will block until one char is available (more on that in 1 minute).

At the same time, if VTIME > 0, it determines a timeout (in deciseconds) after which read() calls will return even if VMIN chars haven’t been read yet; Zero means no timeout. Cool, isn’t it?

What about the difference between VMIN = 0 and VMIN = 1? The difference is that in the former case the timeout counter (if applicable) will be started as soon as read() is called, which means that read() will return without any chars read after the timeout period; in the latter case however, the timeout is only enabled after the first char is typed and is reset when a new char arrives. Actually my understanding is that in terms of behavior, VMIN = 0 and VTIME = 0 are the same as VMIN = 1 and VTIME = anything. If you see a difference, please let me know.

So, our code would become:

Example 3 (Download it!)

[code lang="C"]
/*************************************************************************
 * Author: Eduardo M. Fleury - talkto (at) eduardofleury.com             *
 * Site  : http://eduardofleury.com                                      *
 * Desc. : Example 3 from post: http://blog.eduardofleury.com/?p=16      *
 *                                                                       *
 *************************************************************************
 *                                                                       *
 * This program is free software: you can redistribute it and/or modify  *
 * it under the terms of the GNU General Public License as published by  *
 * the Free Software Foundation, either version 3 of the License, or     *
 * (at your option) any later version.                                   *
 *                                                                       *
 * This program is distributed in the hope that it will be useful,       *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 * GNU General Public License for more details.                          *
 *                                                                       *
 * You should have received a copy of the GNU General Public License     *
 * along with this program.  If not, see . *
 *                                                                       *
 *************************************************************************/

#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

#define VMIN_VALUE 0
#define VTIME_VALUE 50

int main() {

  int our_opt = 0;                 /* Our secret number    */
  char user_opt = 0;               /* User guess           */

  int bytes_read = 0;              /* Number of bytes read */

  struct termios tios, orig_tios;  /* terminal settings    */


  /* Seed random number gen */
  srandom(time(0));

  /* Get our number */
  our_opt = random() % 10;

  /**********************************************************************/
  /* Ok, that's what we're here for... Lets play with terminal settings */
  /**********************************************************************/

  /* Get current terminal settings */
  if (tcgetattr(0, &orig_tios)){
    printf("Error getting current terminal settingsn");
    return 3;
  }

  /* Copy that to "tios" and play with it */
  tios = orig_tios;

  /* We want to disable the canonical mode */
  tios.c_lflag &= ~ICANON;

  /* And make sure ECHO is enabled */
  tios.c_lflag |= ECHO;

  /* Set timeout */
  tios.c_cc[VMIN] = VMIN_VALUE;
  tios.c_cc[VTIME] = VTIME_VALUE;

  /* Apply our settings */
  if (tcsetattr(0, TCSANOW, &tios)){
    printf("Error applying terminal settingsn");
    return 3;
  }

  /* Check whether our settings were correctly applied */
  if (tcgetattr(0, &tios)){
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("Error while asserting terminal settingsn");
    return 3;
  }

  if ( (tios.c_lflag & ICANON)         || !(tios.c_lflag & ECHO) ||
       (tios.c_cc[VMIN] != VMIN_VALUE) ||  (tios.c_cc[VTIME] != VTIME_VALUE) ) {
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("Could not apply all terminal settingsn");
    return 3;
  }


  printf("Choose a number [0-9]: ");
  fflush(stdout);

  bytes_read = read(0, &user_opt, 1);

  if (!bytes_read) {
    /* We got a timeout */
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("nSorry, time is up.n");
    return 2;
  }
  else if (bytes_read < 0){
    tcsetattr(0, TCSANOW, &orig_tios);
    printf("nRead() errorn");
    return 3;
  }

  /* Restore terminal settings */
  tcsetattr(0, TCSANOW, &orig_tios);

  user_opt -= 48;

  if (user_opt == our_opt){
    printf("nCool, you just won a helluva car!n");
    return 0;
  }

  printf("nThe right answer is %d. Take a walk, alright?n", our_opt);
  return 1;

}
[/code]

Note that we set c_cc[VMIN] = 0 so read() can unblock even if nothing was typed and finally we define the timeout period using c_cc[VTIME] to 50 deciseconds (5 seconds). Then we use the return value of read() to determine whether it had an error, timeout or success condition.

This approach requires the use of non-canonical mode so you cannot use this to set a timeout when the user is required to enter a full line and you want to let him edit it, case when you must use poll(). I hope this is interesting from the learning point of view though. It's always nice to understand how and why things work.

All examples in this page are released under the terms of GPLv3 and can be redistributed and modified under its terms.

Comments are always welcome and encouraged.

Take care.

2 thoughts on “Termios functions, the unix terminal canonical mode and other settings.

  1. Pingback: Linux Code and More » Blog Archive » Termios functions, the unix terminal canonical mode and other settings.

Leave a Reply