Changing tab-completion for read builtin in bash

2020-03-26 05:34发布

问题:

The current tab-completion while "read -e" is active in bash seems to be only matching filenames:

read -e
[[TabTab]]
abc.txt  bcd.txt  cde.txt  

I want the completion to be a set of strings defined by me, while file/dir/hostname-completion etc. should be deactivated for the duration of "read -e".

Outside of a script

complete -W 'string1 string2 string3' -E

works well, but i cant get this kind of completion to work inside a script while using "read -e".

回答1:

Although it seems like a reasonable request, I don't believe that is possible.

The existing implementation of the read builtin sets the readline completion environment to a fairly basic configuration before calling readline to handle -e input.

You can see the code in builtins/read.def, in the edit_line function: it sets rl_attempted_completion_function to NULL for the duration of the call to readline. readline has several completion overrides, so it's not 100% obvious that this resets the entire completion environment, but as far as I know this is the function which is used to implement programmable completion as per the complete command.

With some work, you could probably modify the definition of the read command to allow a specific completion function instead of or in addition to the readline standard filename completion function. That would require a non-trivial understanding of bash internals, but it would be a reasonable project if you wanted to gain familiarity with those internals.

As a simpler but less efficient alternative, you could write your own little utility which just accepts one line of keyboard input with readline and echoes it to stdout. Then invoke read redirecting its stdin to your utility:

read -r < <(my_reader string1 string2 string3)

(That assumes that my_reader uses its command-line arguments to construct the potential completion list for the readline library. You'd probably want the option to present a prompt as well.)

The readline documentation includes an example of an application which does simple custom completion; once you translate it from the K&R function prototype syntax, it might be pretty easy to adapt to your needs.


Edit: After I looked at that example again, I thought it had a lot of unnecessary details, so I wrote the following example with fewer unnecessary details. I might upload it to github, but for now it's here even though it's nearly 100 lines:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <readline/readline.h>

static void version(const char* progname) {
  fprintf(stderr, "%s 0.1\n", progname);
}
static void usage(const char* progname) {
  fprintf(stderr, "Usage: %s [-fhv] [-p PROMPT] [-n PROGNAME] [COMPLETION...]\n", progname);
  fprintf(stderr,
          "Reads one line using readline, and prints it to stdout.\n"
          "Returns success if a line was read.\n"
          "  -p PROMPT    Output PROMPT before requesting input.\n"
          "  -n PROGNAME  Set application name to PROGNAME for readline config file\n"
          "               (Default: %s).\n"
          "  -f           Use filename completion as well as specified completions.\n"
          "  -h           Print this help text and exit.\n"
          "  -v           Print version number and exit.\n"
          "  COMPLETION   word to add to the list of possible completions.\n",
          progname);
}

/* Readline really likes globals, so none of its hooks take a context parameter. */
static char** completions = NULL;
static char* generate_next_completion(const char* text, int state) {
  static int index = 0;
  if (state == 0) index = 0; /* reset index if we're starting */
  size_t textlen = strlen(text);
  while (completions[index++])
    if (strncmp(completions[index - 1], text, textlen) == 0)
      return strdup(completions[index - 1]);
  return NULL;
}

/* We use this if we will fall back to filename completion */
static char** generate_completions(const char* text, int start, int end) {
  return rl_completion_matches(text, generate_next_completion);
}

int main (int argc, char **argv) {
  const char* prompt = "";
  const char* progname = strrchr(argv[0], '/');
  progname = progname ? progname + 1 : argv[0];
  rl_readline_name = progname;

  bool use_file_completion = false;

  for (;;) {
    int opt = getopt(argc, argv, "+fp:n:hv");
    switch (opt) {
      case -1:  break;
      case 'f': use_file_completion = true; continue;
      case 'p': prompt = optarg; continue;
      case 'n': rl_readline_name = optarg; continue;
      case 'h': usage(progname); return 0;
      case 'v': version(progname); return 0;
      default:  usage(progname); return 2;
    }
    break;
  }

  /* The default is stdout, which would interfere with capturing output. */
  rl_outstream = stderr;

  completions = argv + optind;
  rl_completion_entry_function = rl_filename_completion_function;
  if (*completions) {
    if (use_file_completion)
      rl_attempted_completion_function = generate_completions; 
    else
      rl_completion_entry_function = generate_next_completion;
  } else {
    /* No specified strings */ 
    if (!use_file_completion)
      rl_inhibit_completion = true; 
  }

  char* line = readline(prompt);

  if (line) {
    puts(line);
    free(line);
    return 0;
  } else {
    fputc('\n', rl_outstream);
    return 1;
  }
}