From efe2453eac45129b972b01d412b31fc029163611 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 30 Jun 2026 07:27:06 +0100 Subject: [PATCH] New tmux parser and test program. --- cmd-parse.y | 2534 ++++++++++++++++++++++-------------------- parser-test/main.c | 59 + parser-test/tmux.h | 62 ++ parser-test/xstubs.c | 118 ++ tmux-parser.h | 82 ++ 5 files changed, 1625 insertions(+), 1230 deletions(-) create mode 100644 parser-test/main.c create mode 100644 parser-test/tmux.h create mode 100644 parser-test/xstubs.c create mode 100644 tmux-parser.h diff --git a/cmd-parse.y b/cmd-parse.y index 0b32f5d34..6d050f189 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -22,51 +22,43 @@ #include #include -#include +#include #include #include #include #include #include "tmux.h" +#include "tmux-parser.h" static int yylex(void); static int yyparse(void); static void printflike(1,2) yyerror(const char *, ...); -static char *yylex_token(int); -static char *yylex_format(void); - #define CMD_PARSE_MAX_ENVIRON_LEN 16384 -struct cmd_parse_scope { - int flag; - TAILQ_ENTRY (cmd_parse_scope) entry; -}; +/* + * A node in the parse tree. Subtypes are distinguished by the type, never by + * flags. The value holds the assignment name, the literal text, the + * environment variable name, or the tilde user as appropriate; it is NULL + * otherwise. Containers store ordered children. + */ +TAILQ_HEAD(cmd_parse_nodes, cmd_parse_node); -enum cmd_parse_argument_type { - CMD_PARSE_STRING, - CMD_PARSE_COMMANDS, - CMD_PARSE_PARSED_COMMANDS -}; - -struct cmd_parse_argument { - enum cmd_parse_argument_type type; - char *string; - struct cmd_parse_commands *commands; - struct cmd_list *cmdlist; - - TAILQ_ENTRY(cmd_parse_argument) entry; -}; -TAILQ_HEAD(cmd_parse_arguments, cmd_parse_argument); - -struct cmd_parse_command { +struct cmd_parse_node { + enum cmd_parse_node_type type; u_int line; - struct cmd_parse_arguments arguments; + u_int end_line; + char *value; - TAILQ_ENTRY(cmd_parse_command) entry; + struct cmd_parse_nodes children; + TAILQ_ENTRY(cmd_parse_node) entry; +}; + +struct cmd_parse_tree { + int references; + struct cmd_parse_node *root; }; -TAILQ_HEAD(cmd_parse_commands, cmd_parse_command); struct cmd_parse_state { FILE *f; @@ -82,36 +74,40 @@ struct cmd_parse_state { u_int escapes; char *error; - struct cmd_parse_commands *commands; - - struct cmd_parse_scope *scope; - TAILQ_HEAD(, cmd_parse_scope) stack; + struct cmd_parse_nodes *commands; }; static struct cmd_parse_state parse_state; +/* Builder used by the lexer while collecting the parts of a string token. */ +struct cmd_parse_scan { + struct cmd_parse_node *node; + char *text; + size_t len; +}; + static char *cmd_parse_get_error(const char *, u_int, const char *); -static void cmd_parse_free_command(struct cmd_parse_command *); -static struct cmd_parse_commands *cmd_parse_new_commands(void); -static void cmd_parse_free_commands(struct cmd_parse_commands *); -static void cmd_parse_build_commands(struct cmd_parse_commands *, - struct cmd_parse_input *, struct cmd_parse_result *); -static void cmd_parse_print_commands(struct cmd_parse_input *, - struct cmd_list *); +static struct cmd_parse_node *cmd_parse_new_node(enum cmd_parse_node_type, + u_int); +static struct cmd_parse_nodes *cmd_parse_new_nodes(void); +static void cmd_parse_free_node(struct cmd_parse_node *); +static void cmd_parse_append(struct cmd_parse_nodes *, + struct cmd_parse_nodes *); +static struct cmd_parse_nodes *cmd_parse_wrap(struct cmd_parse_node *); +static struct cmd_parse_node *cmd_parse_make_assign(enum cmd_parse_node_type, + struct cmd_parse_node *); +static struct cmd_parse_node *cmd_parse_token_from_string(const char *); +static void cmd_parse_onegroup(struct cmd_parse_node *); +static void cmd_parse_print_sequence(char **, struct cmd_parse_node *, + u_int); +static void cmd_parse_print_item(char **, struct cmd_parse_node *, u_int, + int); %} %union { - char *token; - struct cmd_parse_arguments *arguments; - struct cmd_parse_argument *argument; - int flag; - struct { - int flag; - struct cmd_parse_commands *commands; - } elif; - struct cmd_parse_commands *commands; - struct cmd_parse_command *command; + struct cmd_parse_node *node; + struct cmd_parse_nodes *nodes; } %token ERROR @@ -120,26 +116,24 @@ static void cmd_parse_print_commands(struct cmd_parse_input *, %token ELSE %token ELIF %token ENDIF -%token FORMAT TOKEN EQUALS +%token FORMAT TOKEN EQUALS -%type expanded format -%type arguments -%type argument -%type if_open if_elif -%type elif elif1 -%type argument_statements statements statement -%type commands condition condition1 -%type command +%type expanded format argument command_name +%type assign optional_assign hidden_assign +%type commands condition condition1 +%type if_open if_elif if_else +%type statements statement arguments argument_statements +%type command elif elif1 %% -lines : /* empty */ - | statements - { - struct cmd_parse_state *ps = &parse_state; +lines : /* empty */ + | statements + { + struct cmd_parse_state *ps = &parse_state; - ps->commands = $1; - } + ps->commands = $1; + } statements : statement '\n' { @@ -148,41 +142,28 @@ statements : statement '\n' | statements statement '\n' { $$ = $1; - TAILQ_CONCAT($$, $2, entry); - free($2); + cmd_parse_append($$, $2); } statement : /* empty */ { - $$ = xmalloc (sizeof *$$); - TAILQ_INIT($$); + $$ = cmd_parse_new_nodes(); } - | hidden_assignment + | hidden_assign { - $$ = xmalloc (sizeof *$$); - TAILQ_INIT($$); + $$ = cmd_parse_wrap($1); } | condition { - struct cmd_parse_state *ps = &parse_state; - - if (ps->scope == NULL || ps->scope->flag) - $$ = $1; - else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); - } + $$ = cmd_parse_wrap($1); } | commands { - struct cmd_parse_state *ps = &parse_state; - - if (ps->scope == NULL || ps->scope->flag) - $$ = $1; - else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); - } + $$ = cmd_parse_new_nodes(); + if (TAILQ_EMPTY(&$1->children)) + cmd_parse_free_node($1); + else + TAILQ_INSERT_TAIL($$, $1, entry); } format : FORMAT @@ -196,213 +177,99 @@ format : FORMAT 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 *fsp; - struct cmd_find_state fs; - int flags = FORMAT_NOJOBS; - - if (cmd_find_valid_state(&pi->fs)) - fsp = &pi->fs; - else { - cmd_find_from_client(&fs, c, 0); - fsp = &fs; - } - ft = format_create(NULL, pi->item, FORMAT_NONE, flags); - format_defaults(ft, c, fsp->s, fsp->wl, fsp->wp); - - $$ = format_expand(ft, $1); - format_free(ft); - free($1); + $$ = $1; } -optional_assignment : /* empty */ - | assignment - -assignment : EQUALS +optional_assign : /* empty */ { - struct cmd_parse_state *ps = &parse_state; - int flags = ps->input->flags; - int flag = 1; - struct cmd_parse_scope *scope; - - if (ps->scope != NULL) { - flag = ps->scope->flag; - TAILQ_FOREACH(scope, &ps->stack, entry) - flag = flag && scope->flag; - } - - if (strlen($1) > CMD_PARSE_MAX_ENVIRON_LEN) { - yyerror("environment variable is too long"); - YYABORT; - } - if ((~flags & CMD_PARSE_PARSEONLY) && flag) - environ_put(global_environ, $1, 0); - free($1); + $$ = NULL; } - -hidden_assignment : HIDDEN EQUALS + | assign { - struct cmd_parse_state *ps = &parse_state; - int flags = ps->input->flags; - int flag = 1; - struct cmd_parse_scope *scope; - - if (ps->scope != NULL) { - flag = ps->scope->flag; - TAILQ_FOREACH(scope, &ps->stack, entry) - flag = flag && scope->flag; - } - - if (strlen($2) > CMD_PARSE_MAX_ENVIRON_LEN) { - yyerror("environment variable is too long"); - YYABORT; - } - if ((~flags & CMD_PARSE_PARSEONLY) && flag) - environ_put(global_environ, $2, ENVIRON_HIDDEN); - free($2); + $$ = $1; } -if_open : IF expanded +assign : EQUALS + { + $$ = cmd_parse_make_assign(CMD_PARSE_ASSIGN, $1); + } + +hidden_assign : HIDDEN EQUALS { - 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; + $$ = cmd_parse_make_assign(CMD_PARSE_HIDDEN_ASSIGN, $2); } -if_else : ELSE +if_open : IF expanded + { + struct cmd_parse_state *ps = &parse_state; + + $$ = cmd_parse_new_node(CMD_PARSE_IF, ps->input->line); + TAILQ_INSERT_TAIL(&$$->children, $2, entry); + } + +if_else : ELSE + { + struct cmd_parse_state *ps = &parse_state; + + $$ = cmd_parse_new_node(CMD_PARSE_ELSE, ps->input->line); + } + +if_elif : ELIF expanded + { + struct cmd_parse_state *ps = &parse_state; + + $$ = cmd_parse_new_node(CMD_PARSE_ELIF, ps->input->line); + TAILQ_INSERT_TAIL(&$$->children, $2, entry); + } + +condition : if_open '\n' statements ENDIF { - 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; + cmd_parse_append(&$1->children, $3); + $$ = $1; } - -if_elif : ELIF expanded + | if_open '\n' statements if_else '\n' statements ENDIF { - 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; + cmd_parse_append(&$1->children, $3); + cmd_parse_append(&$4->children, $6); + TAILQ_INSERT_TAIL(&$1->children, $4, entry); + $$ = $1; + } + | if_open '\n' statements elif ENDIF + { + cmd_parse_append(&$1->children, $3); + cmd_parse_append(&$1->children, $4); + $$ = $1; + } + | if_open '\n' statements elif if_else '\n' statements ENDIF + { + cmd_parse_append(&$1->children, $3); + cmd_parse_append(&$1->children, $4); + cmd_parse_append(&$5->children, $7); + TAILQ_INSERT_TAIL(&$1->children, $5, entry); + $$ = $1; } -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 - { - if ($1) - $$ = $3; - else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - } - } - | if_open '\n' statements if_else '\n' statements if_close - { - if ($1) { - $$ = $3; - cmd_parse_free_commands($6); - } else { - $$ = $6; - cmd_parse_free_commands($3); - } - } - | if_open '\n' statements elif if_close - { - if ($1) { - $$ = $3; - cmd_parse_free_commands($4.commands); - } else if ($4.flag) { - $$ = $4.commands; - cmd_parse_free_commands($3); - } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); - } - } - | if_open '\n' statements elif if_else '\n' statements if_close - { - if ($1) { - $$ = $3; - cmd_parse_free_commands($4.commands); - cmd_parse_free_commands($7); - } else if ($4.flag) { - $$ = $4.commands; - cmd_parse_free_commands($3); - cmd_parse_free_commands($7); - } else { - $$ = $7; - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); - } - } - -elif : if_elif '\n' statements - { - if ($1) { - $$.flag = 1; - $$.commands = $3; - } else { - $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - } - } - | if_elif '\n' statements elif - { - if ($1) { - $$.flag = 1; - $$.commands = $3; - cmd_parse_free_commands($4.commands); - } else if ($4.flag) { - $$.flag = 1; - $$.commands = $4.commands; - cmd_parse_free_commands($3); - } else { - $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); - } - } +elif : if_elif '\n' statements + { + cmd_parse_append(&$1->children, $3); + $$ = cmd_parse_new_nodes(); + TAILQ_INSERT_TAIL($$, $1, entry); + } + | if_elif '\n' statements elif + { + cmd_parse_append(&$1->children, $3); + $$ = cmd_parse_new_nodes(); + TAILQ_INSERT_TAIL($$, $1, entry); + cmd_parse_append($$, $4); + } commands : command { struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; - $$ = cmd_parse_new_commands(); - if (!TAILQ_EMPTY(&$1->arguments) && - (ps->scope == NULL || ps->scope->flag)) - TAILQ_INSERT_TAIL($$, $1, entry); - else - cmd_parse_free_command($1); + $$ = cmd_parse_new_node(CMD_PARSE_SEQUENCE, pi->line); + cmd_parse_append(&$$->children, $1); } | commands ';' { @@ -411,152 +278,106 @@ commands : command | commands ';' condition1 { $$ = $1; - TAILQ_CONCAT($$, $3, entry); - free($3); + TAILQ_INSERT_TAIL(&$$->children, $3, entry); } | commands ';' command { - struct cmd_parse_state *ps = &parse_state; - - if (!TAILQ_EMPTY(&$3->arguments) && - (ps->scope == NULL || ps->scope->flag)) { - $$ = $1; - TAILQ_INSERT_TAIL($$, $3, entry); - } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); - cmd_parse_free_command($3); - } + $$ = $1; + cmd_parse_append(&$$->children, $3); } | condition1 + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; + + $$ = cmd_parse_new_node(CMD_PARSE_SEQUENCE, pi->line); + TAILQ_INSERT_TAIL(&$$->children, $1, entry); + } + +command : assign + { + $$ = cmd_parse_new_nodes(); + TAILQ_INSERT_TAIL($$, $1, entry); + } + | optional_assign command_name + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_node *cmd; + struct cmd_parse_input *pi = ps->input; + + cmd = cmd_parse_new_node(CMD_PARSE_COMMAND, pi->line); + TAILQ_INSERT_TAIL(&cmd->children, $2, entry); + + $$ = cmd_parse_new_nodes(); + if ($1 != NULL) + TAILQ_INSERT_TAIL($$, $1, entry); + TAILQ_INSERT_TAIL($$, cmd, entry); + } + | optional_assign command_name arguments + { + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_node *cmd; + struct cmd_parse_input *pi = ps->input; + + cmd = cmd_parse_new_node(CMD_PARSE_COMMAND, pi->line); + TAILQ_INSERT_TAIL(&cmd->children, $2, entry); + cmd_parse_append(&cmd->children, $3); + + $$ = cmd_parse_new_nodes(); + if ($1 != NULL) + TAILQ_INSERT_TAIL($$, $1, entry); + TAILQ_INSERT_TAIL($$, cmd, entry); + } + +command_name : TOKEN { $$ = $1; } -command : assignment +condition1 : if_open commands ENDIF { - struct cmd_parse_state *ps = &parse_state; - - $$ = xcalloc(1, sizeof *$$); - $$->line = ps->input->line; - TAILQ_INIT(&$$->arguments); + TAILQ_INSERT_TAIL(&$1->children, $2, entry); + $$ = $1; } - | optional_assignment TOKEN + | if_open commands if_else commands ENDIF { - struct cmd_parse_state *ps = &parse_state; - struct cmd_parse_argument *arg; - - $$ = xcalloc(1, sizeof *$$); - $$->line = ps->input->line; - TAILQ_INIT(&$$->arguments); - - arg = xcalloc(1, sizeof *arg); - arg->type = CMD_PARSE_STRING; - arg->string = $2; - TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); + TAILQ_INSERT_TAIL(&$1->children, $2, entry); + TAILQ_INSERT_TAIL(&$3->children, $4, entry); + TAILQ_INSERT_TAIL(&$1->children, $3, entry); + $$ = $1; } - | optional_assignment TOKEN arguments + | if_open commands elif1 ENDIF { - struct cmd_parse_state *ps = &parse_state; - struct cmd_parse_argument *arg; - - $$ = xcalloc(1, sizeof *$$); - $$->line = ps->input->line; - TAILQ_INIT(&$$->arguments); - - TAILQ_CONCAT(&$$->arguments, $3, entry); - free($3); - - arg = xcalloc(1, sizeof *arg); - arg->type = CMD_PARSE_STRING; - arg->string = $2; - TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); + TAILQ_INSERT_TAIL(&$1->children, $2, entry); + cmd_parse_append(&$1->children, $3); + $$ = $1; + } + | if_open commands elif1 if_else commands ENDIF + { + TAILQ_INSERT_TAIL(&$1->children, $2, entry); + cmd_parse_append(&$1->children, $3); + TAILQ_INSERT_TAIL(&$4->children, $5, entry); + TAILQ_INSERT_TAIL(&$1->children, $4, entry); + $$ = $1; } -condition1 : if_open commands if_close - { - if ($1) - $$ = $2; - else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - } - } - | if_open commands if_else commands if_close - { - if ($1) { - $$ = $2; - cmd_parse_free_commands($4); - } else { - $$ = $4; - cmd_parse_free_commands($2); - } - } - | if_open commands elif1 if_close - { - if ($1) { - $$ = $2; - cmd_parse_free_commands($3.commands); - } else if ($3.flag) { - $$ = $3.commands; - cmd_parse_free_commands($2); - } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); - } - } - | if_open commands elif1 if_else commands if_close - { - if ($1) { - $$ = $2; - cmd_parse_free_commands($3.commands); - cmd_parse_free_commands($5); - } else if ($3.flag) { - $$ = $3.commands; - cmd_parse_free_commands($2); - cmd_parse_free_commands($5); - } else { - $$ = $5; - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); - } - } - -elif1 : if_elif commands - { - if ($1) { - $$.flag = 1; - $$.commands = $2; - } else { - $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - } - } - | if_elif commands elif1 - { - if ($1) { - $$.flag = 1; - $$.commands = $2; - cmd_parse_free_commands($3.commands); - } else if ($3.flag) { - $$.flag = 1; - $$.commands = $3.commands; - cmd_parse_free_commands($2); - } else { - $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); - } - } +elif1 : if_elif commands + { + TAILQ_INSERT_TAIL(&$1->children, $2, entry); + $$ = cmd_parse_new_nodes(); + TAILQ_INSERT_TAIL($$, $1, entry); + } + | if_elif commands elif1 + { + TAILQ_INSERT_TAIL(&$1->children, $2, entry); + $$ = cmd_parse_new_nodes(); + TAILQ_INSERT_TAIL($$, $1, entry); + cmd_parse_append($$, $3); + } arguments : argument { - $$ = xcalloc(1, sizeof *$$); - TAILQ_INIT($$); - + $$ = cmd_parse_new_nodes(); TAILQ_INSERT_HEAD($$, $1, entry); } | argument arguments @@ -567,21 +388,19 @@ arguments : argument argument : TOKEN { - $$ = xcalloc(1, sizeof *$$); - $$->type = CMD_PARSE_STRING; - $$->string = $1; + $$ = $1; } | EQUALS { - $$ = xcalloc(1, sizeof *$$); - $$->type = CMD_PARSE_STRING; - $$->string = $1; + $$ = $1; } | '{' argument_statements { - $$ = xcalloc(1, sizeof *$$); - $$->type = CMD_PARSE_COMMANDS; - $$->commands = $2; + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; + + $$ = cmd_parse_new_node(CMD_PARSE_COMMANDS, pi->line); + cmd_parse_append(&$$->children, $2); } argument_statements : statement '}' @@ -591,8 +410,7 @@ argument_statements : statement '}' | statements statement '}' { $$ = $1; - TAILQ_CONCAT($$, $2, entry); - free($2); + cmd_parse_append($$, $2); } %% @@ -609,104 +427,184 @@ cmd_parse_get_error(const char *file, u_int line, const char *error) return (s); } -static void -cmd_parse_print_commands(struct cmd_parse_input *pi, struct cmd_list *cmdlist) +static struct cmd_parse_node * +cmd_parse_new_node(enum cmd_parse_node_type type, u_int line) { - char *s; + struct cmd_parse_node *node; - if (pi->item == NULL || (~pi->flags & CMD_PARSE_VERBOSE)) + node = xcalloc(1, sizeof *node); + node->type = type; + node->line = line; + node->end_line = line; + TAILQ_INIT(&node->children); + return (node); +} + +static struct cmd_parse_nodes * +cmd_parse_new_nodes(void) +{ + struct cmd_parse_nodes *nodes; + + nodes = xmalloc(sizeof *nodes); + TAILQ_INIT(nodes); + return (nodes); +} + +static void +cmd_parse_free_node(struct cmd_parse_node *node) +{ + struct cmd_parse_node *child, *child1; + + if (node == NULL) return; - s = cmd_list_print(cmdlist, 0); - if (pi->file != NULL) - cmdq_print(pi->item, "%s:%u: %s", pi->file, pi->line, s); - else - cmdq_print(pi->item, "%u: %s", pi->line, s); - free(s); -} -static void -cmd_parse_free_argument(struct cmd_parse_argument *arg) -{ - switch (arg->type) { - case CMD_PARSE_STRING: - free(arg->string); - break; - case CMD_PARSE_COMMANDS: - cmd_parse_free_commands(arg->commands); - break; - case CMD_PARSE_PARSED_COMMANDS: - cmd_list_free(arg->cmdlist); - break; + TAILQ_FOREACH_SAFE(child, &node->children, entry, child1) { + TAILQ_REMOVE(&node->children, child, entry); + cmd_parse_free_node(child); } - free(arg); + free(node->value); + free(node); } +/* Move all nodes from src to the tail of dst, then free the src list head. */ static void -cmd_parse_free_arguments(struct cmd_parse_arguments *args) +cmd_parse_append(struct cmd_parse_nodes *dst, struct cmd_parse_nodes *src) { - struct cmd_parse_argument *arg, *arg1; + TAILQ_CONCAT(dst, src, entry); + free(src); +} - TAILQ_FOREACH_SAFE(arg, args, entry, arg1) { - TAILQ_REMOVE(args, arg, entry); - cmd_parse_free_argument(arg); +/* Wrap a single item node in a new sequence and return a one-element list. */ +static struct cmd_parse_nodes * +cmd_parse_wrap(struct cmd_parse_node *item) +{ + struct cmd_parse_nodes *nodes; + struct cmd_parse_node *seq; + + seq = cmd_parse_new_node(CMD_PARSE_SEQUENCE, item->line); + TAILQ_INSERT_TAIL(&seq->children, item, entry); + + nodes = cmd_parse_new_nodes(); + TAILQ_INSERT_TAIL(nodes, seq, entry); + return (nodes); +} + +/* + * Turn a "NAME=value" string token into an assignment node. The leading TEXT + * child is guaranteed by the lexer to begin with a valid name followed by '='. + * The name becomes the node value and the remainder of the string (text after + * '=' plus any following parts) becomes a string child. + */ +static struct cmd_parse_node * +cmd_parse_make_assign(enum cmd_parse_node_type type, + struct cmd_parse_node *string) +{ + struct cmd_parse_node *node, *value, *first, *child, *child1; + const char *cp, *eq; + + node = cmd_parse_new_node(type, string->line); + + first = TAILQ_FIRST(&string->children); + eq = strchr(first->value, '='); + node->value = xstrndup(first->value, eq - first->value); + + value = cmd_parse_new_node(CMD_PARSE_STRING, string->line); + + cp = eq + 1; + if (*cp != '\0') { + child = cmd_parse_new_node(CMD_PARSE_TEXT, string->line); + child->value = xstrdup(cp); + TAILQ_INSERT_TAIL(&value->children, child, entry); + } + TAILQ_REMOVE(&string->children, first, entry); + cmd_parse_free_node(first); + + TAILQ_FOREACH_SAFE(child, &string->children, entry, child1) { + TAILQ_REMOVE(&string->children, child, entry); + TAILQ_INSERT_TAIL(&value->children, child, entry); + } + cmd_parse_free_node(string); + + TAILQ_INSERT_TAIL(&node->children, value, entry); + return (node); +} + +/* Build a string node containing a single literal text child. */ +static struct cmd_parse_node * +cmd_parse_token_from_string(const char *s) +{ + struct cmd_parse_node *node, *child; + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; + + node = cmd_parse_new_node(CMD_PARSE_STRING, pi->line); + if (*s != '\0') { + child = cmd_parse_new_node(CMD_PARSE_TEXT, pi->line); + child->value = xstrdup(s); + TAILQ_INSERT_TAIL(&node->children, child, entry); + } + return (node); +} + +/* + * Merge consecutive sequence siblings into one at every node. This implements + * CMD_PARSE_ONEGROUP: newlines no longer split sequences, so a string parsed + * over several lines behaves as a single failure scope. + */ +static void +cmd_parse_onegroup(struct cmd_parse_node *node) +{ + struct cmd_parse_node *child, *child1, *first = NULL; + + TAILQ_FOREACH(child, &node->children, entry) + cmd_parse_onegroup(child); + + TAILQ_FOREACH_SAFE(child, &node->children, entry, child1) { + if (child->type != CMD_PARSE_SEQUENCE) { + first = NULL; + continue; + } + if (first == NULL) { + first = child; + continue; + } + TAILQ_CONCAT(&first->children, &child->children, entry); + TAILQ_REMOVE(&node->children, child, entry); + cmd_parse_free_node(child); } } -static void -cmd_parse_free_command(struct cmd_parse_command *cmd) -{ - cmd_parse_free_arguments(&cmd->arguments); - free(cmd); -} - -static struct cmd_parse_commands * -cmd_parse_new_commands(void) -{ - struct cmd_parse_commands *cmds; - - cmds = xmalloc(sizeof *cmds); - TAILQ_INIT(cmds); - return (cmds); -} - -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); - } - free(cmds); -} - -static struct cmd_parse_commands * +static struct cmd_parse_tree * cmd_parse_run_parser(char **cause) { struct cmd_parse_state *ps = &parse_state; - struct cmd_parse_scope *scope, *scope1; + struct cmd_parse_node *root; + struct cmd_parse_tree *tree; int retval; ps->commands = NULL; - 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); } - if (ps->commands == NULL) - return (cmd_parse_new_commands()); - return (ps->commands); + root = cmd_parse_new_node(CMD_PARSE_ROOT, 0); + if (ps->commands != NULL) { + cmd_parse_append(&root->children, ps->commands); + ps->commands = NULL; + } + if (ps->input->flags & CMD_PARSE_ONEGROUP) + cmd_parse_onegroup(root); + + tree = xcalloc(1, sizeof *tree); + tree->references = 1; + tree->root = root; + return (tree); } -static struct cmd_parse_commands * +static struct cmd_parse_tree * cmd_parse_do_file(FILE *f, struct cmd_parse_input *pi, char **cause) { struct cmd_parse_state *ps = &parse_state; @@ -717,7 +615,7 @@ cmd_parse_do_file(FILE *f, struct cmd_parse_input *pi, char **cause) return (cmd_parse_run_parser(cause)); } -static struct cmd_parse_commands * +static struct cmd_parse_tree * cmd_parse_do_buffer(const char *buf, size_t len, struct cmd_parse_input *pi, char **cause) { @@ -730,241 +628,37 @@ cmd_parse_do_buffer(const char *buf, size_t len, struct cmd_parse_input *pi, return (cmd_parse_run_parser(cause)); } -static void -cmd_parse_log_commands(struct cmd_parse_commands *cmds, const char *prefix) +struct cmd_parse_tree * +cmd_parse_from_file(FILE *f, struct cmd_parse_input *pi, char **cause) { - struct cmd_parse_command *cmd; - struct cmd_parse_argument *arg; - u_int i, j; - char *s; - - i = 0; - TAILQ_FOREACH(cmd, cmds, entry) { - j = 0; - TAILQ_FOREACH(arg, &cmd->arguments, entry) { - switch (arg->type) { - case CMD_PARSE_STRING: - log_debug("%s %u:%u: %s", prefix, i, j, - arg->string); - break; - case CMD_PARSE_COMMANDS: - xasprintf(&s, "%s %u:%u", prefix, i, j); - cmd_parse_log_commands(arg->commands, s); - free(s); - break; - case CMD_PARSE_PARSED_COMMANDS: - s = cmd_list_print(arg->cmdlist, 0); - log_debug("%s %u:%u: %s", prefix, i, j, s); - free(s); - break; - } - j++; - } - i++; - } -} - -static int -cmd_parse_expand_alias(struct cmd_parse_command *cmd, - struct cmd_parse_input *pi, struct cmd_parse_result *pr) -{ - struct cmd_parse_argument *first; - struct cmd_parse_commands *cmds; - struct cmd_parse_command *last; - char *alias, *name, *cause; - - if (pi->flags & CMD_PARSE_NOALIAS) - return (0); - memset(pr, 0, sizeof *pr); - - first = TAILQ_FIRST(&cmd->arguments); - if (first == NULL || first->type != CMD_PARSE_STRING) { - pr->status = CMD_PARSE_SUCCESS; - pr->cmdlist = cmd_list_new(); - return (1); - } - name = first->string; - - alias = cmd_get_alias(name); - if (alias == NULL) - return (0); - log_debug("%s: %u alias %s = %s", __func__, pi->line, name, alias); - - cmds = cmd_parse_do_buffer(alias, strlen(alias), pi, &cause); - free(alias); - if (cmds == NULL) { - pr->status = CMD_PARSE_ERROR; - pr->error = cause; - return (1); - } - - last = TAILQ_LAST(cmds, cmd_parse_commands); - if (last == NULL) { - pr->status = CMD_PARSE_SUCCESS; - pr->cmdlist = cmd_list_new(); - return (1); - } - - TAILQ_REMOVE(&cmd->arguments, first, entry); - cmd_parse_free_argument(first); - - TAILQ_CONCAT(&last->arguments, &cmd->arguments, entry); - cmd_parse_log_commands(cmds, __func__); - - pi->flags |= CMD_PARSE_NOALIAS; - cmd_parse_build_commands(cmds, pi, pr); - pi->flags &= ~CMD_PARSE_NOALIAS; - return (1); -} - -static void -cmd_parse_build_command(struct cmd_parse_command *cmd, - struct cmd_parse_input *pi, struct cmd_parse_result *pr) -{ - struct cmd_parse_argument *arg; - struct cmd *add; - char *cause; - struct args_value *values = NULL; - u_int count = 0, idx; - - memset(pr, 0, sizeof *pr); - - if (cmd_parse_expand_alias(cmd, pi, pr)) - return; - - TAILQ_FOREACH(arg, &cmd->arguments, entry) { - values = xrecallocarray(values, count, count + 1, - sizeof *values); - switch (arg->type) { - case CMD_PARSE_STRING: - values[count].type = ARGS_STRING; - values[count].string = xstrdup(arg->string); - break; - case CMD_PARSE_COMMANDS: - cmd_parse_build_commands(arg->commands, pi, pr); - if (pr->status != CMD_PARSE_SUCCESS) - goto out; - values[count].type = ARGS_COMMANDS; - values[count].cmdlist = pr->cmdlist; - break; - case CMD_PARSE_PARSED_COMMANDS: - values[count].type = ARGS_COMMANDS; - values[count].cmdlist = arg->cmdlist; - values[count].cmdlist->references++; - break; - } - count++; - } - - add = cmd_parse(values, count, pi->file, pi->line, pi->flags, &cause); - if (add == NULL) { - pr->status = CMD_PARSE_ERROR; - pr->error = cmd_parse_get_error(pi->file, pi->line, cause); - free(cause); - goto out; - } - pr->status = CMD_PARSE_SUCCESS; - pr->cmdlist = cmd_list_new(); - cmd_list_append(pr->cmdlist, add); - -out: - for (idx = 0; idx < count; idx++) - args_free_value(&values[idx]); - free(values); -} - -static void -cmd_parse_build_commands(struct cmd_parse_commands *cmds, - struct cmd_parse_input *pi, struct cmd_parse_result *pr) -{ - struct cmd_parse_command *cmd; - u_int line = UINT_MAX; - struct cmd_list *current = NULL, *result; - char *s; - - memset(pr, 0, sizeof *pr); - - /* Check for an empty list. */ - if (TAILQ_EMPTY(cmds)) { - pr->status = CMD_PARSE_SUCCESS; - pr->cmdlist = cmd_list_new(); - return; - } - cmd_parse_log_commands(cmds, __func__); - - /* - * Parse each command into a command list. Create a new command list - * for each line (unless the flag is set) 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) { - if (((~pi->flags & CMD_PARSE_ONEGROUP) && cmd->line != line)) { - if (current != NULL) { - cmd_parse_print_commands(pi, current); - cmd_list_move(result, current); - cmd_list_free(current); - } - current = cmd_list_new(); - } - if (current == NULL) - current = cmd_list_new(); - line = pi->line = cmd->line; - - cmd_parse_build_command(cmd, pi, pr); - if (pr->status != CMD_PARSE_SUCCESS) { - cmd_list_free(result); - cmd_list_free(current); - return; - } - cmd_list_append_all(current, pr->cmdlist); - cmd_list_free(pr->cmdlist); - } - if (current != NULL) { - cmd_parse_print_commands(pi, current); - cmd_list_move(result, current); - cmd_list_free(current); - } - - s = cmd_list_print(result, 0); - log_debug("%s: %s", __func__, s); - free(s); - - pr->status = CMD_PARSE_SUCCESS; - pr->cmdlist = result; -} - -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; - char *cause; + struct cmd_parse_input input; if (pi == NULL) { memset(&input, 0, sizeof input); pi = &input; } - memset(&pr, 0, sizeof pr); - - cmds = cmd_parse_do_file(f, pi, &cause); - if (cmds == NULL) { - pr.status = CMD_PARSE_ERROR; - pr.error = cause; - return (&pr); - } - cmd_parse_build_commands(cmds, pi, &pr); - cmd_parse_free_commands(cmds); - return (&pr); - + *cause = NULL; + return (cmd_parse_do_file(f, pi, cause)); } -struct cmd_parse_result * -cmd_parse_from_string(const char *s, struct cmd_parse_input *pi) +struct cmd_parse_tree * +cmd_parse_from_buffer(const void *buf, size_t len, struct cmd_parse_input *pi, + char **cause) { - struct cmd_parse_input input; + struct cmd_parse_input input; + + if (pi == NULL) { + memset(&input, 0, sizeof input); + pi = &input; + } + *cause = NULL; + return (cmd_parse_do_buffer(buf, len, pi, cause)); +} + +struct cmd_parse_tree * +cmd_parse_from_string(const char *s, struct cmd_parse_input *pi, char **cause) +{ + struct cmd_parse_input input; if (pi == NULL) { memset(&input, 0, sizeof input); @@ -977,161 +671,473 @@ cmd_parse_from_string(const char *s, struct cmd_parse_input *pi) * given as an argument to another command. */ pi->flags |= CMD_PARSE_ONEGROUP; - return (cmd_parse_from_buffer(s, strlen(s), pi)); + return (cmd_parse_from_buffer(s, strlen(s), pi, cause)); } -enum cmd_parse_status -cmd_parse_and_insert(const char *s, struct cmd_parse_input *pi, - struct cmdq_item *after, struct cmdq_state *state, char **error) +struct cmd_parse_tree * +cmd_parse_addref(struct cmd_parse_tree *tree) { - struct cmd_parse_result *pr; - struct cmdq_item *item; - - pr = cmd_parse_from_string(s, pi); - switch (pr->status) { - case CMD_PARSE_ERROR: - if (error != NULL) - *error = pr->error; - else - free(pr->error); - break; - case CMD_PARSE_SUCCESS: - item = cmdq_get_command(pr->cmdlist, state); - cmdq_insert_after(after, item); - cmd_list_free(pr->cmdlist); - break; - } - return (pr->status); + tree->references++; + return (tree); } -enum cmd_parse_status -cmd_parse_and_append(const char *s, struct cmd_parse_input *pi, - struct client *c, struct cmdq_state *state, char **error) +void +cmd_parse_free(struct cmd_parse_tree *tree) { - struct cmd_parse_result *pr; - struct cmdq_item *item; - - pr = cmd_parse_from_string(s, pi); - switch (pr->status) { - case CMD_PARSE_ERROR: - if (error != NULL) - *error = pr->error; - else - free(pr->error); - break; - case CMD_PARSE_SUCCESS: - item = cmdq_get_command(pr->cmdlist, state); - cmdq_append(c, item); - cmd_list_free(pr->cmdlist); - break; - } - return (pr->status); + if (tree == NULL) + return; + if (--tree->references != 0) + return; + cmd_parse_free_node(tree->root); + free(tree); } -struct cmd_parse_result * -cmd_parse_from_buffer(const void *buf, size_t len, struct cmd_parse_input *pi) +struct cmd_parse_node * +cmd_parse_root(struct cmd_parse_tree *tree) { - static struct cmd_parse_result pr; - struct cmd_parse_input input; - struct cmd_parse_commands *cmds; - char *cause; - - if (pi == NULL) { - memset(&input, 0, sizeof input); - pi = &input; - } - memset(&pr, 0, sizeof pr); - - if (len == 0) { - pr.status = CMD_PARSE_SUCCESS; - pr.cmdlist = cmd_list_new(); - return (&pr); - } - - cmds = cmd_parse_do_buffer(buf, len, pi, &cause); - if (cmds == NULL) { - pr.status = CMD_PARSE_ERROR; - pr.error = cause; - return (&pr); - } - cmd_parse_build_commands(cmds, pi, &pr); - cmd_parse_free_commands(cmds); - return (&pr); + return (tree->root); } -struct cmd_parse_result * -cmd_parse_from_arguments(struct args_value *values, u_int count, - struct cmd_parse_input *pi) +enum cmd_parse_node_type +cmd_parse_node_type(const struct cmd_parse_node *node) { - static struct cmd_parse_result pr; - struct cmd_parse_input input; - struct cmd_parse_commands *cmds; - struct cmd_parse_command *cmd; - struct cmd_parse_argument *arg; - u_int i; - char *copy; - size_t size; - int end; + return (node->type); +} - /* - * The commands are already split up into arguments, so just separate - * into a set of commands by ';'. - */ - - if (pi == NULL) { - memset(&input, 0, sizeof input); - pi = &input; +const char * +cmd_parse_node_type_string(enum cmd_parse_node_type type) +{ + switch (type) { + case CMD_PARSE_ROOT: + return ("ROOT"); + case CMD_PARSE_SEQUENCE: + return ("SEQUENCE"); + case CMD_PARSE_COMMAND: + return ("COMMAND"); + case CMD_PARSE_STRING: + return ("STRING"); + case CMD_PARSE_COMMANDS: + return ("COMMANDS"); + case CMD_PARSE_TEXT: + return ("TEXT"); + case CMD_PARSE_ENVIRONMENT: + return ("ENVIRONMENT"); + case CMD_PARSE_TILDE: + return ("TILDE"); + case CMD_PARSE_ASSIGN: + return ("ASSIGN"); + case CMD_PARSE_HIDDEN_ASSIGN: + return ("HIDDEN_ASSIGN"); + case CMD_PARSE_IF: + return ("IF"); + case CMD_PARSE_ELIF: + return ("ELIF"); + case CMD_PARSE_ELSE: + return ("ELSE"); } - memset(&pr, 0, sizeof pr); + return ("UNKNOWN"); +} - cmds = cmd_parse_new_commands(); +const char * +cmd_parse_node_value(const struct cmd_parse_node *node) +{ + return (node->value); +} - cmd = xcalloc(1, sizeof *cmd); - cmd->line = pi->line; - TAILQ_INIT(&cmd->arguments); +u_int +cmd_parse_node_line(const struct cmd_parse_node *node) +{ + return (node->line); +} - for (i = 0; i < count; i++) { - end = 0; - if (values[i].type == ARGS_STRING) { - copy = xstrdup(values[i].string); - size = strlen(copy); - if (size != 0 && copy[size - 1] == ';') { - copy[--size] = '\0'; - if (size > 0 && copy[size - 1] == '\\') - copy[size - 1] = ';'; - else - end = 1; - } - if (!end || size != 0) { - arg = xcalloc(1, sizeof *arg); - arg->type = CMD_PARSE_STRING; - arg->string = copy; - TAILQ_INSERT_TAIL(&cmd->arguments, arg, entry); - } else - free(copy); - } else if (values[i].type == ARGS_COMMANDS) { - arg = xcalloc(1, sizeof *arg); - arg->type = CMD_PARSE_PARSED_COMMANDS; - arg->cmdlist = values[i].cmdlist; - arg->cmdlist->references++; - TAILQ_INSERT_TAIL(&cmd->arguments, arg, entry); - } else - fatalx("unknown argument type"); - if (end) { - TAILQ_INSERT_TAIL(cmds, cmd, entry); - cmd = xcalloc(1, sizeof *cmd); - cmd->line = pi->line; - TAILQ_INIT(&cmd->arguments); +u_int +cmd_parse_node_end_line(const struct cmd_parse_node *node) +{ + return (node->end_line); +} + +struct cmd_parse_node * +cmd_parse_node_first_child(struct cmd_parse_node *node) +{ + return (TAILQ_FIRST(&node->children)); +} + +struct cmd_parse_node * +cmd_parse_node_next(struct cmd_parse_node *node) +{ + return (TAILQ_NEXT(node, entry)); +} + +/* Append a printf-style string to a growing buffer. */ +static void printflike(2, 3) +cmd_parse_strcat(char **buf, const char *fmt, ...) +{ + va_list ap; + char *add, *new; + + va_start(ap, fmt); + xvasprintf(&add, fmt, ap); + va_end(ap); + + if (*buf == NULL) + *buf = add; + else { + xasprintf(&new, "%s%s", *buf, add); + free(*buf); + free(add); + *buf = new; + } +} + +/* Escape a value for the debug tree dump. */ +static char * +cmd_parse_escape(const char *s) +{ + char *out; + size_t len = 0, size = strlen(s) * 2 + 1; + int ch; + + out = xmalloc(size); + while ((ch = (u_char)*s++) != '\0') { + if (len + 2 >= size) { + size *= 2; + out = xrealloc(out, size); + } + switch (ch) { + case '\\': + case '"': + out[len++] = '\\'; + out[len++] = ch; + break; + case '\n': + out[len++] = '\\'; + out[len++] = 'n'; + break; + case '\r': + out[len++] = '\\'; + out[len++] = 'r'; + break; + case '\t': + out[len++] = '\\'; + out[len++] = 't'; + break; + default: + out[len++] = ch; + break; } } - if (!TAILQ_EMPTY(&cmd->arguments)) - TAILQ_INSERT_TAIL(cmds, cmd, entry); - else - free(cmd); + out[len] = '\0'; + return (out); +} - cmd_parse_build_commands(cmds, pi, &pr); - cmd_parse_free_commands(cmds); - return (&pr); +static void +cmd_parse_log_node(struct cmd_parse_node *node, u_int depth) +{ + struct cmd_parse_node *child; + char *esc; + + if (node->value != NULL) { + esc = cmd_parse_escape(node->value); + log_debug("%*s%s value=\"%s\"", depth * 2, "", + cmd_parse_node_type_string(node->type), esc); + free(esc); + } else { + log_debug("%*s%s", depth * 2, "", + cmd_parse_node_type_string(node->type)); + } + + TAILQ_FOREACH(child, &node->children, entry) + cmd_parse_log_node(child, depth + 1); +} + +void +cmd_parse_log(const struct cmd_parse_tree *tree) +{ + cmd_parse_log_node(tree->root, 0); +} + +/* Does this literal text need quoting to reparse as itself? */ +static int +cmd_parse_text_safe(const char *s) +{ + const char *cp; + + if (*s == '\0') + return (0); + if (strchr("~#%", *s) != NULL) + return (0); + for (cp = s; *cp != '\0'; cp++) { + if (isalnum((u_char)*cp)) + continue; + if (strchr("_./:@+-,", *cp) == NULL) + return (0); + } + return (1); +} + +static int +cmd_parse_text_control(const char *s) +{ + for (; *s != '\0'; s++) { + if ((u_char)*s < ' ' || (u_char)*s == 0x7f) + return (1); + } + return (0); +} + +static void +cmd_parse_print_text(char **buf, const char *s) +{ + const char *cp; + u_char ch; + + /* Plain text with no special characters can be used as is. */ + if (cmd_parse_text_safe(s)) { + cmd_parse_strcat(buf, "%s", s); + return; + } + + /* + * Control bytes cannot survive single quotes (a literal newline is + * folded by the lexer), so escape with backslashes outside quotes. + */ + if (cmd_parse_text_control(s)) { + for (cp = s; *cp != '\0'; cp++) { + ch = (u_char)*cp; + switch (ch) { + case '\n': + cmd_parse_strcat(buf, "\\n"); + break; + case '\r': + cmd_parse_strcat(buf, "\\r"); + break; + case '\t': + cmd_parse_strcat(buf, "\\t"); + break; + default: + if (ch < ' ' || ch == 0x7f) + cmd_parse_strcat(buf, "\\%03o", ch); + else if (strchr(" \"'$~;{}#%\\", ch) != NULL) + cmd_parse_strcat(buf, "\\%c", ch); + else + cmd_parse_strcat(buf, "%c", ch); + break; + } + } + return; + } + + /* Otherwise single quotes keep everything literal. */ + cmd_parse_strcat(buf, "'"); + for (cp = s; *cp != '\0'; cp++) { + if (*cp == '\'') + cmd_parse_strcat(buf, "'\\''"); + else + cmd_parse_strcat(buf, "%c", *cp); + } + cmd_parse_strcat(buf, "'"); +} + +static void +cmd_parse_print_indent(char **buf, u_int depth) +{ + u_int i; + + for (i = 0; i < depth; i++) + cmd_parse_strcat(buf, "\t"); +} + +static void +cmd_parse_print_string(char **buf, struct cmd_parse_node *string) +{ + struct cmd_parse_node *child; + + if (TAILQ_EMPTY(&string->children)) { + cmd_parse_strcat(buf, "''"); + return; + } + + TAILQ_FOREACH(child, &string->children, entry) { + switch (child->type) { + case CMD_PARSE_TEXT: + cmd_parse_print_text(buf, child->value); + break; + case CMD_PARSE_ENVIRONMENT: + cmd_parse_strcat(buf, "${%s}", child->value); + break; + case CMD_PARSE_TILDE: + if (child->value != NULL && *child->value != '\0') + cmd_parse_strcat(buf, "~%s", child->value); + else + cmd_parse_strcat(buf, "~"); + break; + default: + break; + } + } +} + +static void +cmd_parse_print_commands(char **buf, struct cmd_parse_node *commands, + u_int depth) +{ + struct cmd_parse_node *child; + + if (TAILQ_EMPTY(&commands->children)) { + cmd_parse_strcat(buf, "{}"); + return; + } + + cmd_parse_strcat(buf, "{\n"); + TAILQ_FOREACH(child, &commands->children, entry) { + cmd_parse_print_indent(buf, depth + 1); + cmd_parse_print_sequence(buf, child, depth + 1); + cmd_parse_strcat(buf, "\n"); + } + cmd_parse_print_indent(buf, depth); + cmd_parse_strcat(buf, "}"); +} + +static void +cmd_parse_print_command(char **buf, struct cmd_parse_node *cmd, u_int depth) +{ + struct cmd_parse_node *child; + int first = 1; + + TAILQ_FOREACH(child, &cmd->children, entry) { + if (!first) + cmd_parse_strcat(buf, " "); + first = 0; + if (child->type == CMD_PARSE_COMMANDS) + cmd_parse_print_commands(buf, child, depth); + else + cmd_parse_print_string(buf, child); + } +} + +/* + * Emit a line break before a body line, or a space when printing on one + * line. + */ +static void +cmd_parse_print_break(char **buf, u_int depth, int oneline) +{ + if (oneline) + cmd_parse_strcat(buf, " "); + else { + cmd_parse_strcat(buf, "\n"); + cmd_parse_print_indent(buf, depth); + } +} + +/* + * An %if that shares a sequence with other commands must be printed on one + * line, because only an inline %if reparses in that position; such an %if + * always has single-sequence branch bodies, so this is lossless. An %if that + * is alone in its sequence is printed over multiple lines. + */ +static void +cmd_parse_print_if(char **buf, struct cmd_parse_node *node, u_int depth, + int oneline) +{ + struct cmd_parse_node *child, *sub; + + child = TAILQ_FIRST(&node->children); + cmd_parse_strcat(buf, "%%if "); + cmd_parse_print_string(buf, child); + + for (child = TAILQ_NEXT(child, entry); child != NULL; + child = TAILQ_NEXT(child, entry)) { + switch (child->type) { + case CMD_PARSE_SEQUENCE: + cmd_parse_print_break(buf, depth, oneline); + cmd_parse_print_sequence(buf, child, depth); + break; + case CMD_PARSE_ELIF: + sub = TAILQ_FIRST(&child->children); + cmd_parse_print_break(buf, depth, oneline); + cmd_parse_strcat(buf, "%%elif "); + cmd_parse_print_string(buf, sub); + for (sub = TAILQ_NEXT(sub, entry); sub != NULL; + sub = TAILQ_NEXT(sub, entry)) { + cmd_parse_print_break(buf, depth, oneline); + cmd_parse_print_sequence(buf, sub, depth); + } + break; + case CMD_PARSE_ELSE: + cmd_parse_print_break(buf, depth, oneline); + cmd_parse_strcat(buf, "%%else"); + TAILQ_FOREACH(sub, &child->children, entry) { + cmd_parse_print_break(buf, depth, oneline); + cmd_parse_print_sequence(buf, sub, depth); + } + break; + default: + break; + } + } + cmd_parse_print_break(buf, depth, oneline); + cmd_parse_strcat(buf, "%%endif"); +} + +static void +cmd_parse_print_item(char **buf, struct cmd_parse_node *item, u_int depth, + int oneline) +{ + switch (item->type) { + case CMD_PARSE_COMMAND: + cmd_parse_print_command(buf, item, depth); + break; + case CMD_PARSE_ASSIGN: + cmd_parse_strcat(buf, "%s=", item->value); + cmd_parse_print_string(buf, TAILQ_FIRST(&item->children)); + break; + case CMD_PARSE_HIDDEN_ASSIGN: + cmd_parse_strcat(buf, "%%hidden %s=", item->value); + cmd_parse_print_string(buf, TAILQ_FIRST(&item->children)); + break; + case CMD_PARSE_IF: + cmd_parse_print_if(buf, item, depth, oneline); + break; + default: + break; + } +} + +static void +cmd_parse_print_sequence(char **buf, struct cmd_parse_node *seq, u_int depth) +{ + struct cmd_parse_node *child; + int first = 1, oneline; + + /* More than one item in a sequence forces everything onto one line. */ + oneline = (TAILQ_FIRST(&seq->children) != + TAILQ_LAST(&seq->children, cmd_parse_nodes)); + + TAILQ_FOREACH(child, &seq->children, entry) { + if (!first) + cmd_parse_strcat(buf, " ; "); + first = 0; + cmd_parse_print_item(buf, child, depth, oneline); + } +} + +char * +cmd_parse_print(const struct cmd_parse_tree *tree) +{ + struct cmd_parse_node *root = tree->root, *child; + char *buf = NULL; + int first = 1; + + TAILQ_FOREACH(child, &root->children, entry) { + if (!first) + cmd_parse_strcat(&buf, "\n"); + first = 0; + cmd_parse_print_sequence(&buf, child, 0); + } + if (buf == NULL) + buf = xstrdup(""); + return (buf); } static void printflike(1, 2) @@ -1211,6 +1217,7 @@ static int yylex_getc(void) { struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; int ch; if (ps->escapes != 0) { @@ -1224,7 +1231,7 @@ yylex_getc(void) continue; } if (ch == '\n' && (ps->escapes % 2) == 1) { - ps->input->line++; + pi->line++; ps->escapes--; continue; } @@ -1257,15 +1264,433 @@ yylex_get_word(int ch) return (buf); } +/* Append literal bytes to the pending text of a string being scanned. */ +static void +cmd_parse_scan_add(struct cmd_parse_scan *scan, const char *add, size_t addlen) +{ + yylex_append(&scan->text, &scan->len, add, addlen); +} + +static void +cmd_parse_scan_add1(struct cmd_parse_scan *scan, char add) +{ + yylex_append1(&scan->text, &scan->len, add); +} + +/* Flush pending literal text into a text child. */ +static void +cmd_parse_scan_flush(struct cmd_parse_scan *scan) +{ + struct cmd_parse_node *child; + + if (scan->len == 0) + return; + + child = cmd_parse_new_node(CMD_PARSE_TEXT, scan->node->line); + child->value = xmalloc(scan->len + 1); + memcpy(child->value, scan->text, scan->len); + child->value[scan->len] = '\0'; + TAILQ_INSERT_TAIL(&scan->node->children, child, entry); + + scan->len = 0; +} + +/* Flush pending text, then add an environment or tilde child. */ +static void +cmd_parse_scan_part(struct cmd_parse_scan *scan, enum cmd_parse_node_type type, + const char *value) +{ + struct cmd_parse_node *child; + + cmd_parse_scan_flush(scan); + + child = cmd_parse_new_node(type, scan->node->line); + child->value = xstrdup(value); + TAILQ_INSERT_TAIL(&scan->node->children, child, entry); +} + +static int +yylex_token_escape(struct cmd_parse_scan *scan) +{ + int ch, type, o2, o3, mlen; + u_int size, i, tmp; + char s[9], m[MB_LEN_MAX]; + + ch = yylex_getc(); + + if (ch >= '4' && ch <= '7') { + yyerror("invalid octal escape"); + return (0); + } + if (ch >= '0' && ch <= '3') { + o2 = yylex_getc(); + if (o2 >= '0' && o2 <= '7') { + o3 = yylex_getc(); + if (o3 >= '0' && o3 <= '7') { + ch = 64 * (ch - '0') + + 8 * (o2 - '0') + + (o3 - '0'); + cmd_parse_scan_add1(scan, ch); + return (1); + } + } + yyerror("invalid octal escape"); + return (0); + } + + switch (ch) { + case EOF: + return (0); + case 'a': + ch = '\a'; + break; + case 'b': + ch = '\b'; + break; + case 'e': + ch = '\033'; + break; + case 'f': + ch = '\f'; + break; + case 's': + ch = ' '; + break; + case 'v': + ch = '\v'; + 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; + } + + cmd_parse_scan_add1(scan, 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); + } + mlen = wctomb(m, tmp); + if (mlen <= 0 || mlen > (int)sizeof m) { + yyerror("invalid \\%c argument", type); + return (0); + } + cmd_parse_scan_add(scan, m, mlen); + return (1); +} + +static int +yylex_token_variable(struct cmd_parse_scan *scan) +{ + int ch, brackets = 0; + char name[1024]; + size_t namelen = 0; + + ch = yylex_getc(); + if (ch == EOF) + return (0); + if (ch == '{') + brackets = 1; + else { + if (!yylex_is_var(ch, 1)) { + cmd_parse_scan_add1(scan, '$'); + yylex_ungetc(ch); + return (1); + } + name[namelen++] = ch; + } + + for (;;) { + ch = yylex_getc(); + if (brackets && ch == '}') + break; + if (ch == EOF || !yylex_is_var(ch, 0)) { + if (!brackets) { + yylex_ungetc(ch); + 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'; + + cmd_parse_scan_part(scan, CMD_PARSE_ENVIRONMENT, name); + return (1); +} + +static int +yylex_token_tilde(struct cmd_parse_scan *scan) +{ + int ch; + char name[1024]; + size_t namelen = 0; + + for (;;) { + ch = yylex_getc(); + if (ch == EOF || strchr("/ \t\n\"'", ch) != NULL) { + yylex_ungetc(ch); + break; + } + if (namelen == (sizeof name) - 2) { + yyerror("user name is too long"); + return (0); + } + name[namelen++] = ch; + } + name[namelen] = '\0'; + + cmd_parse_scan_part(scan, CMD_PARSE_TILDE, name); + return (1); +} + +/* + * Scan a single string token into a string node with ordered text, environment + * and tilde children. No expansion is performed: quote removal, line + * continuation, comments and backslash escapes are handled, but $VAR, ~ and + * formats are recorded literally for the executor to expand later. + */ +static struct cmd_parse_node * +yylex_token(int ch) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_input *pi = ps->input; + struct cmd_parse_scan scan; + int inname = 1; + size_t namelen = 0; + enum { START, + NONE, + DOUBLE_QUOTES, + SINGLE_QUOTES } state = NONE, last = START; + + memset(&scan, 0, sizeof scan); + scan.text = xmalloc(1); + scan.node = cmd_parse_new_node(CMD_PARSE_STRING, pi->line); + + for (;;) { + /* EOF or \n are always the end of the token. */ + if (ch == EOF) { + log_debug("%s: end at EOF", __func__); + break; + } + if (state == NONE && ch == '\r') { + ch = yylex_getc(); + if (ch != '\n') { + yylex_ungetc(ch); + ch = '\r'; + } + } + if (ch == '\n') { + if (state == NONE) { + log_debug("%s: end at EOL", __func__); + break; + } + pi->line++; + } + + /* Whitespace or ; or } ends a token unless inside quotes. */ + if (state == NONE && (ch == ' ' || ch == '\t')) { + log_debug("%s: end at WS", __func__); + break; + } + if (state == NONE && (ch == ';' || ch == '}')) { + log_debug("%s: end at %c", __func__, ch); + break; + } + + /* + * Spaces and comments inside quotes after \n are removed but + * the \n is left. + */ + if (ch == '\n' && state != NONE) { + cmd_parse_scan_add1(&scan, '\n'); + while ((ch = yylex_getc()) == ' ' || ch == '\t') + /* nothing */; + if (ch != '#') + continue; + ch = yylex_getc(); + if (strchr(",#{}:", ch) != NULL) { + yylex_ungetc(ch); + ch = '#'; + } else { + while ((ch = yylex_getc()) != '\n' && ch != EOF) + /* nothing */; + } + continue; + } + + /* \ ~ and $ are expanded except in single quotes. */ + if (ch == '\\' && state != SINGLE_QUOTES) { + inname = 0; + if (!yylex_token_escape(&scan)) + goto error; + goto skip; + } + if (ch == '~' && last != state && state != SINGLE_QUOTES) { + inname = 0; + if (!yylex_token_tilde(&scan)) + goto error; + goto skip; + } + if (ch == '$' && state != SINGLE_QUOTES) { + inname = 0; + if (!yylex_token_variable(&scan)) + goto error; + goto skip; + } + if (ch == '}' && state == NONE) + goto error; /* unmatched (matched ones were handled) */ + + /* + * An unquoted "NAME=" prefix is an assignment: the value after + * the '=' begins a fresh word, so a leading ~ there is a tilde. + */ + if (ch == '=' && state == NONE && inname && namelen > 0) { + cmd_parse_scan_add1(&scan, '='); + inname = 0; + last = START; + goto next; + } + + /* ' and " starts or end quotes (and is consumed). */ + if (ch == '\'') { + if (state == NONE) { + inname = 0; + state = SINGLE_QUOTES; + goto next; + } + if (state == SINGLE_QUOTES) { + state = NONE; + goto next; + } + } + if (ch == '"') { + if (state == NONE) { + inname = 0; + state = DOUBLE_QUOTES; + goto next; + } + if (state == DOUBLE_QUOTES) { + state = NONE; + goto next; + } + } + + /* Otherwise add the character to the buffer. */ + if (inname) { + if (namelen == 0 ? !yylex_is_var(ch, 1) : + !yylex_is_var(ch, 0)) + inname = 0; + else + namelen++; + } + cmd_parse_scan_add1(&scan, ch); + + skip: + last = state; + + next: + ch = yylex_getc(); + } + yylex_ungetc(ch); + + cmd_parse_scan_flush(&scan); + free(scan.text); + + return (scan.node); + +error: + cmd_parse_free_node(scan.node); + free(scan.text); + return (NULL); +} + +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(void) { struct cmd_parse_state *ps = &parse_state; - char *token, *cp; + struct cmd_parse_input *pi = ps->input; + struct cmd_parse_node *token; + struct cmd_parse_node *first; + char *word, *cp; int ch, next, condition; if (ps->eol) - ps->input->line++; + pi->line++; ps->eol = 0; condition = ps->condition; @@ -1325,15 +1750,17 @@ yylex(void) */ next = yylex_getc(); if (condition && next == '{') { - yylval.token = yylex_format(); - if (yylval.token == NULL) + word = yylex_format(); + if (word == NULL) return (ERROR); + yylval.node = cmd_parse_token_from_string(word); + free(word); return (FORMAT); } while (next != '\n' && next != EOF) next = yylex_getc(); if (next == '\n') { - ps->input->line++; + pi->line++; return ('\n'); } continue; @@ -1344,35 +1771,38 @@ yylex(void) * % is a condition unless it is all % or all numbers, * then it is a token. */ - yylval.token = yylex_get_word('%'); - for (cp = yylval.token; *cp != '\0'; cp++) { + word = yylex_get_word('%'); + for (cp = word; *cp != '\0'; cp++) { if (*cp != '%' && !isdigit((u_char)*cp)) break; } - if (*cp == '\0') + if (*cp == '\0') { + yylval.node = cmd_parse_token_from_string(word); + free(word); return (TOKEN); + } ps->condition = 1; - if (strcmp(yylval.token, "%hidden") == 0) { - free(yylval.token); + if (strcmp(word, "%hidden") == 0) { + free(word); return (HIDDEN); } - if (strcmp(yylval.token, "%if") == 0) { - free(yylval.token); + if (strcmp(word, "%if") == 0) { + free(word); return (IF); } - if (strcmp(yylval.token, "%else") == 0) { - free(yylval.token); + if (strcmp(word, "%else") == 0) { + free(word); return (ELSE); } - if (strcmp(yylval.token, "%elif") == 0) { - free(yylval.token); + if (strcmp(word, "%elif") == 0) { + free(word); return (ELIF); } - if (strcmp(yylval.token, "%endif") == 0) { - free(yylval.token); + if (strcmp(word, "%endif") == 0) { + free(word); return (ENDIF); } - free(yylval.token); + free(word); return (ERROR); } @@ -1382,11 +1812,17 @@ yylex(void) token = yylex_token(ch); if (token == NULL) return (ERROR); - yylval.token = token; + yylval.node = token; - if (strchr(token, '=') != NULL && yylex_is_var(*token, 1)) { - for (cp = token + 1; *cp != '='; cp++) { - if (!yylex_is_var(*cp, 0)) + /* + * If the token begins with a literal "NAME=", where NAME is a + * valid variable name, it is an assignment. + */ + first = TAILQ_FIRST(&token->children); + if (first != NULL && first->type == CMD_PARSE_TEXT && + yylex_is_var(first->value[0], 1)) { + for (cp = first->value + 1; *cp != '='; cp++) { + if (*cp == '\0' || !yylex_is_var(*cp, 0)) break; } if (*cp == '=') @@ -1396,365 +1832,3 @@ yylex(void) } 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, o2, o3, mlen; - u_int size, i, tmp; - char s[9], m[MB_LEN_MAX]; - - ch = yylex_getc(); - - if (ch >= '4' && ch <= '7') { - yyerror("invalid octal escape"); - return (0); - } - if (ch >= '0' && ch <= '3') { - o2 = yylex_getc(); - if (o2 >= '0' && o2 <= '7') { - o3 = yylex_getc(); - if (o3 >= '0' && o3 <= '7') { - ch = 64 * (ch - '0') + - 8 * (o2 - '0') + - (o3 - '0'); - yylex_append1(buf, len, ch); - return (1); - } - } - yyerror("invalid octal escape"); - return (0); - } - - switch (ch) { - case EOF: - return (0); - case 'a': - ch = '\a'; - break; - case 'b': - ch = '\b'; - break; - case 'e': - ch = '\033'; - break; - case 'f': - ch = '\f'; - break; - case 's': - ch = ' '; - break; - case 'v': - ch = '\v'; - 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); - } - mlen = wctomb(m, tmp); - if (mlen <= 0 || mlen > (int)sizeof m) { - yyerror("invalid \\%c argument", type); - return (0); - } - yylex_append(buf, len, m, mlen); - return (1); -} - -static int -yylex_token_variable(char **buf, size_t *len) -{ - struct environ_entry *envent; - int ch, brackets = 0; - char name[1024]; - 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, '$'); - yylex_ungetc(ch); - return (1); - } - name[namelen++] = ch; - } - - for (;;) { - ch = yylex_getc(); - if (brackets && ch == '}') - break; - if (ch == EOF || !yylex_is_var(ch, 0)) { - if (!brackets) { - yylex_ungetc(ch); - 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 && envent->value != 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 environ_entry *envent; - int ch; - char name[1024]; - size_t namelen = 0; - struct passwd *pw; - const char *home = NULL; - - for (;;) { - ch = yylex_getc(); - if (ch == EOF || strchr("/ \t\n\"'", ch) != NULL) { - yylex_ungetc(ch); - 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 != 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 (ch == EOF) { - log_debug("%s: end at EOF", __func__); - break; - } - if (state == NONE && ch == '\r') { - ch = yylex_getc(); - if (ch != '\n') { - yylex_ungetc(ch); - ch = '\r'; - } - } - if (ch == '\n') { - if (state == NONE) { - log_debug("%s: end at EOL", __func__); - break; - } - ps->input->line++; - } - - /* Whitespace or ; or } ends a token unless inside quotes. */ - if (state == NONE && (ch == ' ' || ch == '\t')) { - log_debug("%s: end at WS", __func__); - break; - } - if (state == NONE && (ch == ';' || ch == '}')) { - log_debug("%s: end at %c", __func__, ch); - break; - } - - /* - * Spaces and comments inside quotes after \n are removed but - * the \n is left. - */ - if (ch == '\n' && state != NONE) { - yylex_append1(&buf, &len, '\n'); - while ((ch = yylex_getc()) == ' ' || ch == '\t') - /* nothing */; - if (ch != '#') - continue; - ch = yylex_getc(); - if (strchr(",#{}:", ch) != NULL) { - yylex_ungetc(ch); - ch = '#'; - } else { - while ((ch = yylex_getc()) != '\n' && ch != EOF) - /* nothing */; - } - continue; - } - - /* \ ~ 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; - } - if (ch == '}' && state == NONE) - goto error; /* unmatched (matched ones were handled) */ - - /* ' 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(); - } - yylex_ungetc(ch); - - buf[len] = '\0'; - log_debug("%s: %s", __func__, buf); - return (buf); - -error: - free(buf); - return (NULL); -} diff --git a/parser-test/main.c b/parser-test/main.c new file mode 100644 index 000000000..9ca92dd0a --- /dev/null +++ b/parser-test/main.c @@ -0,0 +1,59 @@ +/* + * Standalone driver for the cmd-parse.y AST parser. Reads a config/command + * file (argument, or stdin) and prints both forms: the debug AST dump via + * cmd_parse_log (to stderr) and the normalized form via cmd_parse_print (to + * stdout). + * + * Usage: parsetest [file] + */ + +#include + +#include "tmux.h" +#include "tmux-parser.h" + +int +main(int argc, char **argv) +{ + struct cmd_parse_input pi; + struct cmd_parse_tree *tree; + FILE *f = stdin; + char *cause = NULL, *out; + + /* As tmux does at startup, so \u/\U escapes can be encoded. */ + setlocale(LC_CTYPE, ""); + + memset(&pi, 0, sizeof pi); + pi.line = 1; + if (getenv("ONEGROUP") != NULL) + pi.flags |= CMD_PARSE_ONEGROUP; + if (argc > 1) { + pi.file = argv[1]; + if ((f = fopen(argv[1], "r")) == NULL) { + perror(argv[1]); + return (1); + } + } + + tree = cmd_parse_from_file(f, &pi, &cause); + if (f != stdin) + fclose(f); + + if (tree == NULL) { + fprintf(stderr, "parse error: %s\n", + cause != NULL ? cause : "unknown"); + free(cause); + return (1); + } + + fprintf(stdout, "=== AST (stderr) ===\n"); + fflush(stdout); + cmd_parse_log(tree); + + out = cmd_parse_print(tree); + fprintf(stdout, "=== NORMALIZED ===\n%s\n", out); + free(out); + + cmd_parse_free(tree); + return (0); +} diff --git a/parser-test/tmux.h b/parser-test/tmux.h new file mode 100644 index 000000000..27d14862b --- /dev/null +++ b/parser-test/tmux.h @@ -0,0 +1,62 @@ +/* Stub tmux.h for the standalone cmd-parse.y test harness. + * + * This is NOT the real tmux.h. It provides just enough for cmd-parse.y to + * compile and link on its own: the queue macros, the allocation/logging + * helpers it calls, and the small part of struct cmd_parse_input it touches. + * It is found ahead of the real tmux.h via the harness include path. + */ + +#ifndef TMUX_TEST_STUB_H +#define TMUX_TEST_STUB_H + +#include + +#include +#include +#include +#include +#include +#include + +#include "compat/queue.h" + +#ifndef __dead +#define __dead __attribute__((__noreturn__)) +#endif +#ifndef __unused +#define __unused __attribute__((__unused__)) +#endif +#ifndef printflike +#define printflike(a, b) __attribute__((format(printf, a, b))) +#endif + +/* Allocation helpers (implemented in xstubs.c). */ +void *xmalloc(size_t); +void *xcalloc(size_t, size_t); +void *xrealloc(void *, size_t); +char *xstrdup(const char *); +char *xstrndup(const char *, size_t); +int xasprintf(char **, const char *, ...) printflike(2, 3); +int xvasprintf(char **, const char *, va_list); + +/* Logging and fatal errors (implemented in xstubs.c). */ +void log_debug(const char *, ...) printflike(1, 2); +__dead void fatal(const char *, ...) printflike(1, 2); +__dead void fatalx(const char *, ...) printflike(1, 2); + +/* + * Parser input. The real struct has more fields, but the AST parser only + * touches flags, file and line. + */ +struct cmd_parse_input { + int flags; +#define CMD_PARSE_QUIET 0x1 +#define CMD_PARSE_PARSEONLY 0x2 +#define CMD_PARSE_NOALIAS 0x4 +#define CMD_PARSE_VERBOSE 0x8 +#define CMD_PARSE_ONEGROUP 0x10 + const char *file; + u_int line; +}; + +#endif /* TMUX_TEST_STUB_H */ diff --git a/parser-test/xstubs.c b/parser-test/xstubs.c new file mode 100644 index 000000000..2866a2664 --- /dev/null +++ b/parser-test/xstubs.c @@ -0,0 +1,118 @@ +/* Minimal implementations of the helpers cmd-parse.y links against. */ + +#include "tmux.h" + +#include + +void * +xmalloc(size_t size) +{ + void *ptr; + + if (size == 0) + size = 1; + if ((ptr = malloc(size)) == NULL) + fatalx("xmalloc"); + return (ptr); +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + void *ptr; + + if (nmemb == 0 || size == 0) + nmemb = size = 1; + if ((ptr = calloc(nmemb, size)) == NULL) + fatalx("xcalloc"); + return (ptr); +} + +void * +xrealloc(void *oldptr, size_t newsize) +{ + void *ptr; + + if (newsize == 0) + newsize = 1; + if ((ptr = realloc(oldptr, newsize)) == NULL) + fatalx("xrealloc"); + return (ptr); +} + +char * +xstrdup(const char *s) +{ + char *ptr; + + if ((ptr = strdup(s)) == NULL) + fatalx("xstrdup"); + return (ptr); +} + +char * +xstrndup(const char *s, size_t maxlen) +{ + char *ptr; + + if ((ptr = strndup(s, maxlen)) == NULL) + fatalx("xstrndup"); + return (ptr); +} + +int +xvasprintf(char **ret, const char *fmt, va_list ap) +{ + int i; + + if ((i = vasprintf(ret, fmt, ap)) < 0) + fatalx("xvasprintf"); + return (i); +} + +int +xasprintf(char **ret, const char *fmt, ...) +{ + va_list ap; + int i; + + va_start(ap, fmt); + i = xvasprintf(ret, fmt, ap); + va_end(ap); + return (i); +} + +void +log_debug(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +__dead void +fatal(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", strerror(errno)); + exit(1); +} + +__dead void +fatalx(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} diff --git a/tmux-parser.h b/tmux-parser.h new file mode 100644 index 000000000..5f16568a1 --- /dev/null +++ b/tmux-parser.h @@ -0,0 +1,82 @@ +/* $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 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. + */ + +#ifndef TMUX_PARSER_H +#define TMUX_PARSER_H + +#include + +#include + +/* + * Parsed command tree. + * + * The parser builds syntax only. It does not expand formats, environment + * variables, or ~, does not evaluate %if/%elif, does not expand aliases, and + * does not build struct cmd or struct cmd_list. Those are execution-time + * operations. + * + * Command failure scope is represented by CMD_PARSE_SEQUENCE: if an invoked + * command or assignment fails, the invoker skips the remaining children of + * that sequence. No explicit group ID is stored in the tree. + */ + +struct cmd_parse_tree; +struct cmd_parse_node; +struct cmd_parse_input; + +enum cmd_parse_node_type { + CMD_PARSE_ROOT, + CMD_PARSE_SEQUENCE, + + CMD_PARSE_COMMAND, + CMD_PARSE_STRING, + CMD_PARSE_COMMANDS, + + CMD_PARSE_TEXT, + CMD_PARSE_ENVIRONMENT, + CMD_PARSE_TILDE, + + CMD_PARSE_ASSIGN, + CMD_PARSE_HIDDEN_ASSIGN, + + CMD_PARSE_IF, + CMD_PARSE_ELIF, + CMD_PARSE_ELSE +}; + +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_tree *cmd_parse_from_string(const char *, + struct cmd_parse_input *, char **); +struct cmd_parse_tree *cmd_parse_addref(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 *); +void cmd_parse_log(const struct cmd_parse_tree *); +enum cmd_parse_node_type cmd_parse_node_type(const struct cmd_parse_node *); +const char *cmd_parse_node_type_string(enum cmd_parse_node_type); +const char *cmd_parse_node_value(const struct cmd_parse_node *); +u_int cmd_parse_node_line(const struct cmd_parse_node *); +u_int cmd_parse_node_end_line(const struct cmd_parse_node *); +struct cmd_parse_node *cmd_parse_node_first_child(struct cmd_parse_node *); +struct cmd_parse_node *cmd_parse_node_next(struct cmd_parse_node *); + +#endif /* TMUX_PARSER_H */