diff --git a/userland/apps/shell/src/line_edit.c b/userland/apps/shell/src/line_edit.c index e2b6a67c..8ed711a4 100644 --- a/userland/apps/shell/src/line_edit.c +++ b/userland/apps/shell/src/line_edit.c @@ -355,9 +355,7 @@ static void handle_tab(line_edit_state* s, const char* prompt) { int name_prefix_len; if (cmd_mode) { - /* Command mode: search /bin/ for matching executables */ - strncpy(dir_path, "/bin", sizeof(dir_path) - 1); - dir_path[sizeof(dir_path) - 1] = '\0'; + /* Command mode: search every PATH directory for executables */ int cap = tok_len < COMPLETE_NAME_MAX - 1 ? tok_len : COMPLETE_NAME_MAX - 1; memcpy(name_prefix, prefix, (size_t)cap); name_prefix[cap] = '\0'; @@ -395,12 +393,42 @@ static void handle_tab(line_edit_state* s, const char* prompt) { completion_entry* entries = malloc(COMPLETE_MAX * sizeof(completion_entry)); if (!entries) return; - int count = collect_candidates(dir_path, name_prefix, name_prefix_len, - entries, COMPLETE_MAX); + int count = 0; + if (cmd_mode) { + const char* path = getenv("PATH"); + if (!path || !*path) path = "/bin"; + + while (*path && count < COMPLETE_MAX) { + const char* sep = strchr(path, ':'); + int dir_len = sep ? (int)(sep - path) : (int)strlen(path); + + if (dir_len > 0 && dir_len < (int)sizeof(dir_path)) { + memcpy(dir_path, path, (size_t)dir_len); + dir_path[dir_len] = '\0'; + count += collect_candidates(dir_path, name_prefix, name_prefix_len, + entries + count, COMPLETE_MAX - count); + } + + if (!sep) break; + path = sep + 1; + } + } else { + count = collect_candidates(dir_path, name_prefix, name_prefix_len, + entries, COMPLETE_MAX); + } - if (count > 1) + if (count > 1) { qsort(entries, (size_t)count, sizeof(completion_entry), completion_cmp); + /* Drop duplicate names collected from multiple PATH directories */ + int unique = 1; + for (int i = 1; i < count; i++) { + if (strcmp(entries[i].name, entries[unique - 1].name) != 0) + entries[unique++] = entries[i]; + } + count = unique; + } + if (count == 0) { /* no matches */ free(entries); diff --git a/userland/apps/shell/src/shell.c b/userland/apps/shell/src/shell.c index 843310c4..bc3a8fd6 100644 --- a/userland/apps/shell/src/shell.c +++ b/userland/apps/shell/src/shell.c @@ -38,8 +38,31 @@ static int reap_status(int status) { return status; } +/* + * Resolve a bare command name against PATH, falling back to /bin. + * Names containing '/' are used as-is. + */ static const char* resolve_cmd(const char* name, char* path_buf, int buf_size) { if (strchr(name, '/')) return name; + + const char* path = getenv("PATH"); + if (!path || !*path) path = "/bin"; + + while (*path) { + const char* sep = strchr(path, ':'); + int dir_len = sep ? (int)(sep - path) : (int)strlen(path); + + if (dir_len > 0) { + int n = snprintf(path_buf, buf_size, "%.*s/%s", dir_len, path, name); + if (n > 0 && n < buf_size && access(path_buf, X_OK) == 0) + return path_buf; + } + + if (!sep) break; + path = sep + 1; + } + + /* Keep the historical candidate so failure messaging is unchanged */ int n = snprintf(path_buf, buf_size, "/bin/%s", name); return (n > 0 && n < buf_size) ? path_buf : name; }