summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--ChangeLog4
-rw-r--r--builtins.c4
-rw-r--r--edit-readline.c286
-rw-r--r--hash.c24
-rw-r--r--rc.h12
-rw-r--r--which.c16
7 files changed, 332 insertions, 15 deletions
diff --git a/.gitignore b/.gitignore
index eaa1bb5..8a544e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*~
+.*.swp
aclocal.m4
autom4te.cache/
compile
diff --git a/ChangeLog b/ChangeLog
index 78d5216..9aa76ff 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1006,3 +1006,7 @@ Changes since rc-1.5b2
Tidiness: be consistent about using tabs everywhere, and add a .vimrc to
keep it so in the future.
+
+2018-05-30
+
+ Feature: readline can now complete commands.
diff --git a/builtins.c b/builtins.c
index 42411ef..6abbff0 100644
--- a/builtins.c
+++ b/builtins.c
@@ -636,3 +636,7 @@ static void b_limit(char **av) {
}
}
#endif
+
+extern char *compl_builtin(const char *text, int state) {
+ return compl_name(text, state, &builtins[0].name, arraysize(builtins), &builtins[1].name - &builtins[0].name);
+}
diff --git a/edit-readline.c b/edit-readline.c
index acf96aa..47c8db5 100644
--- a/edit-readline.c
+++ b/edit-readline.c
@@ -2,6 +2,9 @@
#include <errno.h>
#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <sys/types.h>
#include <readline/readline.h>
#include <readline/history.h>
#include <readline/rltypedefs.h>
@@ -10,22 +13,294 @@
bool editing = 1;
+static const char *quote_chars = "\t\n !#$&'()*;<=>?@[\\]^`{|}~";
+
struct cookie {
char *buffer;
};
+/* Teach readline how to quote a filename in rc. "text" is the filename to be
+ * quoted. "type" is either SINGLE_MATCH, if there is only one completion
+ * match, or MULT_MATCH. "qp" is a pointer to any opening quote character the
+ * user typed.
+ */
+static char *quote(char *text, int type, char *qp) {
+ char *p, *r;
+
+ /* worst case: string is entirely quote characters each of which will
+ * be doubled, plus the initial and final quotes and \0 */
+ p = r = ealloc(strlen(text) * 2 + 3);
+ /* supply opening quote unless already there */
+ if (*qp != '\'')
+ *p++ = '\'';
+ while (*text) {
+ if (*text == '\'')
+ *p++ = '\''; /* double existing quote */
+ *p++ = *text++;
+ }
+ if (type == SINGLE_MATCH)
+ *p++ = '\'';
+ *p = '\0';
+ return r;
+}
+
+/* "unquote" is called with "text", the text of the word to be dequoted, and
+ * "quote_char", which is the quoting character that delimits the filename.
+ */
+char *unquote(char *text, int quote_char) {
+ char *p, *r;
+
+ p = r = ealloc(strlen(text) + 1);
+ while (*text) {
+ *p++ = *text++;
+ if (quote_char && *(text - 1) == '\'' && *text == '\'')
+ ++text;
+ }
+ *p = '\0';
+ return r;
+}
+
+/* Join two strings with a "/" between them, into a malloc string */
+static char *dir_join(char *a, char *b) {
+ char *p;
+ if (!a) return strdup(b);
+ if (!b) return strdup(a);
+ p = ealloc(strlen(a) + strlen(b) + 2);
+ strcpy(p, a);
+ if (p[strlen(p) - 1] != '/')
+ strcat(p, "/");
+ strcat(p, b);
+ return p;
+}
+
+char *maybe_quote(char *p) {
+ const char *q;
+ for (q = quote_chars; *q; ++q) {
+ if (strchr(p, *q)) {
+ char *r = quote(p, SINGLE_MATCH, "");
+ efree(p);
+ return r;
+ }
+ }
+ return p;
+}
+
+/* Suppose there is a subdirectory in one of the directories on $path,
+ * "/bin/sub/", and the user has typed "suTAB". We want to offer "sub/" as a
+ * completion, but we need to offer two things so it is not taken as the sole
+ * completion. We use "bodge" to hold "sub/" while we return "sub/..." the
+ * first time. Next time, we will notice that "bodge" is set, and return that
+ * instead. (It would be better to persuade readline just to append the "/" as
+ * it does for filename completion, but it's not clear to me if this is
+ * possible.)
+ */
+static char *bodge;
+
+/* Decide if this directory entry is a completion candidate, either executable
+ * or a directory. "dname" is the absolute path of the directory, "name" is the
+ * current entry. "subdirs" is the name being completed up to and including the
+ * last slash (or NULL if there is no slash), "prefix" is the remainder of the
+ * name being completed, "len" is the length of "prefix".
+ */
+static char *entry(char *dname, char *name, char *subdirs,
+ char *prefix, size_t len) {
+ char *full;
+ struct stat st;
+
+ if (strncmp(name, prefix, len) != 0)
+ return NULL;
+ if (streq(name, ".") || streq(name, ".."))
+ return NULL;
+ full = dir_join(dname, name);
+ if (rc_access(full, FALSE, &st)) {
+ efree(full);
+ return maybe_quote(dir_join(subdirs, name));
+ }
+ efree(full);
+ if (S_ISDIR(st.st_mode)) {
+ char *dir_ret = ealloc(strlen(name) + 5);
+ char *r;
+ strcpy(dir_ret, name);
+ strcat(dir_ret, "/");
+ bodge = maybe_quote(dir_join(subdirs, dir_ret));
+ strcat(dir_ret, "...");
+ r = dir_join(subdirs, dir_ret);
+ efree(dir_ret);
+ return maybe_quote(r);
+ }
+ return NULL;
+}
+
+/* Split a string "text" after the last "/" into "pre" and "post". If there is
+ * no "/", "pre" will be NULL. */
+void split_last_slash(const char *text, char **pre, char **post) {
+ char *last_slash = strrchr(text, '/');
+ if (last_slash) {
+ size_t l = last_slash + 1 - text;
+ *pre = ealloc(l + 1);
+ strncpy(*pre, text, l);
+ (*pre)[l] = '\0';
+ *post = strdup(last_slash + 1);
+ } else {
+ *pre = NULL;
+ *post = strdup(text);
+ }
+}
+
+static char *compl_extcmd(const char *text, int state) {
+ static char *dname, *prefix, *subdirs;
+ static DIR *d;
+ static List *path;
+ static size_t len;
+
+ if (!state) {
+ char *utext = unquote((char *)text, *text);
+ split_last_slash(utext, &subdirs, &prefix);
+ efree(utext);
+ d = NULL;
+ path = varlookup("path");
+ len = strlen(prefix);
+ bodge = NULL;
+ }
+ if (bodge) {
+ char *r = bodge;
+ bodge = NULL;
+ return r;
+ }
+ while (d || path) {
+ if (!d) {
+ dname = dir_join(path->w, subdirs);
+ d = opendir(dname);
+ path = path->n;
+ if (!d) efree(dname);
+ } else {
+ struct dirent *e;
+ while ((e = readdir(d))) {
+ char *x;
+ x = entry(dname, e->d_name, subdirs,
+ prefix, len);
+ if (x) return x;
+ }
+ closedir(d);
+ efree(dname);
+ d = NULL;
+ }
+ }
+ efree(subdirs);
+ efree(prefix);
+ return NULL;
+}
+
+static rl_compentry_func_t *const compl_cmd_funcs[] = {
+ compl_builtin,
+ compl_fn,
+ compl_extcmd
+};
+
+static char *compl_command(const char *text, int state) {
+ static size_t i;
+ static int s;
+ char *name = NULL;
+
+ if (!state) {
+ i = 0;
+ s = 0;
+ }
+ while (name == NULL && i < arraysize(compl_cmd_funcs)) {
+ name = compl_cmd_funcs[i](text, s);
+ if (name != NULL) {
+ s = 1;
+ } else {
+ i++;
+ s = 0;
+ }
+ }
+ return name;
+}
+
+static rl_compentry_func_t *compl_func(const char *text, int start, int end) {
+ int quote = FALSE;
+ char last = ';', *s, *t;
+
+ for (s = &rl_line_buffer[0], t = &rl_line_buffer[start]; s < t; s++) {
+ if (!quote && *s != ' ' && *s != '\t')
+ last = *s;
+ if (*s == '\'')
+ quote = !quote;
+ }
+ switch (last) {
+ case '`': case '@': case '|': case '&':
+ case '(': case ')': case '{': case ';':
+ return compl_command;
+ case '$':
+ return compl_var;
+ }
+ return NULL;
+}
+
+static rl_compentry_func_t *compentry_func;
+
+static char **rc_completion(const char *text, int start, int end) {
+ rl_compentry_func_t *func;
+
+ if (compentry_func != NULL) {
+ func = compentry_func;
+ compentry_func = NULL;
+ rl_attempted_completion_over = 1;
+ } else
+ func = compl_func(text, start, end);
+ if (func != NULL)
+ return rl_completion_matches(text, func);
+ else
+ return NULL;
+}
+
+static int expl_complete(rl_compentry_func_t *func, int count, int key) {
+ if (rl_last_func == rl_complete)
+ rl_last_func = NULL;
+ compentry_func = func;
+ return rl_complete(count, key);
+}
+
+static int rc_complete_command(int count, int key) {
+ return expl_complete(compl_extcmd, count, key);
+}
+
+static int rc_complete_filename(int count, int key) {
+ return expl_complete(rl_filename_completion_function, count, key);
+}
+
+static int rc_complete_variable(int count, int key) {
+ return expl_complete(compl_var, count, key);
+}
+
void *edit_begin(int fd) {
List *hist;
struct cookie *c;
rl_initialize();
+
+ rl_attempted_completion_function = rc_completion;
+ rl_basic_quote_characters = "";
+ rl_basic_word_break_characters = " \t\n`@$><=;|&{(";
rl_catch_signals = 0;
rl_completer_quote_characters = "'";
- rl_filename_quote_characters = "\t\n !#$&'()*;<=>?@[\\]^`{|}~";
+ rl_filename_dequoting_function = unquote;
+ rl_filename_quote_characters = quote_chars;
+ rl_filename_quoting_function = quote;
+ rl_readline_name = "rc";
+
+ rl_add_funmap_entry("rc-complete-command", rc_complete_command);
+ rl_add_funmap_entry("rc-complete-filename", rc_complete_filename);
+ rl_add_funmap_entry("rc-complete-variable", rc_complete_variable);
+ rl_bind_keyseq("\e!", rc_complete_command);
+ rl_bind_keyseq("\e/", rc_complete_filename);
+ rl_bind_keyseq("\e$", rc_complete_variable);
hist = varlookup("history");
if (hist != NULL)
- if (read_history(hist->w) != 0 && errno != ENOENT) /* ignore if missing */
+ if (read_history(hist->w) != 0 &&
+ errno != ENOENT) /* ignore if missing */
uerror(hist->w);
c = ealloc(sizeof *c);
@@ -74,10 +349,9 @@ void edit_free(void *cookie) {
struct cookie *c = cookie;
efree(c->buffer);
- /* Set c->buffer to NULL, allowing us to "overfree" it. This
- is a bit of a kludge, but it's otherwise hard to deal with
- the case where a signal causes an early return from
- readline. */
+ /* Set c->buffer to NULL, allowing us to "overfree" it. This is a bit
+ * of a kludge, but it's otherwise hard to deal with the case where a
+ * signal causes an early return from readline. */
c->buffer = NULL;
}
diff --git a/hash.c b/hash.c
index 6a176a4..1b5f405 100644
--- a/hash.c
+++ b/hash.c
@@ -316,3 +316,27 @@ extern void whatare_all_vars(bool showfn, bool showvar) {
if (fp[i].name != NULL && fp[i].name != dead)
prettyprint_fn(1, fp[i].name, fnlookup(fp[i].name));
}
+
+extern char *compl_name(const char *text, int state, char **p, size_t count, ssize_t inc) {
+ static char **n;
+ static size_t i, len;
+ char *name;
+
+ if (!state) {
+ n = p;
+ i = 0;
+ len = strlen(text);
+ }
+ for (name = NULL; name == NULL && i < count; i++, n += inc)
+ if (*n != NULL && strncmp(*n, text, len) == 0)
+ name = strdup(*n);
+ return name;
+}
+
+extern char *compl_fn(const char *text, int state) {
+ return compl_name(text, state, &fp[0].name, fsize, &fp[1].name - &fp[0].name);
+}
+
+extern char *compl_var(const char *text, int state) {
+ return compl_name(text, state, &vp[0].name, vsize, &vp[1].name - &vp[0].name);
+}
diff --git a/rc.h b/rc.h
index 7ae0693..c3b2243 100644
--- a/rc.h
+++ b/rc.h
@@ -4,6 +4,9 @@
#include <assert.h>
+/* for struct stat */
+#include <sys/stat.h>
+
#define RC "rc: "
/* datatypes */
@@ -180,7 +183,7 @@ extern int lineno;
/* builtins.c */
extern builtin_t *isbuiltin(char *);
extern void b_exec(char **), funcall(char **), b_dot(char **), b_builtin(char **);
-extern char *which(char *, bool);
+extern char *compl_builtin(const char *, int);
/* except.c */
extern bool nl_on_intr;
@@ -259,6 +262,9 @@ extern void whatare_all_vars(bool, bool);
extern void whatare_all_signals(void);
extern void prettyprint_var(int, char *, List *);
extern void prettyprint_fn(int, char *, Node *);
+extern char *compl_name(const char *, int, char **, size_t, ssize_t);
+extern char *compl_fn(const char *, int);
+extern char *compl_var(const char *, int);
/* heredoc.c */
extern int heredoc(int);
@@ -394,3 +400,7 @@ extern bool forked;
/* walk.c */
extern bool walk(Node *, bool);
extern bool cond;
+
+/* which.c */
+extern bool rc_access(char *, bool, struct stat *);
+extern char *which(char *, bool);
diff --git a/which.c b/which.c
index 8ef0e39..2bd4a08 100644
--- a/which.c
+++ b/which.c
@@ -47,23 +47,22 @@ static int ingidset(gid_t g) {
Returns a bool instead of this -1 nonsense.
*/
-static bool rc_access(char *path, bool verbose) {
- struct stat st;
+bool rc_access(char *path, bool verbose, struct stat *stp) {
int mask;
- if (stat(path, &st) != 0) {
+ if (stat(path, stp) != 0) {
if (verbose) /* verbose flag only set for absolute pathname */
uerror(path);
return FALSE;
}
if (uid == 0)
mask = X_ALL;
- else if (uid == st.st_uid)
+ else if (uid == stp->st_uid)
mask = X_USR;
- else if (gid == st.st_gid || ingidset(st.st_gid))
+ else if (gid == stp->st_gid || ingidset(stp->st_gid))
mask = X_GRP;
else
mask = X_OTH;
- if (((st.st_mode & S_IFMT) == S_IFREG) && (st.st_mode & mask))
+ if (((stp->st_mode & S_IFMT) == S_IFREG) && (stp->st_mode & mask))
return TRUE;
errno = EACCES;
if (verbose)
@@ -91,6 +90,7 @@ extern char *which(char *name, bool verbose) {
static size_t testlen = 0;
List *path;
int len;
+ struct stat st;
if (name == NULL) /* no filename? can happen with "> foo" as a command */
return NULL;
if (!initialized) {
@@ -114,7 +114,7 @@ extern char *which(char *name, bool verbose) {
#endif
}
if (isabsolute(name)) /* absolute pathname? */
- return rc_access(name, verbose) ? name : NULL;
+ return rc_access(name, verbose, &st) ? name : NULL;
len = strlen(name);
for (path = varlookup("path"); path != NULL; path = path->n) {
size_t need = strlen(path->w) + len + 2; /* one for null terminator, one for the '/' */
@@ -130,7 +130,7 @@ extern char *which(char *name, bool verbose) {
strcat(test, "/");
strcat(test, name);
}
- if (rc_access(test, FALSE))
+ if (rc_access(test, FALSE, &st))
return test;
}
if (verbose) {