diff --git a/incs/executor.h b/incs/executor.h index 886ce875..3b9a54f5 100644 --- a/incs/executor.h +++ b/incs/executor.h @@ -1,94 +1,95 @@ -/* ************************************************************************** */ -/* */ -/* ::: :::::::: */ -/* executor.h :+: :+: :+: */ -/* +:+ +:+ +:+ */ -/* By: marvin +#+ +:+ +#+ */ -/* +#+#+#+#+#+ +#+ */ -/* Created: 2026/01/10 02:38:39 by marvin #+# #+# */ -/* Updated: 2026/01/10 02:38:39 by marvin ### ########.fr */ -/* */ -/* ************************************************************************** */ - -/* Executor public API: turning an AST into side-effects. - The main entry is execute_top_level() which walks the tree returned by - the parser. Every execute_* function returns t_execution_state so the - exit code and any ctrl-c signal propagate up cleanly. */ - -#ifndef EXECUTOR_H -# define EXECUTOR_H - -# include "public/executor_types.h" -# include "alias.h" - -/* forward declarations to avoid heavy includes */ -typedef struct s_shell t_shell; -typedef struct s_ast_node t_ast_node; - -/* An expanded, ready-to-run simple command: pre-command assignments, - the argv array, and the resolved executable path. pooled=true means - argv.ctx comes from argv_pool (don't free it, just reset the vec). */ -typedef struct executable_cmd_s -{ - t_vec_env pre_assigns; /* NAME=VALUE pairs before the command */ - t_vec argv; /* NULL-terminated argv word list */ - char *fname; /* resolved path (heap) or NULL for builtins */ - bool pooled; /* true if argv was borrowed from argv_pool */ -} t_executable_cmd; - -/* argv backing pool (free_utils2.c): borrow on expand, return on teardown. */ -void argv_pool_acquire(t_shell *state, t_executable_cmd *cmd); -void argv_pool_release(t_shell *state, t_executable_cmd *cmd); -void free_argv_pool(t_shell *state); - -/* Execution context for one node in the pipeline: which fds to hook up, - which AST node to execute, and whether this command runs in the parent - process (builtins, function calls) and is allowed to modify state. */ -typedef struct executable_node_s -{ - int infd; /* read end of pipe from previous stage */ - int outfd; /* write end of pipe to next stage */ - int next_infd; /* saved next read end (pipeline setup) */ - t_ast_node *node; /* the AST node being executed */ - t_vec_int redirs; /* indices into state->redirects */ - bool modify_parent_ctx; /* true for builtins that modify state */ -} t_executable_node; - -void execute_top_level(t_shell *state); -int exec_string(t_shell *state, char *str); -int exec_file_string(t_shell *state, char *str, - const char *src); -t_execution_state execute_pipeline(t_shell *state, - t_executable_node *exe); -void set_up_redirection(t_shell *state, - t_executable_node *exe); -t_execution_state execute_simple_command(t_shell *state, - t_executable_node *exe); -void forward_exit_status(t_execution_state res); -t_execution_state execute_command(t_shell *state, - t_executable_node *exe); -t_execution_state execute_tree_node(t_shell *state, - t_executable_node *exe); -t_execution_state execute_simple_list(t_shell *state, - t_executable_node *exe); -t_string word_to_string(t_ast_node node); -t_string word_to_hrdoc_string(t_ast_node node); -void set_cmd_status(t_shell *state, t_execution_state res); -t_execution_state res_status(int status); -t_execution_state res_pid(int pid); -void fire_debug_trap(t_shell *state); -void fire_err_trap(t_shell *state, int code); -void fire_return_trap(t_shell *state, int code); -void trap_save_reset(t_shell *state, char **save); -void pseudo_traps_quiet(t_shell *state); -void trap_restore(t_shell *state, char **save); -void exe_res_set_status(t_shell *st, t_execution_state *res); -int find_cmd_path(t_shell *state, char *cmd_name, - char **path_of_exe); -int exec_external_argv(t_shell *state, t_vec *args); -int run_external_sync(t_shell *state, t_vec *args); -int expand_simple_command(t_shell *state, t_ast_node *node, - t_executable_cmd *ret, t_vec_int *redirects); -int (*builtin_func(char *name))(t_shell *state, t_vec argv); - +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* executor.h :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: marvin +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2026/01/10 02:38:39 by marvin #+# #+# */ +/* Updated: 2026/01/10 02:38:39 by marvin ### ########.fr */ +/* */ +/* ************************************************************************** */ + +/* Executor public API: turning an AST into side-effects. + The main entry is execute_top_level() which walks the tree returned by + the parser. Every execute_* function returns t_execution_state so the + exit code and any ctrl-c signal propagate up cleanly. */ + +#ifndef EXECUTOR_H +# define EXECUTOR_H + +# include "public/executor_types.h" +# include "alias.h" + +/* forward declarations to avoid heavy includes */ +typedef struct s_shell t_shell; +typedef struct s_ast_node t_ast_node; + +/* An expanded, ready-to-run simple command: pre-command assignments, + the argv array, and the resolved executable path. pooled=true means + argv.ctx comes from argv_pool (don't free it, just reset the vec). */ +typedef struct executable_cmd_s +{ + t_vec_env pre_assigns; /* NAME=VALUE pairs before the command */ + t_vec argv; /* NULL-terminated argv word list */ + char *fname; /* resolved path (heap) or NULL for builtins */ + bool pooled; /* true if argv was borrowed from argv_pool */ +} t_executable_cmd; + +/* argv backing pool (free_utils2.c): borrow on expand, return on teardown. */ +void argv_pool_acquire(t_shell *state, t_executable_cmd *cmd); +void argv_pool_release(t_shell *state, t_executable_cmd *cmd); +void free_argv_pool(t_shell *state); + +/* Execution context for one node in the pipeline: which fds to hook up, + which AST node to execute, and whether this command runs in the parent + process (builtins, function calls) and is allowed to modify state. */ +typedef struct executable_node_s +{ + int infd; /* read end of pipe from previous stage */ + int outfd; /* write end of pipe to next stage */ + int next_infd; /* saved next read end (pipeline setup) */ + t_ast_node *node; /* the AST node being executed */ + t_vec_int redirs; /* indices into state->redirects */ + bool modify_parent_ctx; /* true for builtins that modify state */ +} t_executable_node; + +void execute_top_level(t_shell *state); +int exec_string(t_shell *state, char *str); +int exec_file_string(t_shell *state, char *str, + const char *src); +t_execution_state execute_pipeline(t_shell *state, + t_executable_node *exe); +void set_up_redirection(t_shell *state, + t_executable_node *exe); +t_execution_state execute_simple_command(t_shell *state, + t_executable_node *exe); +void forward_exit_status(t_execution_state res); +t_execution_state execute_command(t_shell *state, + t_executable_node *exe); +t_execution_state execute_tree_node(t_shell *state, + t_executable_node *exe); +t_execution_state execute_simple_list(t_shell *state, + t_executable_node *exe); +t_string word_to_string(t_ast_node node); +t_string word_to_hrdoc_string(t_ast_node node); +void set_cmd_status(t_shell *state, t_execution_state res); +t_execution_state res_status(int status); +t_execution_state res_pid(int pid); +void fire_debug_trap(t_shell *state); +void fire_err_trap(t_shell *state, int code); +void fire_return_trap(t_shell *state, int code); +void trap_save_reset(t_shell *state, char **save); +void pseudo_traps_quiet(t_shell *state); +void trap_restore(t_shell *state, char **save); +void exe_res_set_status(t_shell *st, t_execution_state *res); +int find_cmd_path(t_shell *state, char *cmd_name, + char **path_of_exe); +int exec_external_argv(t_shell *state, t_vec *args); +int run_external_sync(t_shell *state, t_vec *args); +int expand_simple_command(t_shell *state, t_ast_node *node, + t_executable_cmd *ret, t_vec_int *redirects); +int (*builtin_func(t_shell *state, char *name))(t_shell *s, + t_vec argv); + #endif \ No newline at end of file diff --git a/incs/ft_builtins.h b/incs/ft_builtins.h index 34b364be..778e2a1e 100644 --- a/incs/ft_builtins.h +++ b/incs/ft_builtins.h @@ -148,7 +148,9 @@ bool zopt_apply(t_shell *state, const char *n, bool on); int builtin_let(t_shell *state, t_vec argv); int builtin_kill(t_shell *state, t_vec argv); int builtin_printf(t_shell *state, t_vec argv); -int (*builtin_func(char *name))(t_shell *state, +int (*builtin_func(t_shell *state, char *name))(t_shell *s, t_vec argv); +int (*zsh_builtin_func(char *name))(t_shell *s, t_vec argv); +int builtin_which(t_shell *state, t_vec argv); #endif diff --git a/src/builtins/builtin_builtin.c b/src/builtins/builtin_builtin.c index ecef49fb..04c932b7 100644 --- a/src/builtins/builtin_builtin.c +++ b/src/builtins/builtin_builtin.c @@ -29,7 +29,7 @@ int builtin_builtin(t_shell *state, t_vec argv) if (argv.len < 2) return (0); - f = builtin_func(((char **)argv.ctx)[1]); + f = builtin_func(state, ((char **)argv.ctx)[1]); if (!f) { ft_eprintf("%s: builtin: %s: not a shell builtin\n", diff --git a/src/builtins/builtin_cmd2.c b/src/builtins/builtin_cmd2.c index b3cbad20..5c33e9b8 100644 --- a/src/builtins/builtin_cmd2.c +++ b/src/builtins/builtin_cmd2.c @@ -58,7 +58,8 @@ static int command_v(t_shell *state, char *name) if (alias_get(&state->aliases, name)) return (ft_printf("alias "), alias_print_one(&state->aliases, name)); - if (type_is_keyword(name) || builtin_func(name) || func_lookup(state, name)) + if (type_is_keyword(name) || builtin_func(state, name) + || func_lookup(state, name)) return (ft_printf("%s\n", name), 0); if (ft_strchr(name, '/')) { @@ -120,8 +121,9 @@ static int command_run(t_shell *state, t_vec argv, size_t start) i = start; while (i < argv.len) vec_push(&sub, &av[i++]); - if (builtin_func(av[start])) - return (rc = builtin_func(av[start])(state, sub), xfree(sub.ctx), rc); + if (builtin_func(state, av[start])) + return (rc = builtin_func(state, av[start])(state, sub), + xfree(sub.ctx), rc); rc = run_external_sync(state, &sub); return (xfree(sub.ctx), rc); } diff --git a/src/builtins/builtin_declare4.c b/src/builtins/builtin_declare4.c index 08df4d7e..f389fd8c 100644 --- a/src/builtins/builtin_declare4.c +++ b/src/builtins/builtin_declare4.c @@ -25,8 +25,6 @@ `} 2>/dev/null`, so the closing brace is not added a second time. */ static void print_body(t_shell *state, t_shell_func *fn) { - const char *t; - size_t n; char *body; if (!fn->text) @@ -35,13 +33,7 @@ static void print_body(t_shell *state, t_shell_func *fn) "(defined by eval?)\n", state->ctx, fn->name); return ; } - t = fn->text; - while (*t == ' ' || *t == '\t' || *t == '\n') - t++; - n = ft_strlen(t); - while (n > 0 && (t[n - 1] == ' ' || t[n - 1] == '\t' || t[n - 1] == '\n')) - n--; - body = ft_strndup(t, n); + body = func_text_trimmed(fn->text); if (fn->body.node_type == AST_COMMAND) ft_printf("%s ()\n{\n%s\n", fn->name, body); else @@ -76,6 +68,21 @@ static int bodies_of(t_shell *state, t_vec argv, size_t i) return (missing); } +/* A function's source text without the blank space around it, as a new + string. Shared by `declare -f` and zsh's `which`, which print the same + body under different headers. */ +char *func_text_trimmed(const char *t) +{ + size_t n; + + while (*t == ' ' || *t == '\t' || *t == '\n') + t++; + n = ft_strlen(t); + while (n > 0 && (t[n - 1] == ' ' || t[n - 1] == '\t' || t[n - 1] == '\n')) + n--; + return (ft_strndup(t, n)); +} + int declare_functions(t_shell *state, t_vec argv, size_t i, bool bodies) { if (i < argv.len && ft_strcmp(((char **)argv.ctx)[i], "--") == 0) diff --git a/src/builtins/builtin_type.c b/src/builtins/builtin_type.c index 1b0c8dd6..851d3e02 100644 --- a/src/builtins/builtin_type.c +++ b/src/builtins/builtin_type.c @@ -16,9 +16,9 @@ /* Check via the dispatch hash table — no need to maintain a separate list. The dispatch table IS the authoritative set of builtins. */ -int type_is_builtin(const char *name) +int type_is_builtin(t_shell *state, const char *name) { - return (builtin_func((char *)name) != NULL); + return (builtin_func(state, (char *)name) != NULL); } /* POSIX reserved words (plus the common function/{/}). */ @@ -100,7 +100,7 @@ int type_one(t_shell *state, const char *name) return (ft_printf("%s is a shell keyword\n", name), 0); if (func_lookup(state, (char *)name)) return (ft_printf("%s is a function\n", name), 0); - if (type_is_builtin(name)) + if (type_is_builtin(state, name)) return (ft_printf("%s is a shell builtin\n", name), 0); return (type_one_path(state, name)); } diff --git a/src/builtins/builtin_type3.c b/src/builtins/builtin_type3.c index c53610ac..427597de 100644 --- a/src/builtins/builtin_type3.c +++ b/src/builtins/builtin_type3.c @@ -28,7 +28,7 @@ int type_t_one(t_shell *state, const char *name) return (ft_printf("keyword\n"), 0); if (func_lookup(state, (char *)name)) return (ft_printf("function\n"), 0); - if (type_is_builtin(name)) + if (type_is_builtin(state, name)) return (ft_printf("builtin\n"), 0); if (cmd_hash_lookup(&state->cmd_cache, name)) return (ft_printf("file\n"), 0); @@ -47,7 +47,7 @@ int type_p_one(t_shell *state, const char *name, int force) char *path; if (!force && (type_is_keyword(name) || func_lookup(state, (char *)name) - || type_is_builtin(name))) + || type_is_builtin(state, name))) return (0); if (ft_strchr(name, '/') && access(name, X_OK) == 0) return (ft_printf("%s\n", name), 0); diff --git a/src/builtins/builtin_zsh_which.c b/src/builtins/builtin_zsh_which.c new file mode 100644 index 00000000..38abc667 --- /dev/null +++ b/src/builtins/builtin_zsh_which.c @@ -0,0 +1,67 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* builtin_zsh_which.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: dlesieur +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2026/10/18 03:00:00 by dlesieur #+# #+# */ +/* Updated: 2026/10/18 03:00:00 by dlesieur ### ########.fr */ +/* */ +/* ************************************************************************** */ + +#include "builtins_private.h" + +/* zsh's `which` (its `whence -c`), a builtin in zsh mode only -- see + zsh_builtin_func() in hash_builtins_zsh.c for why the gate. Plugins call + it as `builtin which`: oh-my-zsh's sudo widget runs `builtin which -a` + on every Esc Esc, and without it each toggle printed "not a shell + builtin" over the prompt. + -a lists every match instead of the first, -p skips aliases, reserved + words, functions and builtins; any other letter is zsh's "bad option", + status 1. A name nothing answers to prints "NAME not found" on stdout + and makes the status 1, as zsh does; no operand at all is status 1. */ +static int which_opts(t_shell *state, t_vec argv, size_t *i, int *flags) +{ + char *a; + size_t k; + + while (*i < argv.len) + { + a = ((char **)argv.ctx)[*i]; + if (a[0] != '-' || !a[1]) + break ; + (*i)++; + if (!ft_strcmp(a, "--")) + break ; + k = 0; + while (a[++k]) + { + if (a[k] == 'a') + *flags |= WHICH_ALL; + else if (a[k] == 'p') + *flags |= WHICH_PATH; + else + return (ft_eprintf("%s: which: bad option: -%c\n", + state->ctx, a[k]), 1); + } + } + return (0); +} + +int builtin_which(t_shell *state, t_vec argv) +{ + size_t i; + int flags; + int rc; + + i = 1; + flags = 0; + if (which_opts(state, argv, &i, &flags)) + return (1); + rc = (i >= argv.len); + while (i < argv.len) + if (which_one(state, ((char **)argv.ctx)[i++], flags)) + rc = 1; + return (rc); +} diff --git a/src/builtins/builtin_zsh_which2.c b/src/builtins/builtin_zsh_which2.c new file mode 100644 index 00000000..c5320b50 --- /dev/null +++ b/src/builtins/builtin_zsh_which2.c @@ -0,0 +1,113 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* builtin_zsh_which2.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: dlesieur +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2026/10/18 03:00:00 by dlesieur #+# #+# */ +/* Updated: 2026/10/18 03:00:00 by dlesieur ### ########.fr */ +/* */ +/* ************************************************************************** */ + +#include "builtins_private.h" +#include "sh_alias.h" +#include "cmd_hash.h" + +/* zsh's `which`: the per-name lookup, in zsh's order -- alias, reserved + word, function, builtin, then the command itself. builtin_zsh_which.c + holds the option parsing. + + Caveat: zsh prints a function by deparsing it, one command per line; + this prints the body's source text as written, so `f() { a; b; }` comes + out as "\ta; b;" where zsh prints "\ta\n\tb". A one-command body matches + byte for byte. */ +static int which_function(t_shell_func *fn) +{ + char *body; + size_t n; + + if (fn->text) + body = func_text_trimmed(fn->text); + else + body = ft_strdup(""); + if (!body) + return (1); + n = ft_strlen(body); + while (n > 0 && body[n - 1] == ';') + body[--n] = '\0'; + ft_printf("%s () {\n\t%s\n}\n", fn->name, body); + xfree(body); + return (1); +} + +/* Caveat: the reserved words are hellish's POSIX set (type_is_keyword); + zsh 5.x also reserves [[, function, time, repeat, coproc, foreach and + the typeset family, so `which [[` says "shell built-in command" here + where zsh says "shell reserved word". */ +static int which_shell(t_shell *state, char *name, bool all) +{ + char *val; + t_shell_func *fn; + int hits; + + hits = 0; + val = alias_get(&state->aliases, name); + if (val) + hits += (ft_printf("%s: aliased to %s\n", name, val), 1); + if ((all || !hits) && type_is_keyword(name)) + hits += (ft_printf("%s: shell reserved word\n", name), 1); + fn = func_lookup(state, name); + if ((all || !hits) && fn) + hits += which_function(fn); + if ((all || !hits) && builtin_func(state, name)) + hits += (ft_printf("%s: shell built-in command\n", name), 1); + return (hits); +} + +/* The command: a name with a slash is itself when executable; otherwise + the hashed path, then $PATH. -a asks exe_path() one directory at a time, + so every match goes through the same is-a-file, is-executable test the + executor uses and is listed in $PATH order. */ +static int which_paths(t_shell *state, char *name, bool all) +{ + char *dir[2]; + char *path; + int denied; + int hits; + size_t i; + + if (ft_strchr(name, '/')) + return (access(name, X_OK) == 0 && ft_printf("%s\n", name) >= 0); + path = cmd_hash_lookup(&state->cmd_cache, name); + if (path && !all) + return (ft_printf("%s\n", path), 1); + path_cache_sync(state); + hits = 0; + i = 0; + dir[1] = NULL; + while (*name && state->path_dirs && state->path_dirs[i] && (all || !hits)) + { + dir[0] = state->path_dirs[i++]; + path = exe_path(dir, name, &denied); + if (path) + hits += (ft_printf("%s\n", path), xfree(path), 1); + } + return (hits); +} + +/* Returns 0 when something answered to `name`, 1 after "not found". */ +int which_one(t_shell *state, char *name, int flags) +{ + int hits; + + hits = 0; + if (!(flags & WHICH_PATH)) + hits = which_shell(state, name, (flags & WHICH_ALL) != 0); + if (!hits || (flags & WHICH_ALL)) + hits += which_paths(state, name, (flags & WHICH_ALL) != 0); + if (hits) + return (0); + ft_printf("%s not found\n", name); + return (1); +} diff --git a/src/builtins/builtins_private.h b/src/builtins/builtins_private.h index e34d8f1e..ae2e9b4c 100644 --- a/src/builtins/builtins_private.h +++ b/src/builtins/builtins_private.h @@ -337,6 +337,12 @@ int parse_redir_len(const char *arg); bool redir_needs_next(const char *arg); char *exe_path(char **path_dirs, char *exe_name, int *perm_denied); char *exe_path_preferred(char **path_dirs, char *exe_name, bool posix); +void path_cache_sync(t_shell *state); +char *func_text_trimmed(const char *t); +# define WHICH_ALL 1 +# define WHICH_PATH 2 + +int which_one(t_shell *state, char *name, int flags); char *command_v_lookup(t_shell *state, const char *name); int list_traps(t_shell *state); @@ -398,7 +404,7 @@ void gopt_init(t_shell *state, t_vec argv, t_getopts *g); int one_option(t_shell *state, t_vec argv, t_getopts *g, char *cur); int type_one_path(t_shell *state, const char *name); int type_one(t_shell *state, const char *name); -int type_is_builtin(const char *name); +int type_is_builtin(t_shell *state, const char *name); int type_is_keyword(const char *name); int type_find_in_path(t_shell *state, const char *name, char **out); int type_dispatch(t_shell *state, const char *name, char mode); diff --git a/src/builtins/hash_builtins_dispatch.c b/src/builtins/hash_builtins_dispatch.c index 76c7378f..3ffa299c 100644 --- a/src/builtins/hash_builtins_dispatch.c +++ b/src/builtins/hash_builtins_dispatch.c @@ -112,12 +112,20 @@ static void init_builtin_hash(t_hash *h) (the `!h.ctx` guard) and then reused for every subsequent lookup — so the cost is one hash + one pointer dereference in the steady state. Returns NULL when `name` is not a builtin, so callers can branch on that to decide - whether to fork a child process. */ -int (*builtin_func(char *name))(t_shell *state, t_vec argv) + whether to fork a child process. + `state` is what lets a name be a builtin in one dialect only: in zsh + mode the zsh-only table (hash_builtins_zsh.c) answers what this one does + not. Every caller goes through here, so `which`, `builtin which`, + `command which` and `type which` cannot disagree about it. */ +int (*builtin_func(t_shell *state, char *name))(t_shell *s, t_vec argv) { static t_hash h = {0}; + t_builtin_fn f; if (!h.ctx) init_builtin_hash(&h); - return ((t_builtin_fn)hash_get(&h, name)); + f = (t_builtin_fn)hash_get(&h, name); + if (!f && zsh_mode(state)) + f = zsh_builtin_func(name); + return (f); } diff --git a/src/builtins/hash_builtins_zsh.c b/src/builtins/hash_builtins_zsh.c index 9991ab2c..e973f71d 100644 --- a/src/builtins/hash_builtins_zsh.c +++ b/src/builtins/hash_builtins_zsh.c @@ -48,3 +48,22 @@ void fill_builtin_hash5(t_hash *h) hash_set(h, "compopt", (void *)builtin_compopt); hash_set(h, "bind", (void *)builtin_bind); } + +/* The one exception to the rule above: names that are ALSO an external + command. `which` is /usr/bin/which to a bash script -- `type which`, + `command -v which` and its output format all depend on that -- and a + zsh builtin to a zsh plugin (oh-my-zsh's sudo widget runs + `builtin which -a`). An unconditional registration would change what + the bash script runs, so these resolve only in zsh mode, through + builtin_func(). */ +int (*zsh_builtin_func(char *name))(t_shell *s, t_vec argv) +{ + static t_hash h = {0}; + + if (!h.ctx) + { + hash_init(&h, 4); + hash_set(&h, "which", (void *)builtin_which); + } + return ((t_builtin_fn)hash_get(&h, name)); +} diff --git a/src/builtins/help_data2.c b/src/builtins/help_data2.c index 11aee64f..1a5d0f6b 100644 --- a/src/builtins/help_data2.c +++ b/src/builtins/help_data2.c @@ -179,6 +179,9 @@ static const t_help g_help_2[] = { {"print", "zsh", "print [-nrlP] [--] [arg ...]", "zsh's echo: escapes on by default, -r turns them off"}, +{"which", "zsh", + "which [-ap] name ...", + "zsh mode only: what runs for name; -a every match, -p paths only"}, {"autoload", "zsh", "autoload [-Uz] name ...", "define a function from a file on $fpath"}, diff --git a/src/execution/execute_builtin.c b/src/execution/execute_builtin.c index 06f5f002..da65fc94 100644 --- a/src/execution/execute_builtin.c +++ b/src/execution/execute_builtin.c @@ -88,7 +88,8 @@ t_execution_state execute_builtin_cmd_fg(t_shell *state, need = prep_redir(state, exe, bak, persist); update_underscore_var(state, cmd); saves = apply_temp_assigns(state, &cmd->pre_assigns); - status = builtin_func(((char **)(cmd->argv.ctx))[0])(state, cmd->argv); + status = builtin_func(state, ((char **)(cmd->argv.ctx))[0])(state, + cmd->argv); restore_temp_assigns(state, &saves); if (need) restore_backup_fds(bak, persist); diff --git a/src/execution/execute_simple_command.c b/src/execution/execute_simple_command.c index 6f3f63a7..178d4420 100644 --- a/src/execution/execute_simple_command.c +++ b/src/execution/execute_simple_command.c @@ -112,7 +112,7 @@ static t_execution_state dispatch_cmd(t_shell *state, if (state->functions.len && func_lookup(state, argv0) && exe->modify_parent_ctx) return (handle_func_call(state, cmd, exe, func_lookup(state, argv0))); - if (builtin_func(argv0) && exe->modify_parent_ctx) + if (builtin_func(state, argv0) && exe->modify_parent_ctx) return (execute_builtin_cmd_fg(state, cmd, exe)); prehash_external(state, argv0); return (execute_cmd_bg(state, exe, cmd)); diff --git a/src/execution/find_cmd_path2.c b/src/execution/find_cmd_path2.c index d39de644..db31739a 100644 --- a/src/execution/find_cmd_path2.c +++ b/src/execution/find_cmd_path2.c @@ -74,7 +74,7 @@ void prehash_external(t_shell *state, char *argv0) char *path; int denied; - if (!argv0 || ft_strchr(argv0, '/') || builtin_func(argv0) + if (!argv0 || ft_strchr(argv0, '/') || builtin_func(state, argv0) || func_lookup(state, argv0)) return ; path_cache_sync(state); diff --git a/src/execution/run_utils2.c b/src/execution/run_utils2.c index 7aa65799..ecc39706 100644 --- a/src/execution/run_utils2.c +++ b/src/execution/run_utils2.c @@ -28,7 +28,7 @@ int run_builtin_or_continue(t_shell *state, t_vec *args) fn = func_lookup(state, ((char **)args->ctx)[0]); if (fn) exit(execute_func_call(state, fn, args).status); - bf = builtin_func(((char **)args->ctx)[0]); + bf = builtin_func(state, ((char **)args->ctx)[0]); if (bf) exit(bf(state, *args)); return (0); diff --git a/src/expander/cmdsub_inplace.c b/src/expander/cmdsub_inplace.c index f4ff5661..2564ffb2 100644 --- a/src/expander/cmdsub_inplace.c +++ b/src/expander/cmdsub_inplace.c @@ -33,7 +33,7 @@ static bool cs_word_opaque(t_shell *state, const char *s, int len) if (!w) return (true); bad = (ft_strchr(w, '$') != NULL || ft_strchr(w, '=') != NULL - || func_lookup(state, w) != NULL || builtin_func(w) != NULL); + || func_lookup(state, w) != NULL || builtin_func(state, w) != NULL); xfree(w); return (bad); } diff --git a/src/expander/expand_zsh_which.c b/src/expander/expand_zsh_which.c index 731cb7c0..58243bbf 100644 --- a/src/expander/expand_zsh_which.c +++ b/src/expander/expand_zsh_which.c @@ -31,7 +31,7 @@ char *zp_which(t_shell *state, char *name) if (!name || !*name) return (NULL); - if (builtin_func(name) || func_lookup(state, name)) + if (builtin_func(state, name) || func_lookup(state, name)) return (ft_strdup(name)); if (ft_strchr(name, '/')) { diff --git a/tests/omz_sudo_widget_test.py b/tests/omz_sudo_widget_test.py index 8ef9898e..f9af2648 100644 --- a/tests/omz_sudo_widget_test.py +++ b/tests/omz_sudo_widget_test.py @@ -106,7 +106,9 @@ def main(): print("SKIP omz sudo plugin: no cached copy and no network") return 0 rc = "PS1='P$ '\nEDITOR=vim\nexport EDITOR\n. %s\n" % path - t = Tty(SHELL, rc=rc, path=fakes() + ":" + os.environ.get("PATH", "")) + first, second = fakes(), fakes() + t = Tty(SHELL, rc=rc, + path=first + ":" + second + ":" + os.environ.get("PATH", "")) check("start/prompt", t.expect(b"P$ "), repr(t.out[-300:])) esc = b"\x1b\x1b" line(t, ["echo a1", esc, "\r"], b"FAKESUDO[echo a1]", @@ -125,8 +127,18 @@ def main(): t.expect(b"a6\r\nP$ ") line(t, [esc, "\r"], b"FAKESUDO[echo a6]", "empty-line/recalls-the-previous-command") + # $EDITOR is the SECOND vim on $PATH: neither `vim` nor ${vim:c} (the + # first one) equals it, so only the widget's `builtin which -a vim | + # grep -Fx` can tell it is the editor. That builtin used to be missing, + # and every Esc Esc printed "builtin: which: not a shell builtin". + t.send("EDITOR=%s/vim\r" % second) + t.expect(b"P$ ") + line(t, ["vim /tmp/f7", esc, "\r"], b"FAKESUDO[-e /tmp/f7]", + "editor/second-on-PATH-found-by-builtin-which") check("no-bad-substitution", b"bad substitution" not in t.out, repr(t.out[-400:])) + check("no-missing-builtin", b"not a shell builtin" not in t.out, + repr(t.out[-400:])) st = t.close() check("exit/not-signalled", not os.WIFSIGNALED(st)) print("%d failed" % len(FAILS)) diff --git a/wiki/builtins/index.md b/wiki/builtins/index.md index 2568fefb..9ba562fa 100644 --- a/wiki/builtins/index.md +++ b/wiki/builtins/index.md @@ -6,7 +6,7 @@ > (tools/gen_builtins_md.py), never edit it by hand. The help > table itself is test-enforced against the dispatch table, so > neither this page nor `help` can drift from what actually -> runs. 72 builtins; anything else on `$PATH` works as usual — +> runs. 73 builtins; anything else on `$PATH` works as usual — > `type NAME` says which is which. ## navigation @@ -156,6 +156,9 @@ **`.`** — `. filename [arguments]`
read and run a file in the current shell +**`bind`** — `bind [-lpsvPSVX] [-m keymap] [-f file] [-q name] [-u name] [-r keyseq] [-x keyseq:command] [keyseq:function | readline-line]` +
set key bindings and readline variables, or list them + **`compgen`** — `compgen [-abcdfkv] [-A action] [-W wordlist] [word]`
print the completions the shell would offer for word