Smart commandline prompt

So in my work I’m developing a tool – Lodin. The program is a simple commandline tool taking an input and query file as parameters – with the small twist that if the query is not given, then the tool enters a prompt asking for the query. The naive way of reading the query in C++ is obviously to do

std::string s; 
std::getline(std::cin,s);

After which the line inputted by the user is contained in the string s. There is however a catch. The user cannot do any editing during inputting: If he makes a typo he has to delete the line to correct the typo (something that bothered me a lot everytime I had to show of the tool). The other day, I finally decided it was about time to fix this and make a proper query prompt.

Finding a library

My first instinct was to program this commandline editor from scratch but quickly realised this would be an overwhelming task. So, as any good software developer, I started searching for an of-the-shelf-library and I found not only one two: readline and editline.

Readline

Readline seemed at first promising; it seems reasonably well-documented and apparently used by most of the Linux utilities. It is, however, GPL’ed and I be forced to release my tool under GPL – I’m not against open source; but I don’t like GPL. So it was not an option.

Editline

Second up was editline. Editline attempts to be a replacement library for readline – and seems to have some kind of compatibility layer (but I haven’t investigated a lot). It uses a BSD-License which I like much. Editline was the winner.

First attempt

Editline is a nice library but its documentation is basically non-existant (one of the main reasons for me to write this post). Nevertheless, I figured out how to use it in the most basic way

const char* prompt (EditLine* ) {
  return "> ";
}

el = el_init ("ToolName",stdin,stdout,stderr);
el_set(el, EL_PROMPT, &prompt);
el_set(el, EL_EDITOR, "emacs");
int length;
const char* line = el_gets (el,&length);

Afterwards, line contains the string the user typed and length contains the length of the string. I think the code is mostly self-explanatory and will not dwell on the details.

The user is now capable of editing the line as he types. Nice.

Auto completion of a single keyword

After having this initial success I, naturally, wanted more. I wanted to have auto-completion of keywords when the user presses “tab”. The trick to make this work is to add a function to editline, and bind the tab-key on the keyboard to execute that function i.e. something along the lines of:

unsigned char complete(EditLine *el, int ch);
el_set(el, EL_ADDFN, "ed-complete", "Complete argument", complete);
el_set(el, EL_BIND, "^I", "ed-complete", nullptr);

First we declare the function complete in c, then we add that function to editline under the name ed-complete and finally we bind the tab-key to execute the function ed-complete. Brilliant, now we are just lacking the actual implementation of the complete function:

unsigned char complete(EditLine *el, int ch) {
  const char* ptr;
  const LineInfo *lf = el_line(el);
  const char* keyword = "Keyword";
  size_t len = 0;
  for (ptr=lf->cursor-1;
      ptr >= lf->buffer && (*ptr)!=' '; ptr--)
      {}
  len = lf->cursor - ++ptr;
  if (strncmp (keyword,ptr,len) == 0) {
    el_insertstr(el,&keyword[len]);
    el_insertstr(el," ");
    return CC_REFRESH
  }
  return CC_ERROR;
}

We use el to obtain the LineInfo structure that contains information about the current line typed in. Then we iterate backwards from the cursor of the LineInfo to find the the start of the line (or the first space). After this iteration the ptr is positioned at the last typed word on the line which we then matches against the keyword with strncmp. If they match, then we insert the remaining part of the keyword with el_insertstr(el,&keyword[len]); and insert a trailing space as well with el_insertstr(el," ");. Finally, we return CC_REFRESH to tell editline that we actually updated its line buffer.

Autocompletion of several keywords

Doing auto-completion of several keywords is not more difficult than the above: one should have a list of keywords and match the last word typed in against these. If only one of them match we can extend the current typed word with the ending of the keyword, if several of the keywords match we can extend the currently typed word with their common prefix. Very simple.