diff --git a/Makefile.am b/Makefile.am index eeb24f821..ebb681700 100644 --- a/Makefile.am +++ b/Makefile.am @@ -103,6 +103,7 @@ dist_tmux_SOURCES = \ cmd-find-window.c \ cmd-find.c \ cmd-if-shell.c \ + cmd-invoke.c \ cmd-join-pane.c \ cmd-kill-pane.c \ cmd-kill-server.c \ diff --git a/cmd-invoke.c b/cmd-invoke.c new file mode 100644 index 000000000..be2af8f3d --- /dev/null +++ b/cmd-invoke.c @@ -0,0 +1,488 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#include "tmux.h" +#include "tmux-parser.h" + +/* One frame in the parse tree stack. */ +struct cmd_invoke_frame { + struct cmd_parse_node *node; + struct cmd_parse_node *next; + struct cmd_parse_node *end; +}; + +/* Shared state for a parsed command tree invocation. */ +struct cmd_invoke_state { + u_int references; + struct cmd_parse_tree *tree; + + struct cmd_invoke_frame *stack; + u_int nstack; + + enum cmd_retval last; + int have_last; + + int argc; + char **argv; + int parse_flags; + char *file; +}; + +static void cmd_invoke_push(struct cmd_invoke_state *, + struct cmd_parse_node *, struct cmd_parse_node *, + struct cmd_parse_node *); +static struct cmd_parse_node *cmd_invoke_next(struct cmd_invoke_state *); +static void cmd_invoke_skip_sequence(struct cmd_invoke_state *); +static int cmd_invoke_expand_string(struct cmdq_item *, + struct cmd_invoke_state *, struct cmd_parse_node *, + char **); +static int cmd_invoke_assignment(struct cmdq_item *, + struct cmd_invoke_state *, struct cmd_parse_node *); +static int cmd_invoke_if(struct cmdq_item *, struct cmd_invoke_state *, + struct cmd_parse_node *); +static struct cmd *cmd_invoke_build_command(struct cmdq_item *, + struct cmd_invoke_state *, struct cmd_parse_node *); + +/* Push a node's child range onto the traversal stack. */ +static void +cmd_invoke_push(struct cmd_invoke_state *is, struct cmd_parse_node *node, + struct cmd_parse_node *first, struct cmd_parse_node *end) +{ + u_int n = is->nstack + 1; + + is->stack = xreallocarray(is->stack, n, sizeof *is->stack); + is->stack[is->nstack].node = node; + is->stack[is->nstack].next = first; + is->stack[is->nstack].end = end; + is->nstack = n; +} + +/* Return the next node to execute from the traversal stack. */ +static struct cmd_parse_node * +cmd_invoke_next(struct cmd_invoke_state *is) +{ + struct cmd_invoke_frame *frame; + struct cmd_parse_node *node; + + for (;;) { + if (is->nstack == 0) + return (NULL); + frame = &is->stack[is->nstack - 1]; + if (frame->next != NULL && frame->next != frame->end) + break; + is->nstack--; + } + + node = frame->next; + frame->next = cmd_parse_node_next(node); + return (node); +} + +/* + * Skip the rest of the active sequence after a command failure. Failure scope + * is the active CMD_PARSE_SEQUENCE. Discard nested frames and advance only as + * far as the end of that sequence. + */ +static void +cmd_invoke_skip_sequence(struct cmd_invoke_state *is) +{ + enum cmd_parse_node_type type; + u_int i; + + for (i = is->nstack; i > 0; i--) { + type = cmd_parse_node_type(is->stack[i - 1].node); + if (type == CMD_PARSE_SEQUENCE) { + is->stack[i - 1].next = is->stack[i - 1].end; + is->nstack = i; + break; + } + } + is->have_last = 0; +} + +/* Append a string to a dynamically allocated buffer. */ +static void +cmd_invoke_append(char **buf, size_t *len, const char *s) +{ + size_t slen; + + if (s != NULL) { + slen = strlen(s); + *buf = xrealloc(*buf, *len + slen + 1); + memcpy(*buf + *len, s, slen + 1); + *len += slen; + } +} + +/* Look up an environment variable. */ +static const char * +cmd_invoke_getenv(struct cmdq_item *item, const char *name) +{ + struct client *c = cmdq_get_client(item); + struct environ_entry *envent; + + if (c != NULL && c->environ != NULL) { + envent = environ_find(c->environ, name); + if (envent != NULL && envent->value != NULL) + return (envent->value); + } + envent = environ_find(global_environ, name); + if (envent != NULL && envent->value != NULL) + return (envent->value); + return (""); +} + +/* Resolve a tilde expansion to a home directory. */ +static const char * +cmd_invoke_tilde(const char *name) +{ + struct passwd *pw; + + if (name == NULL || *name == '\0') + return (find_home()); + if ((pw = getpwnam(name)) == NULL) + return (""); + return (pw->pw_dir); +} + +/* Expand a parsed string node into an argv string. */ +static int +cmd_invoke_expand_string(struct cmdq_item *item, struct cmd_invoke_state *is, + struct cmd_parse_node *node, char **out) +{ + struct cmd_parse_node *child; + const char *s, *value; + char *buf = NULL, *new; + size_t len = 0; + int i; + + child = cmd_parse_node_first_child(node); + while (child != NULL) { + value = cmd_parse_node_value(child); + switch (cmd_parse_node_type(child)) { + case CMD_PARSE_TEXT: + s = value; + break; + case CMD_PARSE_ENVIRONMENT: + s = cmd_invoke_getenv(item, value); + break; + case CMD_PARSE_TILDE: + s = cmd_invoke_tilde(value); + break; + default: + fatalx("unexpected node type in string"); + } + cmd_invoke_append(&buf, &len, s); + child = cmd_parse_node_next(child); + } + if (buf == NULL) + buf = xstrdup(""); + for (i = 0; i < is->argc; i++) { + new = cmd_template_replace(buf, is->argv[i], i + 1); + free(buf); + buf = new; + } + *out = buf; + return (0); +} + +/* Execute an assignment node by updating the environment. */ +static int +cmd_invoke_assignment(struct cmdq_item *item, struct cmd_invoke_state *is, + struct cmd_parse_node *node) +{ + struct cmd_parse_node *value; + const char *name; + char *expanded; + int flags = 0; + + value = cmd_parse_node_first_child(node); + if (value == NULL) + return (-1); + if (cmd_invoke_expand_string(item, is, value, &expanded) != 0) + return (-1); + + name = cmd_parse_node_value(node); + if (cmd_parse_node_type(node) == CMD_PARSE_HIDDEN_ASSIGN) + flags |= ENVIRON_HIDDEN; + environ_set(global_environ, name, flags, "%s", expanded); + free(expanded); + return (0); +} + +/* Expand and evaluate a conditional expression. */ +static int +cmd_invoke_is_true(struct cmdq_item *item, + struct cmd_invoke_state *is, struct cmd_parse_node *node, int *result) +{ + struct format_tree *ft; + char *s, *expanded; + + if (cmd_invoke_expand_string(item, is, node, &s) != 0) + return (-1); + ft = format_create_from_target(item); + + expanded = format_expand(ft, s); + *result = format_true(expanded); + free(expanded); + + format_free(ft); + free(s); + return (0); +} + +/* Push the selected conditional branch onto the traversal stack. */ +static void +cmd_invoke_push_branch(struct cmd_invoke_state *is, + struct cmd_parse_node *node, struct cmd_parse_node *first) +{ + cmd_invoke_push(is, node, first, NULL); +} + +/* Select and queue the branch for a conditional node. */ +static int +cmd_invoke_if(struct cmdq_item *item, struct cmd_invoke_state *is, + struct cmd_parse_node *node) +{ + struct cmd_parse_node *child, *first, *next; + int r; + + next = cmd_parse_node_first_child(node); + if (next == NULL) + return (0); + first = cmd_parse_node_next(next); + if (cmd_invoke_is_true(item, is, next, &r) != 0) + return (-1); + if (r) { + cmd_invoke_push_branch(is, node, first); + return (0); + } + + for (child = first; child != NULL; child = cmd_parse_node_next(child)) { + switch (cmd_parse_node_type(child)) { + case CMD_PARSE_ELIF: + next = cmd_parse_node_first_child(child); + if (next == NULL) + break; + if (cmd_invoke_is_true(item, is, next, &r) != 0) + return (-1); + if (r) { + cmd_invoke_push_branch(is, child, next); + return (0); + } + break; + case CMD_PARSE_ELSE: + next = cmd_parse_node_first_child(child); + cmd_invoke_push_branch(is, child, next); + return (0); + default: + break; + } + } + return (0); +} + +/* Build one command from a parsed command node. */ +static struct cmd * +cmd_invoke_build_command(struct cmdq_item *item, struct cmd_invoke_state *is, + struct cmd_parse_node *node) +{ + struct cmd_parse_node *child; + struct args_value *values = NULL; + struct cmd *cmd; + char *cause = NULL; + u_int count = 0, i; + + child = cmd_parse_node_first_child(node); + while (child != NULL) { + values = xreallocarray(values, count + 1, sizeof *values); + memset(&values[count], 0, sizeof values[count]); + switch (cmd_parse_node_type(child)) { + case CMD_PARSE_STRING: + values[count].type = ARGS_STRING; + if (cmd_invoke_expand_string(item, is, child, + &values[count].string) != 0) + goto fail; + break; + case CMD_PARSE_COMMANDS: + values[count].type = ARGS_COMMANDS; + values[count].cmdparse = child; + break; + default: + fatalx("unexpected node type in command"); + } + count++; + child = cmd_parse_node_next(child); + } + + cmd = cmd_parse(values, count, is->file, cmd_parse_node_line(node), + is->parse_flags, &cause); + if (cmd == NULL) { + cmdq_error(item, "%s", cause); + free(cause); + goto fail; + } + for (i = 0; i < count; i++) { + if (values[i].type == ARGS_STRING) + free(values[i].string); + } + free(values); + return (cmd); + +fail: + for (i = 0; i < count; i++) { + if (values[i].type == ARGS_STRING) + free(values[i].string); + } + free(values); + return (NULL); +} + +/* Create the first invoke queue item for a parsed tree. */ +struct cmdq_item * +cmd_invoke_get(struct cmd_parse_tree *tree, struct cmdq_state *state, int argc, + char **argv) +{ + struct cmd_invoke_state *is; + struct cmd_parse_node *root = cmd_parse_root(tree), *first; + struct cmdq_item *item; + int i; + + is = xcalloc(1, sizeof *is); + is->references = 1; + is->tree = cmd_parse_add_ref(tree); + + is->argc = argc; + if (argc != 0) { + is->argv = xreallocarray(NULL, argc, sizeof *is->argv); + for (i = 0; i < argc; i++) + is->argv[i] = xstrdup(argv[i]); + } + + first = cmd_parse_node_first_child(root); + cmd_invoke_push(is, root, first, NULL); + + item = cmdq_get_invoke(is, state); + cmd_invoke_state_free(is); + return (item); +} + +/* Add a reference to shared invoke state. */ +struct cmd_invoke_state * +cmd_invoke_state_add_ref(struct cmd_invoke_state *is) +{ + is->references++; + return (is); +} + +/* Release a reference to shared invoke state. */ +void +cmd_invoke_state_free(struct cmd_invoke_state *is) +{ + int i; + + if (is == NULL) + return; + if (--is->references != 0) + return; + + for (i = 0; i < is->argc; i++) + free(is->argv[i]); + free(is->argv); + + cmd_parse_free(is->tree); + free(is->file); + free(is->stack); + free(is); +} + +/* Record the result from the last command item. */ +void +cmd_invoke_result(struct cmd_invoke_state *is, enum cmd_retval retval) +{ + is->last = retval; + is->have_last = 1; +} + +/* Fire an invoke item and queue the next command plus continuation. */ +enum cmd_retval +cmd_invoke_fire(struct cmdq_item *item, struct cmd_invoke_state *is) +{ + struct cmd_parse_node *node; + struct cmdq_item *new_item, *next; + struct cmdq_state *state; + struct cmd *cmd; + + if (is->have_last && is->last == CMD_RETURN_ERROR) + cmd_invoke_skip_sequence(is); + else + is->have_last = 0; + + for (;;) { + node = cmd_invoke_next(is); + if (node == NULL) + return (CMD_RETURN_NORMAL); + + switch (cmd_parse_node_type(node)) { + case CMD_PARSE_ROOT: + case CMD_PARSE_SEQUENCE: + cmd_invoke_push(is, node, + cmd_parse_node_first_child(node), NULL); + break; + case CMD_PARSE_ASSIGN: + case CMD_PARSE_HIDDEN_ASSIGN: + if (cmd_invoke_assignment(item, is, node) != 0) + return (CMD_RETURN_ERROR); + break; + case CMD_PARSE_IF: + if (cmd_invoke_if(item, is, node) != 0) + return (CMD_RETURN_ERROR); + break; + case CMD_PARSE_ELIF: + case CMD_PARSE_ELSE: + break; + case CMD_PARSE_COMMAND: + cmd = cmd_invoke_build_command(item, is, node); + if (cmd == NULL) + return (CMD_RETURN_ERROR); + + /* + * Queue one command followed by this walker. WAIT and + * command-inserted items therefore run before resume. + */ + state = cmdq_get_state(item); + new_item = cmdq_get_one_command(cmd, state, is); + next = cmdq_get_invoke(is, state); + cmdq_insert_after(item, next); + cmdq_insert_after(item, new_item); + return (CMD_RETURN_NORMAL); + case CMD_PARSE_STRING: + case CMD_PARSE_COMMANDS: + case CMD_PARSE_TEXT: + case CMD_PARSE_ENVIRONMENT: + case CMD_PARSE_TILDE: + fatalx("unexpected node type"); + } + } +} diff --git a/cmd-parse.y b/cmd-parse.y index 6d050f189..bef2c509b 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -675,7 +675,7 @@ cmd_parse_from_string(const char *s, struct cmd_parse_input *pi, char **cause) } struct cmd_parse_tree * -cmd_parse_addref(struct cmd_parse_tree *tree) +cmd_parse_add_ref(struct cmd_parse_tree *tree) { tree->references++; return (tree); diff --git a/cmd-queue.c b/cmd-queue.c index 2a221516d..c47dcbb42 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -35,6 +35,7 @@ enum cmdq_type { CMDQ_COMMAND, CMDQ_CALLBACK, + CMDQ_INVOKE, }; /* Command queue item. */ @@ -60,6 +61,7 @@ struct cmdq_item { struct cmd_list *cmdlist; struct cmd *cmd; + struct cmd_invoke_state *invoke_state; cmdq_cb cb; void *data; @@ -457,6 +459,8 @@ cmdq_remove(struct cmdq_item *item) server_client_unref(item->client); if (item->cmdlist != NULL) cmd_list_free(item->cmdlist); + if (item->invoke_state != NULL) + cmd_invoke_state_free(item->invoke_state); cmdq_free_state(item->state); TAILQ_REMOVE(&item->queue->list, item, entry); @@ -536,6 +540,41 @@ cmdq_get_command(struct cmd_list *cmdlist, struct cmdq_state *state) return (first); } +/* Get a single command for command queue with invoke state. */ +struct cmdq_item * +cmdq_get_one_command(struct cmd *cmd, struct cmdq_state *state, + struct cmd_invoke_state *invoke_state) +{ + struct cmd_list *cmdlist; + struct cmdq_item *item; + + cmdlist = cmd_list_new(); + cmd_list_append(cmdlist, cmd); + + item = cmdq_get_command(cmdlist, state); + cmd_list_free(cmdlist); + item->invoke_state = cmd_invoke_state_add_ref(invoke_state); + return (item); +} + +/* Get an invoke continuation for command queue. */ +struct cmdq_item * +cmdq_get_invoke(struct cmd_invoke_state *invoke_state, struct cmdq_state *state) +{ + struct cmdq_item *item; + + item = xcalloc(1, sizeof *item); + xasprintf(&item->name, "[invoke/%p]", item); + item->type = CMDQ_INVOKE; + item->group = 0; + if (state == NULL) + item->state = cmdq_new_state(NULL, NULL, 0); + else + item->state = cmdq_link_state(state); + item->invoke_state = cmd_invoke_state_add_ref(invoke_state); + return (item); +} + /* Fill in flag for a command. */ static enum cmd_retval cmdq_find_flag(struct cmdq_item *item, struct cmd_find_state *fs, @@ -772,6 +811,8 @@ cmdq_next(struct client *c) switch (item->type) { case CMDQ_COMMAND: retval = cmdq_fire_command(item); + if (item->invoke_state != NULL) + cmd_invoke_result(item->invoke_state, retval); /* * If a command returns an error, remove any @@ -783,6 +824,9 @@ cmdq_next(struct client *c) case CMDQ_CALLBACK: retval = cmdq_fire_callback(item); break; + case CMDQ_INVOKE: + retval = cmd_invoke_fire(item, item->invoke_state); + break; default: retval = CMD_RETURN_ERROR; break; diff --git a/tmux-parser.h b/tmux-parser.h index 5f16568a1..6de68e716 100644 --- a/tmux-parser.h +++ b/tmux-parser.h @@ -63,10 +63,10 @@ enum cmd_parse_node_type { struct cmd_parse_tree *cmd_parse_from_file(FILE *, struct cmd_parse_input *, char **); struct cmd_parse_tree *cmd_parse_from_buffer(const void *, size_t, - struct cmd_parse_input *, char **); + struct cmd_parse_input *, char **); struct cmd_parse_tree *cmd_parse_from_string(const char *, - struct cmd_parse_input *, char **); -struct cmd_parse_tree *cmd_parse_addref(struct cmd_parse_tree *); + struct cmd_parse_input *, char **); +struct cmd_parse_tree *cmd_parse_add_ref(struct cmd_parse_tree *); void cmd_parse_free(struct cmd_parse_tree *); struct cmd_parse_node *cmd_parse_root(struct cmd_parse_tree *); char *cmd_parse_print(const struct cmd_parse_tree *); diff --git a/tmux.h b/tmux.h index 863a41eca..45eed2c80 100644 --- a/tmux.h +++ b/tmux.h @@ -43,6 +43,9 @@ struct args_command_state; struct client; struct cmd; struct cmd_find_state; +struct cmd_invoke_state; +struct cmd_parse_node; +struct cmd_parse_tree; struct cmdq_item; struct cmdq_list; struct cmdq_state; @@ -3023,6 +3026,14 @@ struct cmd_parse_result *cmd_parse_from_buffer(const void *, size_t, struct cmd_parse_result *cmd_parse_from_arguments(struct args_value *, u_int, struct cmd_parse_input *); +/* cmd-invoke.c */ +struct cmdq_item *cmd_invoke_get(struct cmd_parse_tree *, struct cmdq_state *, + int, char **); +struct cmd_invoke_state *cmd_invoke_state_add_ref(struct cmd_invoke_state *); +void cmd_invoke_state_free(struct cmd_invoke_state *); +void cmd_invoke_result(struct cmd_invoke_state *, enum cmd_retval); +enum cmd_retval cmd_invoke_fire(struct cmdq_item *, struct cmd_invoke_state *); + /* cmd-queue.c */ struct cmdq_state *cmdq_new_state(struct cmd_find_state *, struct key_event *, int); @@ -3046,6 +3057,10 @@ struct key_event *cmdq_get_event(struct cmdq_item *); struct cmd_find_state *cmdq_get_current(struct cmdq_item *); int cmdq_get_flags(struct cmdq_item *); struct cmdq_item *cmdq_get_command(struct cmd_list *, struct cmdq_state *); +struct cmdq_item *cmdq_get_one_command(struct cmd *, struct cmdq_state *, + struct cmd_invoke_state *); +struct cmdq_item *cmdq_get_invoke(struct cmd_invoke_state *, + struct cmdq_state *); #define cmdq_get_callback(cb, data) cmdq_get_callback1(#cb, cb, data) struct cmdq_item *cmdq_get_callback1(const char *, cmdq_cb, void *); struct cmdq_item *cmdq_get_error(const char *);