/* $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) { next = cmd_parse_node_next(next); 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) { cmdq_error(item, "bad assignment"); cmd_invoke_skip_sequence(is); } break; case CMD_PARSE_IF: if (cmd_invoke_if(item, is, node) != 0) { cmdq_error(item, "bad condition"); cmd_invoke_skip_sequence(is); } 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) { cmd_invoke_skip_sequence(is); break; } /* * 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"); } } }