root/usr.bin/tmux/cmd-parse.y
/* $OpenBSD: cmd-parse.y,v 1.57 2026/03/09 14:31:41 nicm Exp $ */

/*
 * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
 *
 * 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 <sys/types.h>

#include <ctype.h>
#include <errno.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wchar.h>

#include "tmux.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);

struct cmd_parse_scope {
        int                              flag;
        TAILQ_ENTRY (cmd_parse_scope)    entry;
};

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 {
        u_int                            line;
        struct cmd_parse_arguments       arguments;

        TAILQ_ENTRY(cmd_parse_command)   entry;
};
TAILQ_HEAD(cmd_parse_commands, cmd_parse_command);

struct cmd_parse_state {
        FILE                            *f;

        const char                      *buf;
        size_t                           len;
        size_t                           off;

        int                              condition;
        int                              eol;
        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 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 *);

%}

%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;
}

%token ERROR
%token HIDDEN
%token IF
%token ELSE
%token ELIF
%token ENDIF
%token <token> FORMAT TOKEN EQUALS

%type <token> expanded format
%type <arguments> arguments
%type <argument> argument
%type <flag> if_open if_elif
%type <elif> elif elif1
%type <commands> argument_statements statements statement
%type <commands> commands condition condition1
%type <command> command

%%

lines           : /* empty */
                | statements
                {
                        struct cmd_parse_state  *ps = &parse_state;

                        ps->commands = $1;
                }

statements      : statement '\n'
                {
                        $$ = $1;
                }
                | statements statement '\n'
                {
                        $$ = $1;
                        TAILQ_CONCAT($$, $2, entry);
                        free($2);
                }

statement       : /* empty */
                {
                        $$ = xmalloc (sizeof *$$);
                        TAILQ_INIT($$);
                }
                | hidden_assignment
                {
                        $$ = xmalloc (sizeof *$$);
                        TAILQ_INIT($$);
                }
                | 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);
                        }
                }
                | 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);
                        }
                }

format          : FORMAT
                {
                        $$ = $1;
                }
                | TOKEN
                {
                        $$ = $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   *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);
                }

optional_assignment     : /* empty */
                        | assignment

assignment      : EQUALS
                {
                        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 ((~flags & CMD_PARSE_PARSEONLY) && flag)
                                environ_put(global_environ, $1, 0);
                        free($1);
                }

hidden_assignment : HIDDEN EQUALS
                {
                        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 ((~flags & CMD_PARSE_PARSEONLY) && flag)
                                environ_put(global_environ, $2, ENVIRON_HIDDEN);
                        free($2);
                }

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
                {
                        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);
                        }
                }

commands        : command
                {
                        struct cmd_parse_state  *ps = &parse_state;

                        $$ = 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);
                }
                | commands ';'
                {
                        $$ = $1;
                }
                | commands ';' condition1
                {
                        $$ = $1;
                        TAILQ_CONCAT($$, $3, entry);
                        free($3);
                }
                | 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);
                        }
                }
                | condition1
                {
                        $$ = $1;
                }

command         : assignment
                {
                        struct cmd_parse_state  *ps = &parse_state;

                        $$ = xcalloc(1, sizeof *$$);
                        $$->line = ps->input->line;
                        TAILQ_INIT(&$$->arguments);
                }
                | optional_assignment TOKEN
                {
                        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);
                }
                | optional_assignment TOKEN arguments
                {
                        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);
                }

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);
                        }
                }

arguments       : argument
                {
                        $$ = xcalloc(1, sizeof *$$);
                        TAILQ_INIT($$);

                        TAILQ_INSERT_HEAD($$, $1, entry);
                }
                | argument arguments
                {
                        TAILQ_INSERT_HEAD($2, $1, entry);
                        $$ = $2;
                }

argument        : TOKEN
                {
                        $$ = xcalloc(1, sizeof *$$);
                        $$->type = CMD_PARSE_STRING;
                        $$->string = $1;
                }
                | EQUALS
                {
                        $$ = xcalloc(1, sizeof *$$);
                        $$->type = CMD_PARSE_STRING;
                        $$->string = $1;
                }
                | '{' argument_statements
                {
                        $$ = xcalloc(1, sizeof *$$);
                        $$->type = CMD_PARSE_COMMANDS;
                        $$->commands = $2;
                }

argument_statements     : statement '}'
                        {
                                $$ = $1;
                        }
                        | statements statement '}'
                        {
                                $$ = $1;
                                TAILQ_CONCAT($$, $2, entry);
                                free($2);
                        }

%%

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 void
cmd_parse_print_commands(struct cmd_parse_input *pi, struct cmd_list *cmdlist)
{
        char    *s;

        if (pi->item == NULL || (~pi->flags & CMD_PARSE_VERBOSE))
                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;
        }
        free(arg);
}

static void
cmd_parse_free_arguments(struct cmd_parse_arguments *args)
{
        struct cmd_parse_argument       *arg, *arg1;

        TAILQ_FOREACH_SAFE(arg, args, entry, arg1) {
                TAILQ_REMOVE(args, arg, entry);
                cmd_parse_free_argument(arg);
        }
}

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 *
cmd_parse_run_parser(char **cause)
{
        struct cmd_parse_state  *ps = &parse_state;
        struct cmd_parse_scope  *scope, *scope1;
        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);
}

