From 723010ba72e337832402f8e44981c02caa30b476 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 23 May 2019 11:13:30 +0000 Subject: [PATCH] Replace the split parser code (cfg.c and cmd-string.c) with a single parser using yacc(1). This is a major change but is clearer and simpler and allows some edge cases to be made more consistent, as well as tidying up how aliases are handled. It will also allow some further improvements later. Entirely the same parser is now used for parsing the configuration file and for string commands. This means that constructs previously only available in .tmux.conf, such as %if, can now be used in string commands (for example, those given to if-shell - not commands invoked from the shell, they are still parsed by the shell itself). The only syntax change I am aware of is that #{} outside quotes or a comment is now considered a format and not a comment, so #{ is now a syntax error (notably, if it is at the start of a line). This also adds two new sections to the man page documenting the syntax and outlining how parsing and command execution works. Thanks to everyone who sent me test configs (they still all parse without errors - but this doesn't mean they still work as intended!). Thanks to Avi Halachmi for testing and man page improvements, also to jmc@ for reviewing the man page changes. --- Makefile | 2 +- cfg.c | 236 ++------- cmd-command-prompt.c | 32 +- cmd-confirm-before.c | 33 +- cmd-display-panes.c | 35 +- cmd-if-shell.c | 91 ++-- cmd-list.c | 28 +- cmd-parse.y | 1207 ++++++++++++++++++++++++++++++++++++++++++ cmd-queue.c | 14 +- cmd-source-file.c | 10 +- cmd-string.c | 393 -------------- cmd.c | 188 ++++--- control.c | 22 +- key-bindings.c | 13 +- menu.c | 35 +- mode-tree.c | 27 +- options.c | 23 +- tmux.1 | 268 ++++++++-- tmux.h | 64 ++- 19 files changed, 1821 insertions(+), 900 deletions(-) create mode 100644 cmd-parse.y delete mode 100644 cmd-string.c diff --git a/Makefile b/Makefile index 90ecbd84..b15da7be 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ SRCS= alerts.c \ cmd-move-window.c \ cmd-new-session.c \ cmd-new-window.c \ + cmd-parse.y \ cmd-paste-buffer.c \ cmd-pipe-pane.c \ cmd-queue.c \ @@ -63,7 +64,6 @@ SRCS= alerts.c \ cmd-show-options.c \ cmd-source-file.c \ cmd-split-window.c \ - cmd-string.c \ cmd-swap-pane.c \ cmd-swap-window.c \ cmd-switch-client.c \ diff --git a/cfg.c b/cfg.c index 0b74ec35..aa5c13b7 100644 --- a/cfg.c +++ b/cfg.c @@ -27,17 +27,6 @@ #include "tmux.h" -/* Condition for %if, %elif, %else and %endif. */ -struct cfg_cond { - size_t line; /* line number of %if */ - int met; /* condition was met */ - int skip; /* skip later %elif/%else */ - int saw_else; /* saw a %else */ - - TAILQ_ENTRY(cfg_cond) entry; -}; -TAILQ_HEAD(cfg_conds, cfg_cond); - struct client *cfg_client; static char *cfg_file; int cfg_finished; @@ -86,9 +75,8 @@ start_cfg(void) struct client *c; /* - * Configuration files are loaded without a client, so NULL is passed - * into load_cfg() and commands run in the global queue with - * item->client NULL. + * Configuration files are loaded without a client, so commands are run + * in the global queue with item->client NULL. * * However, we must block the initial client (but just the initial * client) so that its command runs after the configuration is loaded. @@ -103,11 +91,11 @@ start_cfg(void) } if (cfg_file == NULL) - load_cfg(TMUX_CONF, NULL, NULL, CFG_QUIET, NULL); + load_cfg(TMUX_CONF, NULL, NULL, CMD_PARSE_QUIET, NULL); if (cfg_file == NULL && (home = find_home()) != NULL) { xasprintf(&cfg_file, "%s/.tmux.conf", home); - flags = CFG_QUIET; + flags = CMD_PARSE_QUIET; } if (cfg_file != NULL) load_cfg(cfg_file, NULL, NULL, flags, NULL); @@ -115,214 +103,54 @@ start_cfg(void) cmdq_append(NULL, cmdq_get_callback(cfg_done, NULL)); } -static int -cfg_check_cond(const char *path, size_t line, const char *p, int *skip, - struct client *c, struct cmd_find_state *fs) -{ - struct format_tree *ft; - char *s; - int result; - - while (isspace((u_char)*p)) - p++; - if (p[0] == '\0') { - cfg_add_cause("%s:%zu: invalid condition", path, line); - *skip = 1; - return (0); - } - - ft = format_create(NULL, NULL, FORMAT_NONE, FORMAT_NOJOBS); - if (fs != NULL) - format_defaults(ft, c, fs->s, fs->wl, fs->wp); - else - format_defaults(ft, c, NULL, NULL, NULL); - s = format_expand(ft, p); - result = format_true(s); - free(s); - format_free(ft); - - *skip = result; - return (result); -} - -static void -cfg_handle_if(const char *path, size_t line, struct cfg_conds *conds, - const char *p, struct client *c, struct cmd_find_state *fs) -{ - struct cfg_cond *cond; - struct cfg_cond *parent = TAILQ_FIRST(conds); - - /* - * Add a new condition. If a previous condition exists and isn't - * currently met, this new one also can't be met. - */ - cond = xcalloc(1, sizeof *cond); - cond->line = line; - if (parent == NULL || parent->met) - cond->met = cfg_check_cond(path, line, p, &cond->skip, c, fs); - else - cond->skip = 1; - cond->saw_else = 0; - TAILQ_INSERT_HEAD(conds, cond, entry); -} - -static void -cfg_handle_elif(const char *path, size_t line, struct cfg_conds *conds, - const char *p, struct client *c, struct cmd_find_state *fs) -{ - struct cfg_cond *cond = TAILQ_FIRST(conds); - - /* - * If a previous condition exists and wasn't met, check this - * one instead and change the state. - */ - if (cond == NULL || cond->saw_else) - cfg_add_cause("%s:%zu: unexpected %%elif", path, line); - else if (!cond->skip) - cond->met = cfg_check_cond(path, line, p, &cond->skip, c, fs); - else - cond->met = 0; -} - -static void -cfg_handle_else(const char *path, size_t line, struct cfg_conds *conds) -{ - struct cfg_cond *cond = TAILQ_FIRST(conds); - - /* - * If a previous condition exists and wasn't met and wasn't already - * %else, use this one instead. - */ - if (cond == NULL || cond->saw_else) { - cfg_add_cause("%s:%zu: unexpected %%else", path, line); - return; - } - cond->saw_else = 1; - cond->met = !cond->skip; - cond->skip = 1; -} - -static void -cfg_handle_endif(const char *path, size_t line, struct cfg_conds *conds) -{ - struct cfg_cond *cond = TAILQ_FIRST(conds); - - /* - * Remove previous condition if one exists. - */ - if (cond == NULL) { - cfg_add_cause("%s:%zu: unexpected %%endif", path, line); - return; - } - TAILQ_REMOVE(conds, cond, entry); - free(cond); -} - -static void -cfg_handle_directive(const char *p, const char *path, size_t line, - struct cfg_conds *conds, struct client *c, struct cmd_find_state *fs) -{ - int n = 0; - - while (p[n] != '\0' && !isspace((u_char)p[n])) - n++; - if (strncmp(p, "%if", n) == 0) - cfg_handle_if(path, line, conds, p + n, c, fs); - else if (strncmp(p, "%elif", n) == 0) - cfg_handle_elif(path, line, conds, p + n, c, fs); - else if (strcmp(p, "%else") == 0) - cfg_handle_else(path, line, conds); - else if (strcmp(p, "%endif") == 0) - cfg_handle_endif(path, line, conds); - else - cfg_add_cause("%s:%zu: invalid directive: %s", path, line, p); -} - int load_cfg(const char *path, struct client *c, struct cmdq_item *item, int flags, struct cmdq_item **new_item) { FILE *f; - const char delim[3] = { '\\', '\\', '\0' }; - u_int found = 0; - size_t line = 0; - char *buf, *cause1, *p, *q; - struct cmd_list *cmdlist; + struct cmd_parse_input pi; + struct cmd_parse_result *pr; struct cmdq_item *new_item0; - struct cfg_cond *cond, *cond1; - struct cfg_conds conds; - struct cmd_find_state *fs = NULL; - struct client *fc = NULL; - if (item != NULL) { - fs = &item->target; - fc = cmd_find_client(item, NULL, 1); - } - - TAILQ_INIT(&conds); + if (new_item != NULL) + *new_item = NULL; log_debug("loading %s", path); if ((f = fopen(path, "rb")) == NULL) { - if (errno == ENOENT && (flags & CFG_QUIET)) + if (errno == ENOENT && (flags & CMD_PARSE_QUIET)) return (0); cfg_add_cause("%s: %s", path, strerror(errno)); return (-1); } - while ((buf = fparseln(f, NULL, &line, delim, 0)) != NULL) { - log_debug("%s: %s", path, buf); + memset(&pi, 0, sizeof pi); + pi.flags = flags; + pi.file = path; - p = buf; - while (isspace((u_char)*p)) - p++; - if (*p == '\0') { - free(buf); - continue; - } - q = p + strlen(p) - 1; - while (q != p && isspace((u_char)*q)) - *q-- = '\0'; - - if (*p == '%') { - cfg_handle_directive(p, path, line, &conds, fc, fs); - continue; - } - cond = TAILQ_FIRST(&conds); - if (cond != NULL && !cond->met) - continue; - - cmdlist = cmd_string_parse(p, path, line, &cause1); - if (cmdlist == NULL) { - free(buf); - if (cause1 == NULL) - continue; - cfg_add_cause("%s:%zu: %s", path, line, cause1); - free(cause1); - continue; - } - free(buf); - - new_item0 = cmdq_get_command(cmdlist, NULL, NULL, 0); - if (item != NULL) { - cmdq_insert_after(item, new_item0); - item = new_item0; - } else - cmdq_append(c, new_item0); - cmd_list_free(cmdlist); - - found++; - } + pr = cmd_parse_from_file(f, &pi); fclose(f); - - TAILQ_FOREACH_REVERSE_SAFE(cond, &conds, cfg_conds, entry, cond1) { - cfg_add_cause("%s:%zu: unterminated %%if", path, cond->line); - TAILQ_REMOVE(&conds, cond, entry); - free(cond); + if (pr->status == CMD_PARSE_EMPTY) + return (0); + if (pr->status == CMD_PARSE_ERROR) { + cfg_add_cause("%s", pr->error); + free(pr->error); + return (-1); } + if (flags & CMD_PARSE_PARSEONLY) { + cmd_list_free(pr->cmdlist); + return (0); + } + + new_item0 = cmdq_get_command(pr->cmdlist, NULL, NULL, 0); + if (item != NULL) + cmdq_insert_after(item, new_item0); + else + cmdq_append(c, new_item0); + cmd_list_free(pr->cmdlist); if (new_item != NULL) - *new_item = item; - return (found); + *new_item = new_item0; + return (0); } void diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index a3cc22c8..603ddb0a 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -134,10 +134,10 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s, int done) { struct cmd_command_prompt_cdata *cdata = data; - struct cmd_list *cmdlist; struct cmdq_item *new_item; - char *cause, *new_template, *prompt, *ptr; + char *new_template, *prompt, *ptr; char *input = NULL; + struct cmd_parse_result *pr; if (s == NULL) return (0); @@ -164,20 +164,22 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s, return (1); } - cmdlist = cmd_string_parse(new_template, NULL, 0, &cause); - if (cmdlist == NULL) { - if (cause != NULL) - new_item = cmdq_get_error(cause); - else - new_item = NULL; - free(cause); - } else { - new_item = cmdq_get_command(cmdlist, NULL, NULL, 0); - cmd_list_free(cmdlist); - } - - if (new_item != NULL) + pr = cmd_parse_from_string(new_template, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + new_item = NULL; + break; + case CMD_PARSE_ERROR: + new_item = cmdq_get_error(pr->error); + free(pr->error); cmdq_append(c, new_item); + break; + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, NULL, NULL, 0); + cmd_list_free(pr->cmdlist); + cmdq_append(c, new_item); + break; + } if (!done) free(new_template); diff --git a/cmd-confirm-before.c b/cmd-confirm-before.c index 4017a6f9..be21a78b 100644 --- a/cmd-confirm-before.c +++ b/cmd-confirm-before.c @@ -87,32 +87,33 @@ cmd_confirm_before_callback(struct client *c, void *data, const char *s, __unused int done) { struct cmd_confirm_before_data *cdata = data; - struct cmd_list *cmdlist; struct cmdq_item *new_item; - char *cause; + struct cmd_parse_result *pr; if (c->flags & CLIENT_DEAD) return (0); if (s == NULL || *s == '\0') return (0); - if (tolower((u_char) s[0]) != 'y' || s[1] != '\0') + if (tolower((u_char)s[0]) != 'y' || s[1] != '\0') return (0); - cmdlist = cmd_string_parse(cdata->cmd, NULL, 0, &cause); - if (cmdlist == NULL) { - if (cause != NULL) - new_item = cmdq_get_error(cause); - else - new_item = NULL; - free(cause); - } else { - new_item = cmdq_get_command(cmdlist, NULL, NULL, 0); - cmd_list_free(cmdlist); - } - - if (new_item != NULL) + pr = cmd_parse_from_string(cdata->cmd, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + new_item = NULL; + break; + case CMD_PARSE_ERROR: + new_item = cmdq_get_error(pr->error); + free(pr->error); cmdq_append(c, new_item); + break; + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, NULL, NULL, 0); + cmd_list_free(pr->cmdlist); + cmdq_append(c, new_item); + break; + } return (0); } diff --git a/cmd-display-panes.c b/cmd-display-panes.c index 6e331ae1..aeeb6936 100644 --- a/cmd-display-panes.c +++ b/cmd-display-panes.c @@ -197,11 +197,11 @@ static int cmd_display_panes_key(struct client *c, struct key_event *event) { struct cmd_display_panes_data *cdata = c->overlay_data; - struct cmd_list *cmdlist; struct cmdq_item *new_item; - char *cmd, *expanded, *cause; + char *cmd, *expanded; struct window *w = c->session->curw->window; struct window_pane *wp; + struct cmd_parse_result *pr; if (event->key < '0' || event->key > '9') return (1); @@ -214,22 +214,21 @@ cmd_display_panes_key(struct client *c, struct key_event *event) xasprintf(&expanded, "%%%u", wp->id); cmd = cmd_template_replace(cdata->command, expanded, 1); - cmdlist = cmd_string_parse(cmd, NULL, 0, &cause); - if (cmdlist == NULL) { - if (cause != NULL) - new_item = cmdq_get_error(cause); - else - new_item = NULL; - free(cause); - } else { - new_item = cmdq_get_command(cmdlist, NULL, NULL, 0); - cmd_list_free(cmdlist); - } - if (new_item != NULL) { - if (cdata->item != NULL) - cmdq_insert_after(cdata->item, new_item); - else - cmdq_append(c, new_item); + pr = cmd_parse_from_string(cmd, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + new_item = NULL; + break; + case CMD_PARSE_ERROR: + new_item = cmdq_get_error(pr->error); + free(pr->error); + cmdq_append(c, new_item); + break; + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, NULL, NULL, 0); + cmd_list_free(pr->cmdlist); + cmdq_append(c, new_item); + break; } free(cmd); diff --git a/cmd-if-shell.c b/cmd-if-shell.c index 480912df..40e2b1c3 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -49,8 +49,7 @@ const struct cmd_entry cmd_if_shell_entry = { }; struct cmd_if_shell_data { - char *file; - u_int line; + struct cmd_parse_input input; char *cmd_if; char *cmd_else; @@ -64,51 +63,62 @@ static enum cmd_retval cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item) { struct args *args = self->args; - struct cmdq_shared *shared = item->shared; + struct mouse_event *m = &item->shared->mouse; struct cmd_if_shell_data *cdata; - char *shellcmd, *cmd, *cause; - struct cmd_list *cmdlist; + char *shellcmd, *cmd; struct cmdq_item *new_item; struct client *c = cmd_find_client(item, NULL, 1); struct session *s = item->target.s; struct winlink *wl = item->target.wl; struct window_pane *wp = item->target.wp; + struct cmd_parse_input pi; + struct cmd_parse_result *pr; shellcmd = format_single(item, args->argv[0], c, s, wl, wp); if (args_has(args, 'F')) { - cmd = NULL; if (*shellcmd != '0' && *shellcmd != '\0') cmd = args->argv[1]; else if (args->argc == 3) cmd = args->argv[2]; + else + cmd = NULL; free(shellcmd); if (cmd == NULL) return (CMD_RETURN_NORMAL); - cmdlist = cmd_string_parse(cmd, NULL, 0, &cause); - if (cmdlist == NULL) { - if (cause != NULL) { - cmdq_error(item, "%s", cause); - free(cause); - } + + memset(&pi, 0, sizeof pi); + if (self->file != NULL) + pi.file = self->file; + pi.line = self->line; + pi.item = item; + pi.c = c; + cmd_find_copy_state(&pi.fs, &item->target); + + pr = cmd_parse_from_string(cmd, &pi); + switch (pr->status) { + case CMD_PARSE_EMPTY: + break; + case CMD_PARSE_ERROR: + cmdq_error(item, "%s", pr->error); + free(pr->error); return (CMD_RETURN_ERROR); + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, NULL, m, 0); + cmdq_insert_after(item, new_item); + cmd_list_free(pr->cmdlist); + break; } - new_item = cmdq_get_command(cmdlist, NULL, &shared->mouse, 0); - cmdq_insert_after(item, new_item); - cmd_list_free(cmdlist); return (CMD_RETURN_NORMAL); } cdata = xcalloc(1, sizeof *cdata); - if (self->file != NULL) { - cdata->file = xstrdup(self->file); - cdata->line = self->line; - } cdata->cmd_if = xstrdup(args->argv[1]); if (args->argc == 3) cdata->cmd_else = xstrdup(args->argv[2]); else cdata->cmd_else = NULL; + memcpy(&cdata->mouse, m, sizeof cdata->mouse); cdata->client = item->client; if (cdata->client != NULL) @@ -118,7 +128,16 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item) cdata->item = item; else cdata->item = NULL; - memcpy(&cdata->mouse, &shared->mouse, sizeof cdata->mouse); + + memset(&cdata->input, 0, sizeof cdata->input); + if (self->file != NULL) + cdata->input.file = xstrdup(self->file); + cdata->input.line = self->line; + cdata->input.item = cdata->item; + cdata->input.c = c; + if (cdata->input.c != NULL) + cdata->input.c->references++; + cmd_find_copy_state(&cdata->input.fs, &item->target); if (job_run(shellcmd, s, server_client_get_cwd(item->client, s), NULL, cmd_if_shell_callback, cmd_if_shell_free, cdata, 0) == NULL) { @@ -139,11 +158,11 @@ cmd_if_shell_callback(struct job *job) { struct cmd_if_shell_data *cdata = job_get_data(job); struct client *c = cdata->client; - struct cmd_list *cmdlist; + struct mouse_event *m = &cdata->mouse; struct cmdq_item *new_item; - char *cause, *cmd, *file = cdata->file; - u_int line = cdata->line; + char *cmd; int status; + struct cmd_parse_result *pr; status = job_get_status(job); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) @@ -153,17 +172,20 @@ cmd_if_shell_callback(struct job *job) if (cmd == NULL) goto out; - cmdlist = cmd_string_parse(cmd, file, line, &cause); - if (cmdlist == NULL) { - if (cause != NULL && cdata->item != NULL) - cmdq_error(cdata->item, "%s", cause); - free(cause); + pr = cmd_parse_from_string(cmd, &cdata->input); + switch (pr->status) { + case CMD_PARSE_EMPTY: new_item = NULL; - } else { - new_item = cmdq_get_command(cmdlist, NULL, &cdata->mouse, 0); - cmd_list_free(cmdlist); + break; + case CMD_PARSE_ERROR: + new_item = cmdq_get_error(pr->error); + free(pr->error); + break; + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, NULL, m, 0); + cmd_list_free(pr->cmdlist); + break; } - if (new_item != NULL) { if (cdata->item == NULL) cmdq_append(c, new_item); @@ -187,6 +209,9 @@ cmd_if_shell_free(void *data) free(cdata->cmd_else); free(cdata->cmd_if); - free(cdata->file); + if (cdata->input.c != NULL) + server_client_unref(cdata->input.c); + free((void *)cdata->input.file); + free(cdata); } diff --git a/cmd-list.c b/cmd-list.c index 282533cf..ead0fb61 100644 --- a/cmd-list.c +++ b/cmd-list.c @@ -23,17 +23,39 @@ #include "tmux.h" -static struct cmd_list * +static u_int cmd_list_next_group = 1; + +struct cmd_list * cmd_list_new(void) { struct cmd_list *cmdlist; cmdlist = xcalloc(1, sizeof *cmdlist); cmdlist->references = 1; + cmdlist->group = cmd_list_next_group++; TAILQ_INIT(&cmdlist->list); return (cmdlist); } +void +cmd_list_append(struct cmd_list *cmdlist, struct cmd *cmd) +{ + cmd->group = cmdlist->group; + TAILQ_INSERT_TAIL(&cmdlist->list, cmd, qentry); +} + +void +cmd_list_move(struct cmd_list *cmdlist, struct cmd_list *from) +{ + struct cmd *cmd, *cmd1; + + TAILQ_FOREACH_SAFE(cmd, &from->list, qentry, cmd1) { + TAILQ_REMOVE(&from->list, cmd, qentry); + TAILQ_INSERT_TAIL(&cmdlist->list, cmd, qentry); + } + cmdlist->group = cmd_list_next_group++; +} + struct cmd_list * cmd_list_parse(int argc, char **argv, const char *file, u_int line, char **cause) @@ -100,9 +122,7 @@ cmd_list_free(struct cmd_list *cmdlist) TAILQ_FOREACH_SAFE(cmd, &cmdlist->list, qentry, cmd1) { TAILQ_REMOVE(&cmdlist->list, cmd, qentry); - args_free(cmd->args); - free(cmd->file); - free(cmd); + cmd_free(cmd); } free(cmdlist); diff --git a/cmd-parse.y b/cmd-parse.y new file mode 100644 index 00000000..8b8f33ab --- /dev/null +++ b/cmd-parse.y @@ -0,0 +1,1207 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2019 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 MIND, 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 +#include + +#include "tmux.h" + +static int yylex(void); +static int yyparse(void); +static int printflike(1,2) yyerror(const char *, ...); + +static char *yylex_token(int); +static char *yylex_format(void); + +struct cmd_parse_scope { + int flag; + TAILQ_ENTRY (cmd_parse_scope) entry; +}; + +struct cmd_parse_command { + char *name; + u_int line; + + int argc; + char **argv; + + TAILQ_ENTRY(cmd_parse_command) entry; +}; +TAILQ_HEAD(cmd_parse_commands, cmd_parse_command); + +struct cmd_parse_state { + FILE *f; + int eof; + struct cmd_parse_input *input; + u_int escapes; + + char *error; + struct cmd_parse_commands commands; + + struct cmd_parse_scope *scope; + TAILQ_HEAD(, cmd_parse_scope) stack; +}; +static struct cmd_parse_state parse_state; + +static char *cmd_parse_get_error(const char *, u_int, const char *); +static char *cmd_parse_get_strerror(const char *, u_int); +static void cmd_parse_free_command(struct cmd_parse_command *); +static void cmd_parse_free_commands(struct cmd_parse_commands *); + +%} + +%union +{ + char *token; + struct { + int argc; + char **argv; + } arguments; + int flag; + struct { + int flag; + struct cmd_parse_commands commands; + } elif; + struct cmd_parse_commands commands; + struct cmd_parse_command *command; +} + +%token ERROR +%token IF +%token ELSE +%token ELIF +%token ENDIF +%token FORMAT TOKEN EQUALS + +%type argument expanded +%type arguments +%type if_open if_elif +%type elif elif1 +%type statements statement commands condition condition1 +%type command + +%% + +lines : /* empty */ + | statements + { + struct cmd_parse_state *ps = &parse_state; + + TAILQ_CONCAT(&ps->commands, &$1, entry); + } + +statements : statement '\n' + { + TAILQ_INIT(&$$); + TAILQ_CONCAT(&$$, &$1, entry); + } + | statements statement '\n' + { + TAILQ_INIT(&$$); + TAILQ_CONCAT(&$$, &$1, entry); + TAILQ_CONCAT(&$$, &$2, entry); + } + + +statement : condition + { + struct cmd_parse_state *ps = &parse_state; + + TAILQ_INIT(&$$); + if (ps->scope == NULL || ps->scope->flag) + TAILQ_CONCAT(&$$, &$1, entry); + else + cmd_parse_free_commands(&$1); + } + | assignment + { + TAILQ_INIT(&$$); + } + | commands + { + struct cmd_parse_state *ps = &parse_state; + + TAILQ_INIT(&$$); + if (ps->scope == NULL || ps->scope->flag) + TAILQ_CONCAT(&$$, &$1, entry); + else + cmd_parse_free_commands(&$1); + } + +expanded : FORMAT + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; + struct format_tree *ft; + struct client *c = pi->c; + struct cmd_find_state *fs; + int flags = FORMAT_NOJOBS; + + if (cmd_find_valid_state(&pi->fs)) + fs = &pi->fs; + else + fs = NULL; + ft = format_create(NULL, pi->item, FORMAT_NONE, flags); + if (fs != NULL) + format_defaults(ft, c, fs->s, fs->wl, fs->wp); + else + format_defaults(ft, c, NULL, NULL, NULL); + + $$ = format_expand(ft, $1); + format_free(ft); + free($1); + } + +assignment : /* empty */ + | EQUALS + { + struct cmd_parse_state *ps = &parse_state; + int flags = ps->input->flags; + + if ((~flags & CMD_PARSE_PARSEONLY) && + (ps->scope == NULL || ps->scope->flag)) + environ_put(global_environ, $1); + free($1); + } + +if_open : IF expanded + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_scope *scope; + + scope = xmalloc(sizeof *scope); + $$ = scope->flag = format_true($2); + free($2); + + if (ps->scope != NULL) + TAILQ_INSERT_HEAD(&ps->stack, ps->scope, entry); + ps->scope = scope; + } + +if_else : ELSE + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_scope *scope; + + scope = xmalloc(sizeof *scope); + scope->flag = !ps->scope->flag; + + free(ps->scope); + ps->scope = scope; + } + +if_elif : ELIF expanded + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_scope *scope; + + scope = xmalloc(sizeof *scope); + $$ = scope->flag = format_true($2); + free($2); + + free(ps->scope); + ps->scope = scope; + } + +if_close : ENDIF + { + struct cmd_parse_state *ps = &parse_state; + + free(ps->scope); + ps->scope = TAILQ_FIRST(&ps->stack); + if (ps->scope != NULL) + TAILQ_REMOVE(&ps->stack, ps->scope, entry); + } + +condition : if_open '\n' statements if_close + { + TAILQ_INIT(&$$); + if ($1) + TAILQ_CONCAT(&$$, &$3, entry); + else + cmd_parse_free_commands(&$3); + } + | if_open '\n' statements if_else '\n' statements if_close + { + TAILQ_INIT(&$$); + if ($1) { + TAILQ_CONCAT(&$$, &$3, entry); + cmd_parse_free_commands(&$6); + } else { + TAILQ_CONCAT(&$$, &$6, entry); + cmd_parse_free_commands(&$3); + } + } + | if_open '\n' statements elif if_close + { + TAILQ_INIT(&$$); + if ($1) { + TAILQ_CONCAT(&$$, &$3, entry); + cmd_parse_free_commands(&$4.commands); + } else if ($4.flag) { + TAILQ_CONCAT(&$$, &$4.commands, entry); + cmd_parse_free_commands(&$3); + } else { + cmd_parse_free_commands(&$3); + cmd_parse_free_commands(&$4.commands); + } + } + | if_open '\n' statements elif if_else '\n' statements if_close + { + TAILQ_INIT(&$$); + if ($1) { + TAILQ_CONCAT(&$$, &$3, entry); + cmd_parse_free_commands(&$4.commands); + cmd_parse_free_commands(&$7); + } else if ($4.flag) { + TAILQ_CONCAT(&$$, &$4.commands, entry); + cmd_parse_free_commands(&$3); + cmd_parse_free_commands(&$7); + } else { + TAILQ_CONCAT(&$$, &$7, entry); + cmd_parse_free_commands(&$3); + cmd_parse_free_commands(&$4.commands); + } + } + +elif : if_elif '\n' statements + { + TAILQ_INIT(&$$.commands); + if ($1) + TAILQ_CONCAT(&$$.commands, &$3, entry); + else + cmd_parse_free_commands(&$3); + $$.flag = $1; + } + | if_elif '\n' statements elif + { + TAILQ_INIT(&$$.commands); + if ($1) { + $$.flag = 1; + TAILQ_CONCAT(&$$.commands, &$3, entry); + cmd_parse_free_commands(&$4.commands); + } else { + $$.flag = $4.flag; + TAILQ_CONCAT(&$$.commands, &$4.commands, entry); + cmd_parse_free_commands(&$3); + } + } + + +commands : command + { + struct cmd_parse_state *ps = &parse_state; + + TAILQ_INIT(&$$); + if (ps->scope == NULL || ps->scope->flag) + TAILQ_INSERT_TAIL(&$$, $1, entry); + else + cmd_parse_free_command($1); + } + | commands ';' + { + TAILQ_INIT(&$$); + TAILQ_CONCAT(&$$, &$1, entry); + } + | commands ';' condition1 + { + TAILQ_INIT(&$$); + TAILQ_CONCAT(&$$, &$1, entry); + TAILQ_CONCAT(&$$, &$3, entry); + } + | commands ';' command + { + struct cmd_parse_state *ps = &parse_state; + + TAILQ_INIT(&$$); + if (ps->scope == NULL || ps->scope->flag) { + TAILQ_CONCAT(&$$, &$1, entry); + TAILQ_INSERT_TAIL(&$$, $3, entry); + } else { + cmd_parse_free_commands(&$1); + cmd_parse_free_command($3); + } + } + | condition1 + { + TAILQ_INIT(&$$); + TAILQ_CONCAT(&$$, &$1, entry); + } + +command : assignment TOKEN + { + struct cmd_parse_state *ps = &parse_state; + + $$ = xcalloc(1, sizeof *$$); + $$->name = $2; + $$->line = ps->input->line; + + } + | assignment TOKEN arguments + { + struct cmd_parse_state *ps = &parse_state; + + $$ = xcalloc(1, sizeof *$$); + $$->name = $2; + $$->line = ps->input->line; + + $$->argc = $3.argc; + $$->argv = $3.argv; + } + +condition1 : if_open commands if_close + { + TAILQ_INIT(&$$); + if ($1) + TAILQ_CONCAT(&$$, &$2, entry); + else + cmd_parse_free_commands(&$2); + } + | if_open commands if_else commands if_close + { + TAILQ_INIT(&$$); + if ($1) { + TAILQ_CONCAT(&$$, &$2, entry); + cmd_parse_free_commands(&$4); + } else { + TAILQ_CONCAT(&$$, &$4, entry); + cmd_parse_free_commands(&$2); + } + } + | if_open commands elif1 if_close + { + TAILQ_INIT(&$$); + if ($1) { + TAILQ_CONCAT(&$$, &$2, entry); + cmd_parse_free_commands(&$3.commands); + } else if ($3.flag) { + TAILQ_CONCAT(&$$, &$3.commands, entry); + cmd_parse_free_commands(&$2); + } else { + cmd_parse_free_commands(&$2); + cmd_parse_free_commands(&$3.commands); + } + } + | if_open commands elif1 if_else commands if_close + { + TAILQ_INIT(&$$); + if ($1) { + TAILQ_CONCAT(&$$, &$2, entry); + cmd_parse_free_commands(&$3.commands); + cmd_parse_free_commands(&$5); + } else if ($3.flag) { + TAILQ_CONCAT(&$$, &$3.commands, entry); + cmd_parse_free_commands(&$2); + cmd_parse_free_commands(&$5); + } else { + TAILQ_CONCAT(&$$, &$5, entry); + cmd_parse_free_commands(&$2); + cmd_parse_free_commands(&$3.commands); + } + + } + +elif1 : if_elif commands + { + TAILQ_INIT(&$$.commands); + if ($1) + TAILQ_CONCAT(&$$.commands, &$2, entry); + else + cmd_parse_free_commands(&$2); + $$.flag = $1; + } + | if_elif commands elif1 + { + TAILQ_INIT(&$$.commands); + if ($1) { + $$.flag = 1; + TAILQ_CONCAT(&$$.commands, &$2, entry); + cmd_parse_free_commands(&$3.commands); + } else { + $$.flag = $3.flag; + TAILQ_CONCAT(&$$.commands, &$3.commands, entry); + cmd_parse_free_commands(&$2); + } + } + +arguments : argument + { + $$.argc = 1; + $$.argv = xreallocarray(NULL, 1, sizeof *$$.argv); + + $$.argv[0] = $1; + } + | argument arguments + { + cmd_prepend_argv(&$2.argc, &$2.argv, $1); + free($1); + $$ = $2; + } + +argument : TOKEN + { + $$ = $1; + } + | EQUALS + { + $$ = $1; + } + +%% + +static char * +cmd_parse_get_error(const char *file, u_int line, const char *error) +{ + char *s; + + if (file == NULL) + s = xstrdup(error); + else + xasprintf (&s, "%s:%u: %s", file, line, error); + return (s); +} + +static char * +cmd_parse_get_strerror(const char *file, u_int line) +{ + return (cmd_parse_get_error(file, line, strerror(errno))); +} + +static void +cmd_parse_free_command(struct cmd_parse_command *cmd) +{ + free(cmd->name); + cmd_free_argv(cmd->argc, cmd->argv); + free(cmd); +} + +static void +cmd_parse_free_commands(struct cmd_parse_commands *cmds) +{ + struct cmd_parse_command *cmd, *cmd1; + + TAILQ_FOREACH_SAFE(cmd, cmds, entry, cmd1) { + TAILQ_REMOVE(cmds, cmd, entry); + cmd_parse_free_command(cmd); + } +} + +static struct cmd_parse_commands * +cmd_parse_run_parser(FILE *f, struct cmd_parse_input *pi, char **cause) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_commands *cmds; + struct cmd_parse_scope *scope, *scope1; + int retval; + + memset(ps, 0, sizeof *ps); + + ps->f = f; + ps->eof = 0; + ps->input = pi; + + TAILQ_INIT(&ps->commands); + TAILQ_INIT(&ps->stack); + + retval = yyparse(); + TAILQ_FOREACH_SAFE(scope, &ps->stack, entry, scope1) { + TAILQ_REMOVE(&ps->stack, scope, entry); + free(scope); + } + if (retval != 0) { + *cause = ps->error; + return (NULL); + } + + cmds = xmalloc(sizeof *cmds); + TAILQ_INIT(cmds); + TAILQ_CONCAT(cmds, &ps->commands, entry); + return (cmds); +} + +struct cmd_parse_result * +cmd_parse_from_file(FILE *f, struct cmd_parse_input *pi) +{ + static struct cmd_parse_result pr; + struct cmd_parse_input input; + struct cmd_parse_commands *cmds, *cmds2; + struct cmd_parse_command *cmd, *cmd2, *next, *next2, *after; + u_int line = UINT_MAX; + int i; + struct cmd_list *cmdlist = NULL, *result; + struct cmd *add; + char *alias, *cause, *s; + + if (pi == NULL) { + memset(&input, 0, sizeof input); + pi = &input; + } + memset(&pr, 0, sizeof pr); + + /* + * Parse the file into a list of commands. + */ + cmds = cmd_parse_run_parser(f, pi, &cause); + if (cmds == NULL) { + pr.status = CMD_PARSE_ERROR; + pr.error = cause; + return (&pr); + } + if (TAILQ_EMPTY(cmds)) { + free(cmds); + pr.status = CMD_PARSE_EMPTY; + return (&pr); + } + + /* + * Walk the commands and expand any aliases. Each alias is parsed + * individually to a new command list, any trailing arguments appended + * to the last command, and all commands inserted into the original + * command list. + */ + TAILQ_FOREACH_SAFE(cmd, cmds, entry, next) { + alias = cmd_get_alias(cmd->name); + if (alias == NULL) + continue; + + line = cmd->line; + log_debug("%s: %u %s = %s", __func__, line, cmd->name, alias); + + f = fmemopen(alias, strlen(alias), "r"); + if (f == NULL) { + free(alias); + pr.status = CMD_PARSE_ERROR; + pr.error = cmd_parse_get_strerror(pi->file, line); + goto out; + } + pi->line = line; + cmds2 = cmd_parse_run_parser(f, pi, &cause); + fclose(f); + free(alias); + if (cmds2 == NULL) { + pr.status = CMD_PARSE_ERROR; + pr.error = cause; + goto out; + } + + cmd2 = TAILQ_LAST(cmds2, cmd_parse_commands); + if (cmd2 == NULL) { + TAILQ_REMOVE(cmds, cmd, entry); + cmd_parse_free_command(cmd); + continue; + } + for (i = 0; i < cmd->argc; i++) + cmd_append_argv(&cmd2->argc, &cmd2->argv, cmd->argv[i]); + + after = cmd; + TAILQ_FOREACH_SAFE(cmd2, cmds2, entry, next2) { + cmd2->line = line; + TAILQ_REMOVE(cmds2, cmd2, entry); + TAILQ_INSERT_AFTER(cmds, after, cmd2, entry); + after = cmd2; + } + cmd_parse_free_commands(cmds2); + + TAILQ_REMOVE(cmds, cmd, entry); + cmd_parse_free_command(cmd); + } + + /* + * Parse each command into a command list. Create a new command list + * for each line so they get a new group (so the queue knows which ones + * to remove if a command fails when executed). + */ + result = cmd_list_new(); + TAILQ_FOREACH(cmd, cmds, entry) { + log_debug("%s: %u %s", __func__, cmd->line, cmd->name); + cmd_log_argv(cmd->argc, cmd->argv, __func__); + + if (cmdlist == NULL || cmd->line != line) { + if (cmdlist != NULL) { + cmd_list_move(result, cmdlist); + cmd_list_free(cmdlist); + } + cmdlist = cmd_list_new(); + } + line = cmd->line; + + cmd_prepend_argv(&cmd->argc, &cmd->argv, cmd->name); + add = cmd_parse(cmd->argc, cmd->argv, pi->file, line, &cause); + if (add == NULL) { + cmd_list_free(result); + pr.status = CMD_PARSE_ERROR; + pr.error = cmd_parse_get_error(pi->file, line, cause); + free(cause); + goto out; + } + cmd_list_append(cmdlist, add); + } + if (cmdlist != NULL) { + cmd_list_move(result, cmdlist); + cmd_list_free(cmdlist); + } + + s = cmd_list_print(result); + log_debug("%s: %s", __func__, s); + free(s); + + pr.status = CMD_PARSE_SUCCESS; + pr.cmdlist = result; + +out: + cmd_parse_free_commands(cmds); + free(cmds); + + return (&pr); +} + +struct cmd_parse_result * +cmd_parse_from_string(const char *s, struct cmd_parse_input *pi) +{ + static struct cmd_parse_result pr; + struct cmd_parse_result *prp; + FILE *f; + + if (*s == '\0') { + pr.status = CMD_PARSE_EMPTY; + pr.cmdlist = NULL; + pr.error = NULL; + return (&pr); + } + + f = fmemopen((void *)s, strlen(s), "r"); + if (f == NULL) { + pr.status = CMD_PARSE_ERROR; + pr.cmdlist = NULL; + pr.error = cmd_parse_get_strerror(pi->file, pi->line); + return (NULL); + } + prp = cmd_parse_from_file(f, pi); + fclose(f); + return (prp); +} + +static int printflike(1, 2) +yyerror(const char *fmt, ...) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; + va_list ap; + char *error; + + if (ps->error != NULL) + return (0); + + va_start(ap, fmt); + xvasprintf(&error, fmt, ap); + va_end(ap); + + ps->error = cmd_parse_get_error(pi->file, pi->line, error); + free(error); + return (0); +} + +static int +yylex_is_var(char ch, int first) +{ + if (ch == '=') + return (0); + if (first && isdigit((u_char)ch)) + return (0); + return (isalnum((u_char)ch) || ch == '_'); +} + +static void +yylex_append(char **buf, size_t *len, const char *add, size_t addlen) +{ + if (addlen > SIZE_MAX - 1 || *len > SIZE_MAX - 1 - addlen) + fatalx("buffer is too big"); + *buf = xrealloc(*buf, (*len) + 1 + addlen); + memcpy((*buf) + *len, add, addlen); + (*len) += addlen; +} + +static void +yylex_append1(char **buf, size_t *len, char add) +{ + yylex_append(buf, len, &add, 1); +} + +static int +yylex_getc(void) +{ + struct cmd_parse_state *ps = &parse_state; + int ch; + + if (ps->escapes != 0) { + ps->escapes--; + return ('\\'); + } + for (;;) { + ch = getc(ps->f); + if (ch == '\\') { + ps->escapes++; + continue; + } + if (ch == '\n' && (ps->escapes % 2) == 1) { + ps->input->line++; + ps->escapes--; + continue; + } + + if (ps->escapes != 0) { + ungetc(ch, ps->f); + ps->escapes--; + return ('\\'); + } + return (ch); + } +} + +static char * +yylex_get_word(int ch) +{ + struct cmd_parse_state *ps = &parse_state; + char *buf; + size_t len; + + len = 0; + buf = xmalloc(1); + + do + yylex_append1(&buf, &len, ch); + while ((ch = yylex_getc()) != EOF && strchr(" \t\n", ch) == NULL); + ungetc(ch, ps->f); + + buf[len] = '\0'; + log_debug("%s: %s", __func__, buf); + return (buf); +} + +static int +yylex(void) +{ + struct cmd_parse_state *ps = &parse_state; + char *token, *cp; + int ch, next; + + for (;;) { + ch = yylex_getc(); + + if (ch == EOF) { + /* + * Ensure every file or string is terminated by a + * newline. This keeps the parser simpler and avoids + * having to add a newline to each string. + */ + if (ps->eof) + break; + ps->eof = 1; + return ('\n'); + } + + if (ch == ' ' || ch == '\t') { + /* + * Ignore whitespace. + */ + continue; + } + + if (ch == '\n') { + /* + * End of line. Update the line number. + */ + ps->input->line++; + return ('\n'); + } + + if (ch == ';') { + /* + * A semicolon is itself. + */ + return (';'); + } + + if (ch == '#') { + /* + * #{ opens a format; anything else is a comment, + * ignore up to the end of the line. + */ + next = yylex_getc(); + if (next == '{') { + yylval.token = yylex_format(); + if (yylval.token == NULL) + return (ERROR); + return (FORMAT); + } + while (next != '\n' && next != EOF) + next = yylex_getc(); + if (next == '\n') { + ps->input->line++; + return ('\n'); + } + continue; + } + + if (ch == '%') { + /* + * % is a condition unless it is alone, then it is a + * token. + */ + yylval.token = yylex_get_word('%'); + if (strcmp(yylval.token, "%") == 0) + return (TOKEN); + if (strcmp(yylval.token, "%if") == 0) { + free(yylval.token); + return (IF); + } + if (strcmp(yylval.token, "%else") == 0) { + free(yylval.token); + return (ELSE); + } + if (strcmp(yylval.token, "%elif") == 0) { + free(yylval.token); + return (ELIF); + } + if (strcmp(yylval.token, "%endif") == 0) { + free(yylval.token); + return (ENDIF); + } + free(yylval.token); + return (ERROR); + } + + /* + * Otherwise this is a token. + */ + token = yylex_token(ch); + if (token == NULL) + return (ERROR); + yylval.token = token; + + if (strchr(token, '=') != NULL && yylex_is_var(*token, 1)) { + for (cp = token + 1; *cp != '='; cp++) { + if (!yylex_is_var(*cp, 0)) + break; + } + if (*cp == '=') + return (EQUALS); + } + return (TOKEN); + } + return (0); +} + +static char * +yylex_format(void) +{ + char *buf; + size_t len; + int ch, brackets = 1; + + len = 0; + buf = xmalloc(1); + + yylex_append(&buf, &len, "#{", 2); + for (;;) { + if ((ch = yylex_getc()) == EOF || ch == '\n') + goto error; + if (ch == '#') { + if ((ch = yylex_getc()) == EOF || ch == '\n') + goto error; + if (ch == '{') + brackets++; + yylex_append1(&buf, &len, '#'); + } else if (ch == '}') { + if (brackets != 0 && --brackets == 0) { + yylex_append1(&buf, &len, ch); + break; + } + } + yylex_append1(&buf, &len, ch); + } + if (brackets != 0) + goto error; + + buf[len] = '\0'; + log_debug("%s: %s", __func__, buf); + return (buf); + +error: + free(buf); + return (NULL); +} + +static int +yylex_token_escape(char **buf, size_t *len) +{ + int ch, type; + u_int size, i, tmp; + char s[9]; + struct utf8_data ud; + + switch (ch = yylex_getc()) { + case EOF: + return (0); + case 'e': + ch = '\033'; + break; + case 'r': + ch = '\r'; + break; + case 'n': + ch = '\n'; + break; + case 't': + ch = '\t'; + break; + case 'u': + type = 'u'; + size = 4; + goto unicode; + case 'U': + type = 'U'; + size = 8; + goto unicode; + } + + yylex_append1(buf, len, ch); + return (1); + +unicode: + for (i = 0; i < size; i++) { + ch = yylex_getc(); + if (ch == EOF || ch == '\n') + return (0); + if (!isxdigit((u_char)ch)) { + yyerror("invalid \\%c argument", type); + return (0); + } + s[i] = ch; + } + s[i] = '\0'; + + if ((size == 4 && sscanf(s, "%4x", &tmp) != 1) || + (size == 8 && sscanf(s, "%8x", &tmp) != 1)) { + yyerror("invalid \\%c argument", type); + return (0); + } + if (utf8_split(tmp, &ud) != UTF8_DONE) { + yyerror("invalid \\%c argument", type); + return (0); + } + yylex_append(buf, len, ud.data, ud.size); + return (1); +} + +static int +yylex_token_variable(char **buf, size_t *len) +{ + struct cmd_parse_state *ps = &parse_state; + struct environ_entry *envent; + int ch, brackets = 0; + char name[BUFSIZ]; + size_t namelen = 0; + const char *value; + + ch = yylex_getc(); + if (ch == EOF) + return (0); + if (ch == '{') + brackets = 1; + else { + if (!yylex_is_var(ch, 1)) { + yylex_append1(buf, len, '$'); + ungetc(ch, ps->f); + return (1); + } + name[namelen++] = ch; + } + + for (;;) { + ch = yylex_getc(); + if (brackets && ch == '}') + break; + if (ch == EOF || !yylex_is_var(ch, 0)) { + if (!brackets) { + ungetc(ch, ps->f); + break; + } + yyerror("invalid environment variable"); + return (0); + } + if (namelen == (sizeof name) - 2) { + yyerror("environment variable is too long"); + return (0); + } + name[namelen++] = ch; + } + name[namelen] = '\0'; + + envent = environ_find(global_environ, name); + if (envent != NULL) { + value = envent->value; + log_debug("%s: %s -> %s", __func__, name, value); + yylex_append(buf, len, value, strlen(value)); + } + return (1); +} + +static int +yylex_token_tilde(char **buf, size_t *len) +{ + struct cmd_parse_state *ps = &parse_state; + struct environ_entry *envent; + int ch; + char name[BUFSIZ]; + size_t namelen = 0; + struct passwd *pw; + const char *home = NULL; + + for (;;) { + ch = yylex_getc(); + if (ch == EOF || strchr("/ \t\n\"'", ch) != NULL) { + ungetc(ch, ps->f); + break; + } + if (namelen == (sizeof name) - 2) { + yyerror("user name is too long"); + return (0); + } + name[namelen++] = ch; + } + name[namelen] = '\0'; + + if (*name == '\0') { + envent = environ_find(global_environ, "HOME"); + if (envent != NULL && *envent->value != '\0') + home = envent->value; + else if ((pw = getpwuid(getuid())) != NULL) + home = pw->pw_dir; + } else { + if ((pw = getpwnam(name)) != NULL) + home = pw->pw_dir; + } + if (home == NULL) + return (0); + + log_debug("%s: ~%s -> %s", __func__, name, home); + yylex_append(buf, len, home, strlen(home)); + return (1); +} + +static char * +yylex_token(int ch) +{ + struct cmd_parse_state *ps = &parse_state; + char *buf; + size_t len; + enum { START, + NONE, + DOUBLE_QUOTES, + SINGLE_QUOTES } state = NONE, last = START; + + len = 0; + buf = xmalloc(1); + + for (;;) { + /* + * EOF or \n are always the end of the token. If inside quotes + * they are an error. + */ + if (ch == EOF || ch == '\n') { + if (state != NONE) + goto error; + break; + } + + /* Whitespace or ; ends a token unless inside quotes. */ + if ((ch == ' ' || ch == '\t' || ch == ';') && state == NONE) + break; + + /* + * \ ~ and $ are expanded except in single quotes. + */ + if (ch == '\\' && state != SINGLE_QUOTES) { + if (!yylex_token_escape(&buf, &len)) + goto error; + goto skip; + } + if (ch == '~' && last != state && state != SINGLE_QUOTES) { + if (!yylex_token_tilde(&buf, &len)) + goto error; + goto skip; + } + if (ch == '$' && state != SINGLE_QUOTES) { + if (!yylex_token_variable(&buf, &len)) + goto error; + goto skip; + } + + /* + * ' and " starts or end quotes (and is consumed). + */ + if (ch == '\'') { + if (state == NONE) { + state = SINGLE_QUOTES; + goto next; + } + if (state == SINGLE_QUOTES) { + state = NONE; + goto next; + } + } + if (ch == '"') { + if (state == NONE) { + state = DOUBLE_QUOTES; + goto next; + } + if (state == DOUBLE_QUOTES) { + state = NONE; + goto next; + } + } + + /* + * Otherwise add the character to the buffer. + */ + yylex_append1(&buf, &len, ch); + + skip: + last = state; + + next: + ch = yylex_getc(); + } + ungetc(ch, ps->f); + + buf[len] = '\0'; + log_debug("%s: %s", __func__, buf); + return (buf); + +error: + free(buf); + return (NULL); +} diff --git a/cmd-queue.c b/cmd-queue.c index 68bedae8..c343a212 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -175,15 +175,6 @@ cmdq_remove(struct cmdq_item *item) free(item); } -/* Set command group. */ -static u_int -cmdq_next_group(void) -{ - static u_int group; - - return (++group); -} - /* Remove all subsequent items that match this item's group. */ static void cmdq_remove_group(struct cmdq_item *item) @@ -206,7 +197,6 @@ cmdq_get_command(struct cmd_list *cmdlist, struct cmd_find_state *current, { struct cmdq_item *item, *first = NULL, *last = NULL; struct cmd *cmd; - u_int group = cmdq_next_group(); struct cmdq_shared *shared; shared = xcalloc(1, sizeof *shared); @@ -222,13 +212,15 @@ cmdq_get_command(struct cmd_list *cmdlist, struct cmd_find_state *current, xasprintf(&item->name, "[%s/%p]", cmd->entry->name, item); item->type = CMDQ_COMMAND; - item->group = group; + item->group = cmd->group; item->flags = flags; item->shared = shared; item->cmdlist = cmdlist; item->cmd = cmd; + log_debug("%s: %s group %u", __func__, item->name, item->group); + shared->references++; cmdlist->references++; diff --git a/cmd-source-file.c b/cmd-source-file.c index fbe01c21..bfb530b7 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -38,8 +38,8 @@ const struct cmd_entry cmd_source_file_entry = { .name = "source-file", .alias = "source", - .args = { "q", 1, 1 }, - .usage = "[-q] path", + .args = { "nq", 1, 1 }, + .usage = "[-nq] path", .flags = 0, .exec = cmd_source_file_exec @@ -59,7 +59,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) u_int i; if (args_has(args, 'q')) - flags |= CFG_QUIET; + flags |= CMD_PARSE_QUIET; + if (args_has(args, 'n')) + flags |= CMD_PARSE_PARSEONLY; if (*path == '/') pattern = xstrdup(path); @@ -72,7 +74,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) retval = CMD_RETURN_NORMAL; if (glob(pattern, 0, NULL, &g) != 0) { - if (errno != ENOENT || (~flags & CFG_QUIET)) { + if (errno != ENOENT || (~flags & CMD_PARSE_QUIET)) { cmdq_error(item, "%s: %s", path, strerror(errno)); retval = CMD_RETURN_ERROR; } diff --git a/cmd-string.c b/cmd-string.c deleted file mode 100644 index 058f997c..00000000 --- a/cmd-string.c +++ /dev/null @@ -1,393 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2008 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 MIND, 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 -#include -#include - -#include "tmux.h" - -/* - * Parse a command from a string. - */ - -static int cmd_string_getc(const char *, size_t *); -static void cmd_string_ungetc(size_t *); -static void cmd_string_copy(char **, char *, size_t *); -static char *cmd_string_string(const char *, size_t *, char, int); -static char *cmd_string_variable(const char *, size_t *); -static char *cmd_string_expand_tilde(const char *, size_t *); - -static int -cmd_string_getc(const char *s, size_t *p) -{ - const u_char *ucs = s; - - if (ucs[*p] == '\0') - return (EOF); - return (ucs[(*p)++]); -} - -static void -cmd_string_ungetc(size_t *p) -{ - (*p)--; -} - -static int -cmd_string_unicode(wchar_t *wc, const char *s, size_t *p, char ch) -{ - int size = (ch == 'u') ? 4 : 8; - u_int tmp; - - if (size == 4 && sscanf(s + *p, "%4x", &tmp) != 1) - return (-1); - if (size == 8 && sscanf(s + *p, "%8x", &tmp) != 1) - return (-1); - *p += size; - - *wc = (wchar_t)tmp; - return (0); -} - -int -cmd_string_split(const char *s, int *rargc, char ***rargv) -{ - size_t p = 0; - int ch, argc = 0, append = 0; - char **argv = NULL, *buf = NULL, *t; - const char *whitespace, *equals; - size_t len = 0; - - for (;;) { - ch = cmd_string_getc(s, &p); - switch (ch) { - case '\'': - if ((t = cmd_string_string(s, &p, '\'', 0)) == NULL) - goto error; - cmd_string_copy(&buf, t, &len); - break; - case '"': - if ((t = cmd_string_string(s, &p, '"', 1)) == NULL) - goto error; - cmd_string_copy(&buf, t, &len); - break; - case '$': - if ((t = cmd_string_variable(s, &p)) == NULL) - goto error; - cmd_string_copy(&buf, t, &len); - break; - case '#': - /* Comment: discard rest of line. */ - while ((ch = cmd_string_getc(s, &p)) != EOF) - ; - /* FALLTHROUGH */ - case EOF: - case ' ': - case '\t': - if (buf != NULL) { - buf = xrealloc(buf, len + 1); - buf[len] = '\0'; - - argv = xreallocarray(argv, argc + 1, - sizeof *argv); - argv[argc++] = buf; - - buf = NULL; - len = 0; - } - - if (ch != EOF) - break; - - while (argc != 0) { - equals = strchr(argv[0], '='); - whitespace = argv[0] + strcspn(argv[0], " \t"); - if (equals == NULL || equals > whitespace) - break; - environ_put(global_environ, argv[0]); - argc--; - memmove(argv, argv + 1, argc * (sizeof *argv)); - } - goto done; - case '~': - if (buf != NULL) { - append = 1; - break; - } - t = cmd_string_expand_tilde(s, &p); - if (t == NULL) - goto error; - cmd_string_copy(&buf, t, &len); - break; - default: - append = 1; - break; - } - if (append) { - if (len >= SIZE_MAX - 2) - goto error; - buf = xrealloc(buf, len + 1); - buf[len++] = ch; - } - append = 0; - } - -done: - *rargc = argc; - *rargv = argv; - - free(buf); - return (0); - -error: - if (argv != NULL) - cmd_free_argv(argc, argv); - free(buf); - return (-1); -} - -struct cmd_list * -cmd_string_parse(const char *s, const char *file, u_int line, char **cause) -{ - struct cmd_list *cmdlist = NULL; - int argc; - char **argv; - - if (cause != NULL) - *cause = NULL; - log_debug ("%s: %s", __func__, s); - - if (cmd_string_split(s, &argc, &argv) != 0) { - xasprintf(cause, "invalid or unknown command: %s", s); - return (NULL); - } - if (argc != 0) { - cmdlist = cmd_list_parse(argc, argv, file, line, cause); - if (cmdlist == NULL) { - cmd_free_argv(argc, argv); - return (NULL); - } - } - cmd_free_argv(argc, argv); - return (cmdlist); -} - -static void -cmd_string_copy(char **dst, char *src, size_t *len) -{ - size_t srclen; - - srclen = strlen(src); - - *dst = xrealloc(*dst, *len + srclen + 1); - strlcpy(*dst + *len, src, srclen + 1); - - *len += srclen; - free(src); -} - -static char * -cmd_string_string(const char *s, size_t *p, char endch, int esc) -{ - int ch; - wchar_t wc; - struct utf8_data ud; - char *buf = NULL, *t; - size_t len = 0; - - while ((ch = cmd_string_getc(s, p)) != endch) { - switch (ch) { - case EOF: - goto error; - case '\\': - if (!esc) - break; - switch (ch = cmd_string_getc(s, p)) { - case EOF: - goto error; - case 'e': - ch = '\033'; - break; - case 'r': - ch = '\r'; - break; - case 'n': - ch = '\n'; - break; - case 't': - ch = '\t'; - break; - case 'u': - case 'U': - if (cmd_string_unicode(&wc, s, p, ch) != 0) - goto error; - if (utf8_split(wc, &ud) != UTF8_DONE) - goto error; - if (len >= SIZE_MAX - ud.size - 1) - goto error; - buf = xrealloc(buf, len + ud.size); - memcpy(buf + len, ud.data, ud.size); - len += ud.size; - continue; - } - break; - case '$': - if (!esc) - break; - if ((t = cmd_string_variable(s, p)) == NULL) - goto error; - cmd_string_copy(&buf, t, &len); - continue; - } - - if (len >= SIZE_MAX - 2) - goto error; - buf = xrealloc(buf, len + 1); - buf[len++] = ch; - } - - buf = xrealloc(buf, len + 1); - buf[len] = '\0'; - return (buf); - -error: - free(buf); - return (NULL); -} - -static char * -cmd_string_variable(const char *s, size_t *p) -{ - int ch, fch; - char *buf, *t; - size_t len; - struct environ_entry *envent; - -#define cmd_string_first(ch) ((ch) == '_' || \ - ((ch) >= 'a' && (ch) <= 'z') || ((ch) >= 'A' && (ch) <= 'Z')) -#define cmd_string_other(ch) ((ch) == '_' || \ - ((ch) >= 'a' && (ch) <= 'z') || ((ch) >= 'A' && (ch) <= 'Z') || \ - ((ch) >= '0' && (ch) <= '9')) - - buf = NULL; - len = 0; - - fch = EOF; - switch (ch = cmd_string_getc(s, p)) { - case EOF: - goto error; - case '{': - fch = '{'; - - ch = cmd_string_getc(s, p); - if (!cmd_string_first(ch)) - goto error; - /* FALLTHROUGH */ - default: - if (!cmd_string_first(ch)) { - xasprintf(&t, "$%c", ch); - return (t); - } - - buf = xrealloc(buf, len + 1); - buf[len++] = ch; - - for (;;) { - ch = cmd_string_getc(s, p); - if (ch == EOF || !cmd_string_other(ch)) - break; - else { - if (len >= SIZE_MAX - 3) - goto error; - buf = xrealloc(buf, len + 1); - buf[len++] = ch; - } - } - } - - if (fch == '{' && ch != '}') - goto error; - if (ch != EOF && fch != '{') - cmd_string_ungetc(p); /* ch */ - - buf = xrealloc(buf, len + 1); - buf[len] = '\0'; - - envent = environ_find(global_environ, buf); - free(buf); - if (envent == NULL) - return (xstrdup("")); - return (xstrdup(envent->value)); - -error: - free(buf); - return (NULL); -} - -static char * -cmd_string_expand_tilde(const char *s, size_t *p) -{ - struct passwd *pw; - struct environ_entry *envent; - char *home, *path, *user, *cp; - int last; - - home = NULL; - - last = cmd_string_getc(s, p); - if (last == EOF || last == '/' || last == ' '|| last == '\t') { - envent = environ_find(global_environ, "HOME"); - if (envent != NULL && *envent->value != '\0') - home = envent->value; - else if ((pw = getpwuid(getuid())) != NULL) - home = pw->pw_dir; - } else { - cmd_string_ungetc(p); - - cp = user = xmalloc(strlen(s)); - for (;;) { - last = cmd_string_getc(s, p); - if (last == EOF || - last == '/' || - last == ' '|| - last == '\t') - break; - *cp++ = last; - } - *cp = '\0'; - - if ((pw = getpwnam(user)) != NULL) - home = pw->pw_dir; - free(user); - } - - if (home == NULL) - return (NULL); - - if (last != EOF) - xasprintf(&path, "%s%c", home, last); - else - xasprintf(&path, "%s", home); - return (path); -} diff --git a/cmd.c b/cmd.c index 59f2d4e9..8736a6c7 100644 --- a/cmd.c +++ b/cmd.c @@ -214,6 +214,29 @@ cmd_log_argv(int argc, char **argv, const char *prefix) log_debug("%s: argv[%d]=%s", prefix, i, argv[i]); } +void +cmd_prepend_argv(int *argc, char ***argv, char *arg) +{ + char **new_argv; + int i; + + new_argv = xreallocarray(NULL, (*argc) + 1, sizeof *new_argv); + new_argv[0] = xstrdup(arg); + for (i = 0; i < *argc; i++) + new_argv[1 + i] = (*argv)[i]; + + free(*argv); + *argv = new_argv; + (*argc)++; +} + +void +cmd_append_argv(int *argc, char ***argv, char *arg) +{ + *argv = xreallocarray(*argv, (*argc) + 1, sizeof **argv); + (*argv)[(*argc)++] = xstrdup(arg); +} + int cmd_pack_argv(int argc, char **argv, char *buf, size_t len) { @@ -318,105 +341,102 @@ cmd_stringify_argv(int argc, char **argv) return (buf); } -static int -cmd_try_alias(int *argc, char ***argv) +char * +cmd_get_alias(const char *name) { - struct options_entry *o; - struct options_array_item *a; - union options_value *ov; - int old_argc = *argc, new_argc, i; - char **old_argv = *argv, **new_argv; - size_t wanted; - const char *cp = NULL; + struct options_entry *o; + struct options_array_item *a; + union options_value *ov; + size_t wanted, n; + const char *equals; o = options_get_only(global_options, "command-alias"); if (o == NULL) - return (-1); - wanted = strlen(old_argv[0]); + return (NULL); + wanted = strlen(name); a = options_array_first(o); while (a != NULL) { ov = options_array_item_value(a); - cp = strchr(ov->string, '='); - if (cp != NULL && - (size_t)(cp - ov->string) == wanted && - strncmp(old_argv[0], ov->string, wanted) == 0) - break; + + equals = strchr(ov->string, '='); + if (equals != NULL) { + n = equals - ov->string; + if (n == wanted && strncmp(name, ov->string, n) == 0) + return (xstrdup(equals + 1)); + } + a = options_array_next(a); } - if (a == NULL) - return (-1); + return (NULL); +} - if (cmd_string_split(cp + 1, &new_argc, &new_argv) != 0) - return (-1); +static const struct cmd_entry * +cmd_find(const char *name, char **cause) +{ + const struct cmd_entry **loop, *entry, *found = NULL; + int ambiguous; + char s[BUFSIZ]; - *argc = new_argc + old_argc - 1; - *argv = xcalloc((*argc) + 1, sizeof **argv); + ambiguous = 0; + for (loop = cmd_table; *loop != NULL; loop++) { + entry = *loop; + if (entry->alias != NULL && strcmp(entry->alias, name) == 0) { + ambiguous = 0; + found = entry; + break; + } - for (i = 0; i < new_argc; i++) - (*argv)[i] = xstrdup(new_argv[i]); - for (i = 1; i < old_argc; i++) - (*argv)[new_argc + i - 1] = xstrdup(old_argv[i]); + if (strncmp(entry->name, name, strlen(name)) != 0) + continue; + if (found != NULL) + ambiguous = 1; + found = entry; - log_debug("alias: %s=%s", old_argv[0], cp + 1); - for (i = 0; i < *argc; i++) - log_debug("alias: argv[%d] = %s", i, (*argv)[i]); + if (strcmp(entry->name, name) == 0) + break; + } + if (ambiguous) + goto ambiguous; + if (found == NULL) { + xasprintf(cause, "unknown command: %s", name); + return (NULL); + } + return (found); - cmd_free_argv(new_argc, new_argv); - return (0); +ambiguous: + *s = '\0'; + for (loop = cmd_table; *loop != NULL; loop++) { + entry = *loop; + if (strncmp(entry->name, name, strlen(name)) != 0) + continue; + if (strlcat(s, entry->name, sizeof s) >= sizeof s) + break; + if (strlcat(s, ", ", sizeof s) >= sizeof s) + break; + } + s[strlen(s) - 2] = '\0'; + xasprintf(cause, "ambiguous command: %s, could be: %s", name, s); + return (NULL); } struct cmd * cmd_parse(int argc, char **argv, const char *file, u_int line, char **cause) { + const struct cmd_entry *entry; const char *name; - const struct cmd_entry **entryp, *entry; struct cmd *cmd; struct args *args; - char s[BUFSIZ]; - int ambiguous, allocated = 0; - *cause = NULL; if (argc == 0) { xasprintf(cause, "no command"); return (NULL); } name = argv[0]; -retry: - ambiguous = 0; - entry = NULL; - for (entryp = cmd_table; *entryp != NULL; entryp++) { - if ((*entryp)->alias != NULL && - strcmp((*entryp)->alias, argv[0]) == 0) { - ambiguous = 0; - entry = *entryp; - break; - } - - if (strncmp((*entryp)->name, argv[0], strlen(argv[0])) != 0) - continue; - if (entry != NULL) - ambiguous = 1; - entry = *entryp; - - /* Bail now if an exact match. */ - if (strcmp(entry->name, argv[0]) == 0) - break; - } - if ((ambiguous || entry == NULL) && - server_proc != NULL && - !allocated && - cmd_try_alias(&argc, &argv) == 0) { - allocated = 1; - goto retry; - } - if (ambiguous) - goto ambiguous; - if (entry == NULL) { - xasprintf(cause, "unknown command: %s", name); + entry = cmd_find(name, cause); + if (entry == NULL) return (NULL); - } cmd_log_argv(argc, argv, entry->name); args = args_parse(entry->args.template, argc, argv); @@ -435,23 +455,11 @@ retry: cmd->file = xstrdup(file); cmd->line = line; - if (allocated) - cmd_free_argv(argc, argv); - return (cmd); + cmd->alias = NULL; + cmd->argc = argc; + cmd->argv = cmd_copy_argv(argc, argv); -ambiguous: - *s = '\0'; - for (entryp = cmd_table; *entryp != NULL; entryp++) { - if (strncmp((*entryp)->name, argv[0], strlen(argv[0])) != 0) - continue; - if (strlcat(s, (*entryp)->name, sizeof s) >= sizeof s) - break; - if (strlcat(s, ", ", sizeof s) >= sizeof s) - break; - } - s[strlen(s) - 2] = '\0'; - xasprintf(cause, "ambiguous command: %s, could be: %s", name, s); - return (NULL); + return (cmd); usage: if (args != NULL) @@ -460,6 +468,18 @@ usage: return (NULL); } +void +cmd_free(struct cmd *cmd) +{ + free(cmd->alias); + cmd_free_argv(cmd->argc, cmd->argv); + + free(cmd->file); + + args_free(cmd->args); + free(cmd); +} + char * cmd_print(struct cmd *cmd) { diff --git a/control.c b/control.c index 41c50df7..b7ac3f62 100644 --- a/control.c +++ b/control.c @@ -68,9 +68,9 @@ control_error(struct cmdq_item *item, void *data) void control_callback(struct client *c, int closed, __unused void *data) { - char *line, *cause; - struct cmd_list *cmdlist; + char *line; struct cmdq_item *item; + struct cmd_parse_result *pr; if (closed) c->flags |= CLIENT_EXIT; @@ -84,15 +84,21 @@ control_callback(struct client *c, int closed, __unused void *data) break; } - cmdlist = cmd_string_parse(line, NULL, 0, &cause); - if (cmdlist == NULL) { - item = cmdq_get_callback(control_error, cause); + pr = cmd_parse_from_string(line, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + break; + case CMD_PARSE_ERROR: + item = cmdq_get_callback(control_error, pr->error); cmdq_append(c, item); - } else { - item = cmdq_get_command(cmdlist, NULL, NULL, 0); + free(pr->error); + break; + case CMD_PARSE_SUCCESS: + item = cmdq_get_command(pr->cmdlist, NULL, NULL, 0); item->shared->flags |= CMDQ_SHARED_CONTROL; cmdq_append(c, item); - cmd_list_free(cmdlist); + cmd_list_free(pr->cmdlist); + break; } free(line); diff --git a/key-bindings.c b/key-bindings.c index c4874b50..2bc659aa 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -433,16 +433,15 @@ key_bindings_init(void) "bind -Tcopy-mode-vi C-Up send -X scroll-up", "bind -Tcopy-mode-vi C-Down send -X scroll-down", }; - u_int i; - struct cmd_list *cmdlist; - char *cause; + u_int i; + struct cmd_parse_result *pr; for (i = 0; i < nitems(defaults); i++) { - cmdlist = cmd_string_parse(defaults[i], "", i, &cause); - if (cmdlist == NULL) + pr = cmd_parse_from_string(defaults[i], NULL); + if (pr->status != CMD_PARSE_SUCCESS) fatalx("bad default key: %s", defaults[i]); - cmdq_append(NULL, cmdq_get_command(cmdlist, NULL, NULL, 0)); - cmd_list_free(cmdlist); + cmdq_append(NULL, cmdq_get_command(pr->cmdlist, NULL, NULL, 0)); + cmd_list_free(pr->cmdlist); } } diff --git a/menu.c b/menu.c index 395e1455..9de6c5fc 100644 --- a/menu.c +++ b/menu.c @@ -200,9 +200,8 @@ menu_key_cb(struct client *c, struct key_event *event) u_int i; int count = menu->count, old = md->choice; const struct menu_item *item; - struct cmd_list *cmdlist; struct cmdq_item *new_item; - char *cause; + struct cmd_parse_result *pr; if (KEYC_IS_MOUSE(event->key)) { if (md->flags & MENU_NOMOUSE) @@ -272,22 +271,22 @@ chosen: md->cb = NULL; return (1); } - cmdlist = cmd_string_parse(item->command, NULL, 0, &cause); - if (cmdlist == NULL) { - if (cause != NULL) - new_item = cmdq_get_error(cause); - else - new_item = NULL; - free(cause); - } else { - new_item = cmdq_get_command(cmdlist, NULL, NULL, 0); - cmd_list_free(cmdlist); - } - if (new_item != NULL) { - if (md->item != NULL) - cmdq_insert_after(md->item, new_item); - else - cmdq_append(c, new_item); + + pr = cmd_parse_from_string(item->command, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + new_item = NULL; + break; + case CMD_PARSE_ERROR: + new_item = cmdq_get_error(pr->error); + free(pr->error); + cmdq_append(c, new_item); + break; + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, NULL, NULL, 0); + cmd_list_free(pr->cmdlist); + cmdq_append(c, new_item); + break; } return (1); } diff --git a/mode-tree.c b/mode-tree.c index fb186c1a..9dd96190 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1045,8 +1045,8 @@ mode_tree_run_command(struct client *c, struct cmd_find_state *fs, const char *template, const char *name) { struct cmdq_item *new_item; - struct cmd_list *cmdlist; - char *command, *cause; + char *command; + struct cmd_parse_result *pr; command = cmd_template_replace(template, name, 1); if (command == NULL || *command == '\0') { @@ -1054,17 +1054,22 @@ mode_tree_run_command(struct client *c, struct cmd_find_state *fs, return; } - cmdlist = cmd_string_parse(command, NULL, 0, &cause); - if (cmdlist == NULL) { - if (cause != NULL && c != NULL) { - *cause = toupper((u_char)*cause); - status_message_set(c, "%s", cause); + pr = cmd_parse_from_string(command, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + break; + case CMD_PARSE_ERROR: + if (c != NULL) { + *pr->error = toupper((u_char)*pr->error); + status_message_set(c, "%s", pr->error); } - free(cause); - } else { - new_item = cmdq_get_command(cmdlist, fs, NULL, 0); + free(pr->error); + break; + case CMD_PARSE_SUCCESS: + new_item = cmdq_get_command(pr->cmdlist, fs, NULL, 0); cmdq_append(c, new_item); - cmd_list_free(cmdlist); + cmd_list_free(pr->cmdlist); + break; } free(command); diff --git a/options.c b/options.c index 488f6cf9..fa8752b0 100644 --- a/options.c +++ b/options.c @@ -353,8 +353,7 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, { struct options_array_item *a; char *new; - struct cmd_list *cmdlist; - char *error; + struct cmd_parse_result *pr; if (!OPTIONS_IS_ARRAY(o)) { if (cause != NULL) @@ -363,13 +362,19 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, } if (OPTIONS_IS_COMMAND(o)) { - cmdlist = cmd_string_parse(value, NULL, 0, &error); - if (cmdlist == NULL && error != NULL) { - if (cause != NULL) - *cause = error; - else - free(error); + pr = cmd_parse_from_string(value, NULL); + switch (pr->status) { + case CMD_PARSE_EMPTY: + *cause = xstrdup("empty command"); return (-1); + case CMD_PARSE_ERROR: + if (cause != NULL) + *cause = pr->error; + else + free(pr->error); + return (-1); + case CMD_PARSE_SUCCESS: + break; } } @@ -397,7 +402,7 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, if (OPTIONS_IS_STRING(o)) a->value.string = new; else if (OPTIONS_IS_COMMAND(o)) - a->value.cmdlist = cmdlist; + a->value.cmdlist = pr->cmdlist; return (0); } diff --git a/tmux.1 b/tmux.1 index e5238b35..4c86ac95 100644 --- a/tmux.1 +++ b/tmux.1 @@ -355,8 +355,217 @@ Key bindings may be changed with the and .Ic unbind-key commands. +.Sh COMMAND PARSING AND EXECUTION +.Nm +supports a large number of commands which can be used to control its +behaviour. +Each command is named and can accept zero or more flags and arguments. +They may be bound to a key with the +.Ic bind-key +command or run from the shell prompt, a shell script, a configuration file or +the command prompt. +For example, the same +.Ic set-option +command run from the shell prompt, from +.Pa ~/.tmux.conf +and bound to a key may look like: +.Bd -literal -offset indent +$ tmux set-option -g status-style bg=cyan + +set-option -g status-style bg=cyan + +bind-key C set-option -g status-style bg=cyan +.Ed +.Pp +Here, the command name is +.Ql set-option , +.Ql Fl g +is a flag and +.Ql status-style +and +.Ql bg=cyan +are arguments. +.Pp +.Nm +distinguishes between command parsing and execution. +In order to execute a command, +.Nm +needs it to be split up into its name and arguments. +This is command parsing. +If a command is run from the shell, the shell parses it; from inside +.Nm +or from a configuration file, +.Nm +does. +Examples of when +.Nm +parses commands are: +.Bl -dash -offset indent +.It +in a configuration file; +.It +typed at the command prompt (see +.Ic command-prompt ) ; +.It +given to +.Ic bind-key ; +.It +passed as arguments to +.Ic if-shell +or +.Ic confirm-before . +.El +.Pp +To execute commands, each client has a +.Ql command queue . +A global command queue not attached to any client is used on startup +for configuration files like +.Pa ~/.tmux.conf . +Parsed commands added to the queue are executed in order. +Some commands, like +.Ic if-shell +and +.Ic confirm-before , +parse their argument to create a new command which is inserted immediately +after themselves. +This means that arguments can be parsed twice or more - once when the parent command (such as +.Ic if-shell ) +is parsed and again when it parses and executes its command. +Commands like +.Ic if-shell , +.Ic run-shell +and +.Ic display-panes +stop execution of subsequent commands on the queue until something happens - +.Ic if-shell +and +.Ic run-shell +until a shell command finishes and +.Ic display-panes +until a key is pressed. +For example, the following commands: +.Bd -literal -offset indent +new-session; new-window +if-shell "true" "split-window" +kill-session +.Ed +.Pp +Will execute +.Ic new-session , +.Ic new-window , +.Ic if-shell , +the shell command +.Xr true 1 , +.Ic new-window +and +.Ic kill-session +in that order. +.Pp +The +.Sx COMMANDS +section lists the +.Nm +commands and their arguments. +.Sh PARSING SYNTAX +This section describes the syntax of commands parsed by +.Nm , +for example in a configuration file or at the command prompt. +Note the when commands are entered into the shell, they are parsed by the shell +- see for example +.Xr ksh 1 +or +.Xr csh 1 . +.Pp +Each command is terminated by a newline or a semicolon (;). +Commands separated by semicolons together form a +.Ql command sequence +- if a command in the sequence encounters an error, no subsequent commands are +executed. +.Pp +Comments are marked by the unquoted # character - any remaining text after a +comment is ignored until the end of the line. +.Pp +If the last character of a line is \e, the line is joined with the following +line (the \e and the newline are completely removed). +This is called line continuation and applies both inside and outside quoted +strings and in comments. +.Pp +Command arguments may be specified as strings surrounded by either single (') +or double quotes ("). +.\" " +This is required when the argument contains any special character. +Strings cannot span multiple lines except with line continuation. +.Pp +Outside of quotes and inside double quotes, these replacements are performed: +.Bl -dash -offset indent +.It +Environment variables preceded by $ are replaced with their value from the +global environment (see the +.Sx GLOBAL AND SESSION ENVIRONMENT +section). +.It +A leading ~ or ~user is expanded to the home directory of the current or +specified user. +.It +\euXXXX or \euXXXXXXXX is replaced by the Unicode codepoint corresponding to +the given four or eight digit hexadecimal number. +.It +When preceded (escaped) by a \e, the following characters are replaced: \ee by +the escape character; \er by a carriage return; \en by a newline; and \et by a +tab. +.Pp +Any other characters preceded by \e are replaced by themselves (that is, the \e +is removed) and are not treated as having any special meaning - so for example +\e; will not mark a command sequence and \e$ will not expand an environment +variable. +.El +.Pp +Environment variables may be set by using the syntax +.Ql name=value , +for example +.Ql HOME=/home/user . +Variables set during parsing are added to the global environment. +.Pp +Commands may be parsed conditionally by surrounding them with +.Ql %if , +.Ql %elif , +.Ql %else +and +.Ql %endif . +The argument to +.Ql %if +and +.Ql %elif +is expanded as a format (see +.Sx FORMATS ) +and if it evaluates to false (zero or empty), subsequent text is ignored until +the closing +.Ql %elif , +.Ql %else +or +.Ql %endif . +For example: +.Bd -literal -offset indent +%if #{==:#{host},myhost} +set -g status-style bg=red +%elif #{==:#{host},myotherhost} +set -g status-style bg=green +%else +set -g status-style bg=blue +%endif +.Ed +.Pp +Will change the status line to red if running on +.Ql myhost , +green if running on +.Ql myotherhost , +or blue if running on another host. +Conditionals may be given on one line, for example: +.Bd -literal -offset indent +%if #{==:#{host},myhost} set -g status-style bg=red %endif +.Ed .Sh COMMANDS -This section contains a list of the commands supported by +This section describes the commands supported by .Nm . Most commands accept the optional .Fl t @@ -622,16 +831,6 @@ Or if using $ tmux bind-key F1 set-option status off .Ed .Pp -Multiple commands may be specified together as part of a -.Em command sequence . -Each command should be separated by spaces and a semicolon; -commands are executed sequentially from left to right and -lines ending with a backslash continue on to the next line, -except when escaped by another backslash. -A literal semicolon may be included by escaping it with a backslash (for -example, when specifying a command sequence to -.Ic bind-key ) . -.Pp Example .Nm commands include: @@ -1005,7 +1204,7 @@ and .Fl T show debugging information about jobs and terminals. .It Xo Ic source-file -.Op Fl q +.Op Fl nq .Ar path .Xc .D1 (alias: Ic source ) @@ -1019,44 +1218,9 @@ If is given, no error will be returned if .Ar path does not exist. -.Pp -Within a configuration file, commands may be made conditional by surrounding -them with -.Em %if -and -.Em %endif -lines. -Additional -.Em %elif -and -.Em %else -lines may also be used. -The argument to -.Em %if -and -.Em %elif -is expanded as a format and if it evaluates to false (zero or empty), -subsequent lines are ignored until the next -.Em %elif , -.Em %else -or -.Em %endif . -For example: -.Bd -literal -offset indent -%if #{==:#{host},myhost} -set -g status-style bg=red -%elif #{==:#{host},myotherhost} -set -g status-style bg=green -%else -set -g status-style bg=blue -%endif -.Ed -.Pp -Will change the status line to red if running on -.Ql myhost , -green if running on -.Ql myotherhost , -or blue if running on another host. +With +.Fl n , +the file is parsed but no commands are executed. .It Ic start-server .D1 (alias: Ic start ) Start the @@ -4134,7 +4298,7 @@ right of the list if there is not enough space. .Ic norange .Xc Mark a range in the -. Ic status-format +.Ic status-format option. .Ic range=left and @@ -4450,7 +4614,7 @@ This command works only from inside .Op Fl x Ar position .Op Fl y Ar position .Xc -.D1 (alias: Ic menu) +.D1 (alias: Ic menu ) Display a menu on .Ar target-client . .Ar target-pane diff --git a/tmux.h b/tmux.h index 2979a6e3..f996b1f4 100644 --- a/tmux.h +++ b/tmux.h @@ -1279,18 +1279,25 @@ struct cmd_find_state { /* Command and list of commands. */ struct cmd { - const struct cmd_entry *entry; - struct args *args; + const struct cmd_entry *entry; + struct args *args; + u_int group; - char *file; - u_int line; + char *file; + u_int line; - TAILQ_ENTRY(cmd) qentry; + char *alias; + int argc; + char **argv; + + TAILQ_ENTRY(cmd) qentry; }; +TAILQ_HEAD(cmds, cmd); struct cmd_list { - int references; - TAILQ_HEAD(, cmd) list; + int references; + u_int group; + struct cmds list; }; /* Command return values. */ @@ -1301,6 +1308,31 @@ enum cmd_retval { CMD_RETURN_STOP }; +/* Command parse result. */ +enum cmd_parse_status { + CMD_PARSE_EMPTY, + CMD_PARSE_ERROR, + CMD_PARSE_SUCCESS +}; +struct cmd_parse_result { + enum cmd_parse_status status; + struct cmd_list *cmdlist; + char *error; +}; +struct cmd_parse_input { + int flags; +#define CMD_PARSE_QUIET 0x1 +#define CMD_PARSE_PARSEONLY 0x2 +#define CMD_PARSE_NOALIAS 0x4 + + const char *file; + u_int line; + + struct cmdq_item *item; + struct client *c; + struct cmd_find_state fs; +}; + /* Command queue item type. */ enum cmdq_type { CMDQ_COMMAND, @@ -1671,7 +1703,6 @@ void proc_toggle_log(struct tmuxproc *); /* cfg.c */ extern int cfg_finished; extern struct client *cfg_client; -#define CFG_QUIET 0x1 void start_cfg(void); int load_cfg(const char *, struct client *, struct cmdq_item *, int, struct cmdq_item **); @@ -1956,12 +1987,16 @@ int cmd_find_from_nothing(struct cmd_find_state *, int); /* cmd.c */ void cmd_log_argv(int, char **, const char *); +void cmd_prepend_argv(int *, char ***, char *); +void cmd_append_argv(int *, char ***, char *); int cmd_pack_argv(int, char **, char *, size_t); int cmd_unpack_argv(char *, size_t, int, char ***); char **cmd_copy_argv(int, char **); void cmd_free_argv(int, char **); char *cmd_stringify_argv(int, char **); +char *cmd_get_alias(const char *); struct cmd *cmd_parse(int, char **, const char *, u_int, char **); +void cmd_free(struct cmd *); char *cmd_print(struct cmd *); int cmd_mouse_at(struct window_pane *, struct mouse_event *, u_int *, u_int *, int); @@ -1975,7 +2010,16 @@ extern const struct cmd_entry *cmd_table[]; enum cmd_retval cmd_attach_session(struct cmdq_item *, const char *, int, int, const char *, int); +/* cmd-parse.c */ +void cmd_parse_empty(struct cmd_parse_input *); +struct cmd_parse_result *cmd_parse_from_file(FILE *, struct cmd_parse_input *); +struct cmd_parse_result *cmd_parse_from_string(const char *, + struct cmd_parse_input *); + /* cmd-list.c */ +struct cmd_list *cmd_list_new(void); +void cmd_list_append(struct cmd_list *, struct cmd *); +void cmd_list_move(struct cmd_list *, struct cmd_list *); struct cmd_list *cmd_list_parse(int, char **, const char *, u_int, char **); void cmd_list_free(struct cmd_list *); char *cmd_list_print(struct cmd_list *); @@ -1997,10 +2041,6 @@ void cmdq_guard(struct cmdq_item *, const char *, int); void printflike(2, 3) cmdq_print(struct cmdq_item *, const char *, ...); void printflike(2, 3) cmdq_error(struct cmdq_item *, const char *, ...); -/* cmd-string.c */ -int cmd_string_split(const char *, int *, char ***); -struct cmd_list *cmd_string_parse(const char *, const char *, u_int, char **); - /* cmd-wait-for.c */ void cmd_wait_for_flush(void);