Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to suggest files with tab completion using readline?

Within the Bash shell, I can use tab-completion to use suggest file and directory names. How can I achieve this with nodejs and readline?

Examples:

  • /<Tab> should suggest /root/, /bin/, etc.
  • /et<Tab> should complete to /etc/.
  • fo<Tab> should complete to foobar assuming such a file exists in the current directory.

I was thinking of using globbing (pattern search_term.replace(/[?*]/g, "\\$&") + "*"), but is there maybe a library that I have overlooked?

This is my current approach using glob, it is broken when using //<Tab> as it returns the canonicalized name and has possibly some other oddities:

function command_completion(line) {
    var hits;
    // likely broken, one does not simply escape a glob char
    var pat = line.replace(/[?*]/g, "\\$&") + "*";
    // depends: glob >= 3.0
    var glob = require("glob").sync;
    hits = glob(pat, {
        silent: true,
        nobrace: true,
        noglobstar: true,
        noext: true,
        nocomment: true,
        nonegate: true
    });

    return [hits, line];
}

var readline = require("readline");
rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    completer: command_completion
});
rl.prompt();
like image 950
Lekensteyn Avatar asked Apr 17 '13 19:04

Lekensteyn


1 Answers

Here is a working solution with a few quirks:

  • It doesn't support relative paths
  • When trying to display suggestions by pressing tab twice, it displays the full path in the list of suggestions.
  • It prefers '/' over '\', but tolerates '\' separators on windows
  • It only supports directories and files. (no devices, pipes, sockets, soft links)

Code:

const { promises: fsPromises } = require("fs"); 
const { parse, sep } = require("path");

function fileSystemCompleter(line, callback) {
  let { dir, base } = parse(line);
  fsPromises.readdir(dir, { withFileTypes: true })
    .then((dirEntries) => {
      // for an exact match that is a directory, read the contents of the directory
      if (dirEntries.find((entry) => entry.name === base && entry.isDirectory())) {
        dir = dir === "/" || dir === sep ? `${dir}${base}` : `${dir}/${base}`;
        return fsPromises.readdir(dir, { withFileTypes: true })
      }
      return dirEntries.filter((entry) => entry.name.startsWith(base));
    })
    .then((matchingEntries) => {
      if (dir === sep || dir === "/") {
        dir = "";
      }
      const hits = matchingEntries
        .filter((entry) => entry.isFile() || entry.isDirectory())
        .map((entry) => `${dir}/${entry.name}${entry.isDirectory() && !entry.name.endsWith("/") ? "/" : ""}`);
      callback(null, [hits, line]);
    })
    .catch(() => (callback(null, [[], line])));
}
like image 165
grahamaj Avatar answered Nov 15 '22 09:11

grahamaj