static struct cmd_parse_commands *
cmd_parse_do_file(FILE *f, struct cmd_parse_input *pi, char **cause)
{
        struct cmd_parse_state  *ps = &parse_state;

        memset(ps, 0, sizeof *ps);
        ps->input = pi;
        ps->f = f;
        return (cmd_parse_run_parser(cause));
}

static struct cmd_parse_commands *
cmd_parse_do_buffer(const char *buf, size_t len, struct cmd_parse_input *pi,
    char **cause)
{
        struct cmd_parse_state  *ps = &parse_state;

        memset(ps, 0, sizeof *ps);
        ps->input = pi;
        ps->buf = buf;
        ps->len = len;
        return (cmd_parse_run_parser(cause));
}

static void
cmd_parse_log_commands(struct cmd_parse_commands *cmds, const char *prefix)
{
        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;

        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);

}

struct cmd_parse_result *
cmd_parse_from_string(const char *s, struct cmd_parse_input *pi)
{
        struct cmd_parse_input  input;

        if (pi == NULL) {
                memset(&input, 0, sizeof input);
                pi = &input;
        }

        /*
         * When parsing a string, put commands in one group even if there are
         * multiple lines. This means { a \n b } is identical to "a ; b" when
         * given as an argument to another command.
         */
        pi->flags |= CMD_PARSE_ONEGROUP;
        return (cmd_parse_from_buffer(s, strlen(s), pi));
}

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_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);
}

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)
{
        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);
}

struct cmd_parse_result *
cmd_parse_from_buffer(const void *buf, size_t len, struct cmd_parse_input *pi)
{
        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);
}

struct cmd_parse_result *
cmd_parse_from_arguments(struct args_value *values, u_int count,
    struct cmd_parse_input *pi)
{
        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;

        /*
         * 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;
        }
        memset(&pr, 0, sizeof pr);

        cmds = cmd_parse_new_commands();

        cmd = xcalloc(1, sizeof *cmd);
        cmd->line = pi->line;
        TAILQ_INIT(&cmd->arguments);

        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);
                }
        }
        if (!TAILQ_EMPTY(&cmd->arguments))
                TAILQ_INSERT_TAIL(cmds, cmd, entry);
        else
                free(cmd);

        cmd_parse_build_commands(cmds, pi, &pr);
        cmd_parse_free_commands(cmds);
        return (&pr);
}

static void 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;

        va_start(ap, fmt);
        xvasprintf(&error, fmt, ap);
        va_end(ap);

        ps->error = cmd_parse_get_error(pi->file, pi->line, error);
        free(error);
}

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_getc1(void)
{
        struct cmd_parse_state  *ps = &parse_state;
        int                      ch;

        if (ps->f != NULL)
                ch = getc(ps->f);
        else {
                if (ps->off == ps->len)
                        ch = EOF;
                else
                        ch = ps->buf[ps->off++];
        }
        return (ch);
}

static void
yylex_ungetc(int ch)
{
        struct cmd_parse_state  *ps = &parse_state;

        if (ps->f != NULL)
                ungetc(ch, ps->f);
        else if (ps->off > 0 && ch != EOF)
                ps->off--;
}

static int
yylex_getc(void)
{
        struct cmd_parse_state  *ps = &parse_state;
        int                      ch;

        if (ps->escapes != 0) {
                ps->escapes--;
                return ('\\');
        }
        for (;;) {
                ch = yylex_getc1();
                if (ch == '\\') {
                        ps->escapes++;
                        continue;
                }
                if (ch == '\n' && (ps->escapes % 2) == 1) {
                        ps->input->line++;
                        ps->escapes--;
                        continue;
                }

                if (ps->escapes != 0) {
                        yylex_ungetc(ch);
                        ps->escapes--;
                        return ('\\');
                }
                return (ch);
        }
}

static char *
yylex_get_word(int ch)
{
        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);
        yylex_ungetc(ch);

        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, condition;

        if (ps->eol)
                ps->input->line++;
        ps->eol = 0;

        condition = ps->condition;
        ps->condition = 0;

        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 == '\r') {
                        /*
                         * Treat \r\n as \n.
                         */
                        ch = yylex_getc();
                        if (ch != '\n') {
                                yylex_ungetc(ch);
                                ch = '\r';
                        }
                }
                if (ch == '\n') {
                        /*
                         * End of line. Update the line number.
                         */
                        ps->eol = 1;
                        return ('\n');
                }

                if (ch == ';' || ch == '{' || ch == '}') {
                        /*
                         * A semicolon or { or } is itself.
                         */
                        return (ch);
                }

                if (ch == '#') {
                        /*
                         * #{ after a condition opens a format; anything else
                         * is a comment, ignore up to the end of the line.
                         */
                        next = yylex_getc();
                        if (condition && 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 all % or all numbers,
                         * then it is a token.
                         */
                        yylval.token = yylex_get_word('%');
                        for (cp = yylval.token; *cp != '\0'; cp++) {
                                if (*cp != '%' && !isdigit((u_char)*cp))
                                        break;
                        }
                        if (*cp == '\0')
                                return (TOKEN);
                        ps->condition = 1;
                        if (strcmp(yylval.token, "%hidden") == 0) {
                                free(yylval.token);
                                return (HIDDEN);
                        }
                        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, 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);
}