From 254046c5675adbfd6f71ea95ebe8f81d4abb6b94 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Wed, 23 Apr 2025 09:28:17 -0700 Subject: [PATCH 1/3] Add special-case completion logic for first-position --- access.c | 3 +-- es.h | 4 ++- input.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++----- stdenv.h | 2 -- util.c | 2 +- var.c | 15 +++++++++++ 6 files changed, 93 insertions(+), 12 deletions(-) diff --git a/access.c b/access.c index e393c1ef..a7cc1ea5 100644 --- a/access.c +++ b/access.c @@ -1,6 +1,5 @@ /* access.c -- access testing and path searching ($Revision: 1.2 $) */ -#define REQUIRE_STAT 1 #define REQUIRE_PARAM 1 #include "es.h" @@ -40,7 +39,7 @@ static Boolean ingroupset(gidset_t gid) { return FALSE; } -static int testperm(struct stat *stat, unsigned int perm) { +extern int testperm(struct stat *stat, unsigned int perm) { unsigned int mask; static gidset_t uid, gid; static Boolean initialized = FALSE; diff --git a/es.h b/es.h index 83ab2216..7396acbd 100644 --- a/es.h +++ b/es.h @@ -198,6 +198,7 @@ extern void setnoexport(List *list); extern void addtolist(void *arg, char *key, void *value); extern List *listvars(Boolean internal); extern List *varswithprefix(char *prefix); +extern List *fnswithprefix(char *prefix); typedef struct Push Push; extern Push *pushlist; @@ -216,6 +217,7 @@ extern void printstatus(int pid, int status); /* access.c */ +extern int testperm(struct stat *stat, unsigned int perm); extern char *checkexecutable(char *file); @@ -283,7 +285,7 @@ extern void *erealloc(void *p, size_t n); extern void efree(void *p); extern void ewrite(int fd, const char *s, size_t n); extern long eread(int fd, char *buf, size_t n); -extern Boolean isabsolute(char *path); +extern Boolean isabsolute(const char *path); extern Boolean streq2(const char *s, const char *t1, const char *t2); diff --git a/input.c b/input.c index c07b1168..7d5be165 100644 --- a/input.c +++ b/input.c @@ -529,9 +529,68 @@ static char *list_completion_function(const char *text, int state) { return result; } -char **builtin_completion(const char *text, int UNUSED start, int UNUSED end) { - char **matches = NULL; +/* detect if we're currently at the start of a line. works ~80% well */ +static Boolean cmdstart(int point) { + int i; + Boolean quote = FALSE, start = TRUE; + for (i = 0; i < point; i++) { + char c = rl_line_buffer[i]; + switch (c) { + case '\'': + quote = !quote; + break; + case '&': case '|': case '{': case '`': + if (!quote) start = TRUE; + break; + /* \n doesn't work right :( */ + case ' ': case '\t': case '\n': case '!': + break; + default: + if (!quote) start = FALSE; + } + } + return start; +} +/* first-position completion. includes + * - built-ins: local let for fn %closure match + * - functions + * - if absolute (including home), absolute executable files + */ +static List *listexecutables(char *text) { + int i = 0; + char *s; + List *compl = NULL; + static char *builtins[] = {"local", "let", "for", "fn", "%closure", "match"}; + for (i = 0; i < 6; i++) + if (strneq(text, builtins[i], strlen(text))) + compl = mklist(mkstr(builtins[i]), compl); + + compl = append(fnswithprefix(text), compl); + + if (isabsolute(text) || *text == '~') { + i = 0; + /* goofy hack :) */ + while ((s = rl_filename_completion_function(text, i)) != NULL) { + struct stat st; + i = 1; + /* TODO: ~/foo doesn't stat(), so don't try */ + if (*s != '~') { + if (stat(s, &st) == -1) + continue; + /* 1 == EXEC */ + if (testperm(&st, 1) != 0) + continue; + } + /* TODO: recurse in directories? */ + compl = mklist(mkstr(s), compl); + } + } + return compl; +} + +char **builtin_completion(const char *text, int start, int UNUSED end) { + /* variable or primitive completion */ if (*text == '$') { wordslistgen = varswithprefix; complprefix = "$"; @@ -543,14 +602,22 @@ char **builtin_completion(const char *text, int UNUSED start, int UNUSED end) { case '^': complprefix = "$^"; break; case '#': complprefix = "$#"; break; } - matches = rl_completion_matches(text, list_completion_function); + return rl_completion_matches(text, list_completion_function); } /* ~foo => username. ~foo/bar already gets completed as filename. */ - if (!matches && *text == '~' && !strchr(text, '/')) - matches = rl_completion_matches(text, rl_username_completion_function); + if (*text == '~' && !strchr(text, '/')) + return rl_completion_matches(text, rl_username_completion_function); + + /* first-word completion, which is ~special~ */ + if (cmdstart(start)) { + wordslistgen = listexecutables; + complprefix = ""; + rl_attempted_completion_over = 1; + return rl_completion_matches(text, list_completion_function); + } - return matches; + return NULL; /* fall back to normal filename completion */ } #endif /* HAVE_READLINE */ diff --git a/stdenv.h b/stdenv.h index 98bfbf87..a08e391f 100644 --- a/stdenv.h +++ b/stdenv.h @@ -44,9 +44,7 @@ #include #endif -#if REQUIRE_STAT #include -#endif #if REQUIRE_DIRENT #if HAVE_DIRENT_H diff --git a/util.c b/util.c index bf1a53ae..ebf7b48e 100644 --- a/util.c +++ b/util.c @@ -34,7 +34,7 @@ extern void uerror(char *s) { } /* isabsolute -- test to see if pathname begins with "/", "./", or "../" */ -extern Boolean isabsolute(char *path) { +extern Boolean isabsolute(const char *path) { return path[0] == '/' || (path[0] == '.' && (path[1] == '/' || (path[1] == '.' && path[2] == '/'))); diff --git a/var.c b/var.c index bacc7523..3376bca6 100644 --- a/var.c +++ b/var.c @@ -357,6 +357,12 @@ static void listwithprefix(void *arg, char *key, void *value) { addtolist(arg, key, value); } +static void fnwithprefix(void *arg, char *key, void *value) { + if (strneq(key, "fn-", 3) && + strneq(key + 3, list_prefix, strlen(list_prefix))) + addtolist(arg, key + 3, value); +} + /* listvars -- return a list of all the (dynamic) variables */ extern List *listvars(Boolean internal) { Ref(List *, varlist, NULL); @@ -365,6 +371,15 @@ extern List *listvars(Boolean internal) { RefReturn(varlist); } +/* fnswithprefix -- return a list of all the (dynamic) functions + * matching the given prefix */ +extern List *fnswithprefix(char *prefix) { + Ref(List *, fnlist, NULL); + list_prefix = prefix; + dictforall(vars, fnwithprefix, &fnlist); + RefReturn(fnlist); +} + /* varswithprefix -- return a list of all the (dynamic) variables * matching the given prefix */ extern List *varswithprefix(char *prefix) { From 714dbf5d66ef04e82a83f23787bd1af5eb660c69 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 17 May 2025 09:05:55 -0700 Subject: [PATCH 2/3] Improve command-start detection Now we know we're in command position after `blah <=` and `blah |[2] `. --- input.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/input.c b/input.c index 7d5be165..4f5b2845 100644 --- a/input.c +++ b/input.c @@ -529,27 +529,70 @@ static char *list_completion_function(const char *text, int state) { return result; } -/* detect if we're currently at the start of a line. works ~80% well */ +enum st { + NORMAL, + START, /* start of a command */ + PIPESTART, /* just after a '|' */ + PIPESTARTBRACKET, /* the '|[' in 'a |[2] b' */ + LT /* the '<' in '<=word' */ +}; + +/* detect if we're currently at the start of a line. works ~90% well */ static Boolean cmdstart(int point) { int i; - Boolean quote = FALSE, start = TRUE; + Boolean quote = FALSE; + enum st state = START; for (i = 0; i < point; i++) { char c = rl_line_buffer[i]; - switch (c) { - case '\'': + if (c == '\'') { quote = !quote; + continue; + } + if (quote) continue; + + switch (state) { + case PIPESTARTBRACKET: + if (c == ']') + state = START; break; - case '&': case '|': case '{': case '`': - if (!quote) start = TRUE; + case LT: + if (c == '=') + state = START; + else + state = NORMAL; break; - /* \n doesn't work right :( */ - case ' ': case '\t': case '\n': case '!': + case PIPESTART: + if (c == '[') { + state = PIPESTARTBRACKET; + break; + } + state = START; /* || correct? */ + /* fallthrough */ + case START: + switch (c) { + case ' ': case '\t': case '\n': case '!': + break; + default: + state = NORMAL; + } break; - default: - if (!quote) start = FALSE; + case NORMAL: + switch (c) { + case '&': case '{': case '`': + state = START; + break; + case '|': + state = PIPESTART; + break; + case '<': + state = LT; + break; + default: + break; /* nothing to do */ + } } } - return start; + return state == START || state == PIPESTART; } /* first-position completion. includes From d495b5c10632948d172b9372e40b8b1e787b2ad7 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Mon, 19 May 2025 10:22:32 -0700 Subject: [PATCH 3/3] more improvements to start-of-command detection --- input.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/input.c b/input.c index 4f5b2845..a3dade6d 100644 --- a/input.c +++ b/input.c @@ -566,16 +566,12 @@ static Boolean cmdstart(int point) { state = PIPESTARTBRACKET; break; } - state = START; /* || correct? */ + state = START; /* does this handle || correctly? */ /* fallthrough */ case START: - switch (c) { - case ' ': case '\t': case '\n': case '!': - break; - default: - state = NORMAL; - } - break; + if (c == ' ' || c == '\t' || c == '\n' || c == '!') + continue; + /* fallthrough */ case NORMAL: switch (c) { case '&': case '{': case '`': @@ -588,7 +584,9 @@ static Boolean cmdstart(int point) { state = LT; break; default: - break; /* nothing to do */ + /* fallthroughs make this useful */ + state = NORMAL; + break; } } }