I'm writing a bash tab completion command and have the basics working.

I'm trying to make it a little more sophisticated but the bash manual isn't great.

Specifically the manual mentions the shell variable COMP_TYPE.

I'm trying to work out what triggers each type and whether the type changes how the output of my completion command is interpreted.

For example there is a type of '%' which is referred to as a menu.

The command I'm completing for takes a set of switches so I'm wondering if the menu type actual displays some sort of menu.

> dcli compile -o -w hello.dart

Cli completion for command switches would be nicer if it came in a little menu with some formatting.

Some more details on how the types are triggered and how the output is treated would be really useful.


I'm looking for clarity on three things:

  1. how a particular type is selected. Does the user actually type % to trigger the menu type?

Typing ls % doesn't seem to trigger a completion.

  1. If the user triggers a menu type - what does it actually imply that the completion should do? i.e. for a tab completion I provide a list of options, what should be different if its a menu type?

  2. If the user triggers a menu type does it change how bash process the completion commands output. With normal tab completion the completion command prints one result per line and bash displays these as choices. So does the user selecting the menu type change what the completion command can output or does it change how bash interprets/uses the output?

Edit 2:

As requested here is the Dart code that implements the completion:

void main(List<String> args) {
  if (args.length < 2) {
    printerr('dcli_complete provides tab completion from the bash command line for dcli');
    printerr("You don't run dcli_complete directly");

  //var appname = args[0];
  var word = args[1];

  var commands = Commands.applicationCommands;

  var results = <String>[];

  var priorCommandFound = false;

  // do we have a prior word.
  if (args.length == 3) {
    var priorWord = args[2];
    //print('prior word: $priorWord');
    if (priorWord.isNotEmpty) {
      var priorCommand = Commands.findCommand(priorWord, Commands.asMap(Commands.applicationCommands));

      if (priorCommand != null) {
        /// We found a command let it complete the expansion according to its own rules
        //print('priorCommand ${priorCommand.name}');
        results = priorCommand.completion(word);
        priorCommandFound = true;

  if (!priorCommandFound) {
    // find all commands that matches the 'word' using it as prefix
    // and add them to the output.
    for (var command in commands) {
      if (command.name.startsWith(word)) {
  for (var result in results) {

Example complete method when a 'prior' command is found.

  List<String> completion(String word) {
    return completionExpandScripts(word);

List<String> completionExpandScripts(String word, {String workingDirectory = '.'}) {
  var root = workingDirectory;

  /// expand ~ to the home dir.
  if (word.startsWith('~')) {
    word = word.replaceAll('~', HOME);

  var searchTerm = word;

  // a trailing slash and we treat the word as a directory.
  if (word.endsWith(Platform.pathSeparator)) {
    root = join(root, word);
    searchTerm = '';
  } else {
    // no trailing slash but the word may contain a directory path
    // in which case we use the last part as the search term
    // and append any remaining path to the root.
    if (word.isNotEmpty) {
      var parts = split(word);

      searchTerm = parts.last;

      if (parts.length > 1) {
        root = join(root, parts.sublist(0, parts.length - 1).join(Platform.pathSeparator));

  /// if the resulting path is invalid return an empty list.
  if (!exists(root)) return <String>[];

  // /// if the work ends in a slash then we treat it as a directory
  // /// then we need to use the directory as the root so we
  // /// search in it.
  // if (exists(join(root, searchTerm))) {
  //   root = join(root, searchTerm);
  //   searchTerm = '';
  // }

  var entries = find('$searchTerm*', types: [Find.directory, Find.file], root: root, recursive: false).toList();

  var results = <String>[];
  for (var script in entries) {
    if (word.isEmpty || relative(script, from: workingDirectory).startsWith(word)) {
      var matchPath = join(root, script);
      String filePath;
      if (isDirectory(matchPath)) {
        // its a directory add trailing slash and returning.
        filePath = '${relative('$script', from: workingDirectory)}/';
      } else {
        filePath = relative(script, from: workingDirectory);
      if (filePath.contains(' ')) {
        /// we quote filenames that include a space
        filePath = '"$filePath"';

  return results;

Edit 3:

This is the most detailed explanation I've found so far it it still doesn't really help.

According to the manual:

COMP_TYPE Set to an integer value corresponding to the type of completion attempted that caused a completion function to be called: TAB, for normal completion, ?, for listing completions after succes- sive tabs, !, for listing alternatives on partial word comple- tion, @, to list completions if the word is not unmodified, or %, for menu completion. This variable is available only in shell functions and external commands invoked by the program- mable completion facilities (see Programmable Completion below).

I can get TAB, ? and % but I don't know how I can get ! and @.

You should see !' when a user hits TAB with show-all-if-ambiguous' set (so the possible completions are listed immediately), and @' if the user hits TAB with show-all-if-unmodified' set (the possible completions don't have a common prefix so there can't be any partial completion, and so the completions are displayed immediately). These are internal values for readline's idea of the type of completion desired, and they're reflected in the value of COMP_TYPE.

This stack overflow article provides some explanation of what the show-if-xxx settings do, however they appear to affect how readline operates but it is unclear how readline interacts with my completion command.


It also doesn't explain the '%' COMP_TYPE and how it is triggered.

  • Note that the SHELL variable comp_type does not exist. Variable names are case sensitive. Do write COMP_TYPE.
    – Cbhihe
    Oct 12, 2020 at 21:21
  • @cbhihe I was just being lazy typing on mobile:) Oct 13, 2020 at 4:10
  • @Cbhihe I've edited my question. Oct 13, 2020 at 5:45
  • Can you please show us the "tab completion command" for which you announce that its "basics are working" ? Your question marked as "1." makes me ask again: did you look at the "Programmable Completion" facility section of the Bash man page ? ---- Also look at hypexr.org/bash_tutorial.php#completion as well as at stackoverflow.com/a/21476506. The latter will be directly useful if what you are after is tab completion on roll-your-own type commands. On most systems running bash, /etc/bash_completion and peeking inside of /etc/bash_completion.d/ will also help you.
    – Cbhihe
    Oct 13, 2020 at 10:10
  • I've dropped in the code, its written in dart, but easy enough to read. I've read the manual and both of the above links multiple times and can find no explanation of what triggers the COMP_TYPE to be set not what it changes. Oct 13, 2020 at 10:54


You must log in to answer this question.

Browse other questions tagged .