Sat Aug 18 2018

GNU Readline for Tab autocomplete and Bash like History

GNU Readline is an easy-to-use library for autocompleting using Tab key and bash like history using up/down array keys for interactive programs with command line interface.

In this post, we will build an interactive program in C++ that uses GNU Readline library. The program will take input from the user a number name and output the corresponding number. For, e.g. if the user enters one, the program will output 1. The program will prompt for input again and again until the user quits by pressing Ctrl+D.

Download GNU Readline

First, download and install the GNU Readline library from here. Ubuntu users can easily download from apt using the following command.

$ sudo apt install libreadline-dev

Using GNU Readline

The complete example is given at the end of the post.

First, include the Readline header files that will be used by the program.

#include <readline/history.h>
#include <readline/readline.h>

In the main function of the program, set the pointer rl_attempted_completion_function to the function that will generate possible matches corresponding to the partial input entered by the user. This pointer is declared in the Readline header file which will be called by other functions.

int main() {
  rl_attempted_completion_function = command_completion;
  return 0;
}

Define the command_completion function. It makes a call to the rl_completion_matches function which calls a command_generator function (which will be defined later) repeatedly until the command_generator function returns a null pointer. The command_generator function returns a match one at a time. The rl_completion_matches function returns the array of all those matches.

char **command_completion(const char *text, int start, int end) {
  rl_attempted_completion_over = 1;
  return rl_completion_matches(text, command_generator);
}

Now, define the commad_generator function. The command_generator function takes the partial input entered by the user and a state as arguments. The state is zero the first time the command_generator function is called for a partial input and non-zero otherwise. In our example, we have a static variable match_index which is reset to zero when the state is zero and incremented when the state is non-zero.

We also have static variable matches containing all the matches generated when the function was called the first time on the partial input. In subsequent calls, the string whose index is match_index in matches vector is returned. The matches are generated by comparing the partial input with the words in the vocabulary.

std::vector<std::string> vocabulory{"zero", "one", "two",   "three", "four",
                                    "five", "six", "seven", "eight", "nine"};

char *command_generator(const char *text, int state) {
  static std::vector<std::string> matches;
  static size_t match_index = 0;

  if (state == 0) {
    matches.clear();
    match_index = 0;

    std::string textstr(text);
    for (auto word : vocabulory) {
      if (word.size() >= textstr.size() &&
          word.compare(0, textstr.size(), textstr) == 0) {
        matches.push_back(word);
      }
    }
  }

  if (match_index >= matches.size()) {
    return nullptr;
  } else {
    return strdup(matches[match_index++].c_str());
  }
}

At last, complete the main function. The main function calls the readline function which takes input as the prompt to be displayed on the stdout. The readline returns a null pointer if Ctrl+D was entered and the main function exits.

The add_history function adds the command to the history buffer which will be accessed by the user using up/down arrow keys similar to bash.

int main() {
  rl_attempted_completion_function = command_completion;

  char *buf;
  std::string cmd;

  while ((buf = readline("> ")) != nullptr) {
    cmd = std::string(buf);
    if (cmd.size() > 0) {
      add_history(buf);
    }
    free(buf);
    std::stringstream scmd(cmd);
    scmd >> cmd;

    std::vector<std::string>::iterator itr =
        std::find(vocabulory.begin(), vocabulory.end(), cmd);
    if (itr != vocabulory.end()) {
      std::cout << cmd << ": " << std::distance(vocabulory.begin(), itr)
                << std::endl;
    } else {
      std::cout << "Invalid number name" << std::endl;
    }
  }

  std::cout << std::endl;
  return 0;
}

A call to rl_bind_key('\t', rl_insert); in the main function will disable the tab completion feature. In the complete example below, we call this function if the user has specified -d flag as command line argument.

Complete example

#include <algorithm>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include <readline/history.h>
#include <readline/readline.h>

std::vector<std::string> vocabulory{"zero", "one", "two",   "three", "four",
                                    "five", "six", "seven", "eight", "nine"};

char *command_generator(const char *text, int state) {
  static std::vector<std::string> matches;
  static size_t match_index = 0;

  if (state == 0) {
    matches.clear();
    match_index = 0;

    std::string textstr(text);
    for (auto word : vocabulory) {
      if (word.size() >= textstr.size() &&
          word.compare(0, textstr.size(), textstr) == 0) {
        matches.push_back(word);
      }
    }
  }

  if (match_index >= matches.size()) {
    return nullptr;
  } else {
    return strdup(matches[match_index++].c_str());
  }
}

char **command_completion(const char *text, int start, int end) {
  rl_attempted_completion_over = 1;
  return rl_completion_matches(text, command_generator);
}

int main(int argc, char **argv) {
  if (argc > 1 && std::string(argv[1]) == "-d") {
    rl_bind_key('\t', rl_insert);
  }
  rl_attempted_completion_function = command_completion;

  char *buf;
  std::string cmd;

  while ((buf = readline("> ")) != nullptr) {
    cmd = std::string(buf);
    if (cmd.size() > 0) {
      add_history(buf);
    }
    free(buf);
    std::stringstream scmd(cmd);
    scmd >> cmd;

    std::vector<std::string>::iterator itr =
        std::find(vocabulory.begin(), vocabulory.end(), cmd);
    if (itr != vocabulory.end()) {
      std::cout << cmd << ": " << std::distance(vocabulory.begin(), itr)
                << std::endl;
    } else {
      std::cout << "Invalid number name" << std::endl;
    }
  }

  std::cout << std::endl;
  return 0;
}

Compile the above program with -lreadline flag to link the program with Readline.

$ g++ -o program file.cpp -lreadline

Run using the below command.

$ ./program

References to read more about GNU Readline