tmux/cmd-parse.y
nicm 5afe7eb850 Do not lazily use BUFSIZ for "I don't care what size" when building
strings because it is only guaranteed to be 256 bytes and even the
default 1024 is not always enough. Reported by Gregory Pakosz.
2019-11-27 20:48:03 +00:00

1549 lines
30 KiB
Plaintext

/* $OpenBSD$ */
/*
* 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 "tmux.h"
static int yylex(void);
static int yyparse(void);
static int printflike(1,2) yyerror(const char *, ...);
static char *yylex_token(int);
static char *yylex_format(void);
struct cmd_parse_scope {
int flag;
TAILQ_ENTRY (cmd_parse_scope) entry;
};
struct cmd_parse_command {
char *name;
u_int line;
int argc;
char **argv;
TAILQ_ENTRY(cmd_parse_command) entry;
};
TAILQ_HEAD(cmd_parse_commands, cmd_parse_command);
struct cmd_parse_state {
FILE *f;
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_print_commands(struct cmd_parse_input *, u_int,
struct cmd_list *);
%}
%union
{
char *token;
struct {
int argc;
char **argv;
} arguments;
int flag;
struct {
int flag;
struct cmd_parse_commands *commands;
} elif;
struct cmd_parse_commands *commands;
struct cmd_parse_command *command;
}
%token ERROR
%token IF
%token ELSE
%token ELIF
%token ENDIF
%token <token> FORMAT TOKEN EQUALS
%type <token> argument expanded format
%type <arguments> arguments
%type <flag> if_open if_elif
%type <elif> elif elif1
%type <commands> statements statement 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 : 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);
}
}
| assignment
{
$$ = xmalloc (sizeof *$$);
TAILQ_INIT($$);
}
| 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 *fs;
int flags = FORMAT_NOJOBS;
if (cmd_find_valid_state(&pi->fs))
fs = &pi->fs;
else
fs = NULL;
ft = format_create(NULL, pi->item, FORMAT_NONE, flags);
if (fs != NULL)
format_defaults(ft, c, fs->s, fs->wl, fs->wp);
else
format_defaults(ft, c, NULL, NULL, NULL);
$$ = format_expand(ft, $1);
format_free(ft);
free($1);
}
assignment : /* empty */
| EQUALS
{
struct cmd_parse_state *ps = &parse_state;
int flags = ps->input->flags;
if ((~flags & CMD_PARSE_PARSEONLY) &&
(ps->scope == NULL || ps->scope->flag))
environ_put(global_environ, $1);
free($1);
}
if_open : IF expanded
{
struct cmd_parse_state *ps = &parse_state;
struct cmd_parse_scope *scope;
scope = xmalloc(sizeof *scope);
$$ = scope->flag = format_true($2);
free($2);
if (ps->scope != NULL)
TAILQ_INSERT_HEAD(&ps->stack, ps->scope, entry);
ps->scope = scope;
}
if_else : ELSE
{
struct cmd_parse_state *ps = &parse_state;
struct cmd_parse_scope *scope;
scope = xmalloc(sizeof *scope);
scope->flag = !ps->scope->flag;
free(ps->scope);
ps->scope = scope;
}
if_elif : ELIF expanded
{
struct cmd_parse_state *ps = &parse_state;
struct cmd_parse_scope *scope;
scope = xmalloc(sizeof *scope);
$$ = scope->flag = format_true($2);
free($2);
free(ps->scope);
ps->scope = scope;
}
if_close : ENDIF
{
struct cmd_parse_state *ps = &parse_state;
free(ps->scope);
ps->scope = TAILQ_FIRST(&ps->stack);
if (ps->scope != NULL)
TAILQ_REMOVE(&ps->stack, ps->scope, entry);
}
condition : if_open '\n' statements if_close
{
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 (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 (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 TOKEN
{
struct cmd_parse_state *ps = &parse_state;
$$ = xcalloc(1, sizeof *$$);
$$->name = $2;
$$->line = ps->input->line;
}
| assignment TOKEN arguments
{
struct cmd_parse_state *ps = &parse_state;
$$ = xcalloc(1, sizeof *$$);
$$->name = $2;
$$->line = ps->input->line;
$$->argc = $3.argc;
$$->argv = $3.argv;
}
condition1 : if_open commands if_close
{
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
{
$$.argc = 1;
$$.argv = xreallocarray(NULL, 1, sizeof *$$.argv);
$$.argv[0] = $1;
}
| argument arguments
{
cmd_prepend_argv(&$2.argc, &$2.argv, $1);
free($1);
$$ = $2;
}
argument : TOKEN
{
$$ = $1;
}
| EQUALS
{
$$ = $1;
}
%%
static char *
cmd_parse_get_error(const char *file, u_int line, const char *error)
{
char *s;
if (file == NULL)
s = xstrdup(error);
else
xasprintf (&s, "%s:%u: %s", file, line, error);
return (s);
}
static void
cmd_parse_print_commands(struct cmd_parse_input *pi, u_int line,
struct cmd_list *cmdlist)
{
char *s;
if (pi->item != NULL && (pi->flags & CMD_PARSE_VERBOSE)) {
s = cmd_list_print(cmdlist, 0);
if (pi->file != NULL)
cmdq_print(pi->item, "%s:%u: %s", pi->file, line, s);
else
cmdq_print(pi->item, "%u: %s", line, s);
free(s);
}
}
static void
cmd_parse_free_command(struct cmd_parse_command *cmd)
{
free(cmd->name);
cmd_free_argv(cmd->argc, cmd->argv);
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 struct cmd_parse_result *
cmd_parse_build_commands(struct cmd_parse_commands *cmds,
struct cmd_parse_input *pi)
{
static struct cmd_parse_result pr;
struct cmd_parse_commands *cmds2;
struct cmd_parse_command *cmd, *cmd2, *next, *next2, *after;
u_int line = UINT_MAX;
int i;
struct cmd_list *cmdlist = NULL, *result;
struct cmd *add;
char *alias, *cause, *s;
/* Check for an empty list. */
if (TAILQ_EMPTY(cmds)) {
cmd_parse_free_commands(cmds);
pr.status = CMD_PARSE_EMPTY;
return (&pr);
}
/*
* Walk the commands and expand any aliases. Each alias is parsed
* individually to a new command list, any trailing arguments appended
* to the last command, and all commands inserted into the original
* command list.
*/
TAILQ_FOREACH_SAFE(cmd, cmds, entry, next) {
alias = cmd_get_alias(cmd->name);
if (alias == NULL)
continue;
line = cmd->line;
log_debug("%s: %u %s = %s", __func__, line, cmd->name, alias);
pi->line = line;
cmds2 = cmd_parse_do_buffer(alias, strlen(alias), pi, &cause);
free(alias);
if (cmds2 == NULL) {
pr.status = CMD_PARSE_ERROR;
pr.error = cause;
goto out;
}
cmd2 = TAILQ_LAST(cmds2, cmd_parse_commands);
if (cmd2 == NULL) {
TAILQ_REMOVE(cmds, cmd, entry);
cmd_parse_free_command(cmd);
continue;
}
for (i = 0; i < cmd->argc; i++)
cmd_append_argv(&cmd2->argc, &cmd2->argv, cmd->argv[i]);
after = cmd;
TAILQ_FOREACH_SAFE(cmd2, cmds2, entry, next2) {
cmd2->line = line;
TAILQ_REMOVE(cmds2, cmd2, entry);
TAILQ_INSERT_AFTER(cmds, after, cmd2, entry);
after = cmd2;
}
cmd_parse_free_commands(cmds2);
TAILQ_REMOVE(cmds, cmd, entry);
cmd_parse_free_command(cmd);
}
/*
* Parse each command into a command list. Create a new command list
* for each line so they get a new group (so the queue knows which ones
* to remove if a command fails when executed).
*/
result = cmd_list_new();
TAILQ_FOREACH(cmd, cmds, entry) {
log_debug("%s: %u %s", __func__, cmd->line, cmd->name);
cmd_log_argv(cmd->argc, cmd->argv, __func__);
if (cmdlist == NULL || cmd->line != line) {
if (cmdlist != NULL) {
cmd_parse_print_commands(pi, line, cmdlist);
cmd_list_move(result, cmdlist);
cmd_list_free(cmdlist);
}
cmdlist = cmd_list_new();
}
line = cmd->line;
cmd_prepend_argv(&cmd->argc, &cmd->argv, cmd->name);
add = cmd_parse(cmd->argc, cmd->argv, pi->file, line, &cause);
if (add == NULL) {
cmd_list_free(result);
pr.status = CMD_PARSE_ERROR;
pr.error = cmd_parse_get_error(pi->file, line, cause);
free(cause);
goto out;
}
cmd_list_append(cmdlist, add);
}
if (cmdlist != NULL) {
cmd_parse_print_commands(pi, line, cmdlist);
cmd_list_move(result, cmdlist);
cmd_list_free(cmdlist);
}
s = cmd_list_print(result, 0);
log_debug("%s: %s", __func__, s);
free(s);
pr.status = CMD_PARSE_SUCCESS;
pr.cmdlist = result;
out:
cmd_parse_free_commands(cmds);
return (&pr);
}
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);
}
return (cmd_parse_build_commands(cmds, pi));
}
struct cmd_parse_result *
cmd_parse_from_string(const char *s, struct cmd_parse_input *pi)
{
static struct cmd_parse_result pr;
struct cmd_parse_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 (*s == '\0') {
pr.status = CMD_PARSE_EMPTY;
pr.cmdlist = NULL;
pr.error = NULL;
return (&pr);
}
cmds = cmd_parse_do_buffer(s, strlen(s), pi, &cause);
if (cmds == NULL) {
pr.status = CMD_PARSE_ERROR;
pr.error = cause;
return (&pr);
}
return (cmd_parse_build_commands(cmds, pi));
}
struct cmd_parse_result *
cmd_parse_from_arguments(int argc, char **argv, struct cmd_parse_input *pi)
{
struct cmd_parse_input input;
struct cmd_parse_commands *cmds;
struct cmd_parse_command *cmd;
char **copy, **new_argv;
size_t size;
int i, last, new_argc;
/*
* 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;
}
cmd_log_argv(argc, argv, "%s", __func__);
cmds = cmd_parse_new_commands();
copy = cmd_copy_argv(argc, argv);
last = 0;
for (i = 0; i < argc; i++) {
size = strlen(copy[i]);
if (size == 0 || copy[i][size - 1] != ';')
continue;
copy[i][--size] = '\0';
if (size > 0 && copy[i][size - 1] == '\\') {
copy[i][size - 1] = ';';
continue;
}
new_argc = i - last;
new_argv = copy + last;
if (size != 0)
new_argc++;
if (new_argc != 0) {
cmd_log_argv(new_argc, new_argv, "%s: at %u", __func__,
i);
cmd = xcalloc(1, sizeof *cmd);
cmd->name = xstrdup(new_argv[0]);
cmd->line = pi->line;
cmd->argc = new_argc - 1;
cmd->argv = cmd_copy_argv(new_argc - 1, new_argv + 1);
TAILQ_INSERT_TAIL(cmds, cmd, entry);
}
last = i + 1;
}
if (last != argc) {
new_argv = copy + last;
new_argc = argc - last;
if (new_argc != 0) {
cmd_log_argv(new_argc, new_argv, "%s: at %u", __func__,
last);
cmd = xcalloc(1, sizeof *cmd);
cmd->name = xstrdup(new_argv[0]);
cmd->line = pi->line;
cmd->argc = new_argc - 1;
cmd->argv = cmd_copy_argv(new_argc - 1, new_argv + 1);
TAILQ_INSERT_TAIL(cmds, cmd, entry);
}
}
cmd_free_argv(argc, copy);
return (cmd_parse_build_commands(cmds, pi));
}
static int printflike(1, 2)
yyerror(const char *fmt, ...)
{
struct cmd_parse_state *ps = &parse_state;
struct cmd_parse_input *pi = ps->input;
va_list ap;
char *error;
if (ps->error != NULL)
return (0);
va_start(ap, fmt);
xvasprintf(&error, fmt, ap);
va_end(ap);
ps->error = cmd_parse_get_error(pi->file, pi->line, error);
free(error);
return (0);
}
static int
yylex_is_var(char ch, int first)
{
if (ch == '=')
return (0);
if (first && isdigit((u_char)ch))
return (0);
return (isalnum((u_char)ch) || ch == '_');
}
static void
yylex_append(char **buf, size_t *len, const char *add, size_t addlen)
{
if (addlen > SIZE_MAX - 1 || *len > SIZE_MAX - 1 - addlen)
fatalx("buffer is too big");
*buf = xrealloc(*buf, (*len) + 1 + addlen);
memcpy((*buf) + *len, add, addlen);
(*len) += addlen;
}
static void
yylex_append1(char **buf, size_t *len, char add)
{
yylex_append(buf, len, &add, 1);
}
static int
yylex_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 == '\n') {
/*
* End of line. Update the line number.
*/
ps->eol = 1;
return ('\n');
}
if (ch == ';') {
/*
* A semicolon is itself.
*/
return (';');
}
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, "%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;
u_int size, i, tmp;
char s[9];
struct utf8_data ud;
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);
}
if (utf8_split(tmp, &ud) != UTF8_DONE) {
yyerror("invalid \\%c argument", type);
return (0);
}
yylex_append(buf, len, ud.data, ud.size);
return (1);
}
static int
yylex_token_variable(char **buf, size_t *len)
{
struct 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) {
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 != '\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 int
yylex_token_brace(char **buf, size_t *len)
{
struct cmd_parse_state *ps = &parse_state;
int ch, lines = 0, nesting = 1, escape = 0;
int quote = '\0', token = 0;
/*
* Extract a string up to the matching unquoted '}', including newlines
* and handling nested braces.
*
* To detect the final and intermediate braces which affect the nesting
* depth, we scan the input as if it was a tmux config file, and ignore
* braces which would be considered quoted, escaped, or in a comment.
*
* We update the token state after every character because '#' begins a
* comment only when it begins a token. For simplicity, we treat an
* unquoted directive format as comment.
*
* The result is verbatim copy of the input excluding the final brace.
*/
for (ch = yylex_getc1(); ch != EOF; ch = yylex_getc1()) {
yylex_append1(buf, len, ch);
if (ch == '\n')
lines++;
/*
* If the previous character was a backslash (escape is set),
* escape anything if unquoted or in double quotes, otherwise
* escape only '\n' and '\\'.
*/
if (escape &&
(quote == '\0' ||
quote == '"' ||
ch == '\n' ||
ch == '\\')) {
escape = 0;
if (ch != '\n')
token = 1;
continue;
}
/*
* The character is not escaped. If it is a backslash, set the
* escape flag.
*/
if (ch == '\\') {
escape = 1;
continue;
}
escape = 0;
/* A newline always resets to unquoted. */
if (ch == '\n') {
quote = token = 0;
continue;
}
if (quote) {
/*
* Inside quotes or comment. Check if this is the
* closing quote.
*/
if (ch == quote && quote != '#')
quote = 0;
token = 1; /* token continues regardless */
} else {
/* Not inside quotes or comment. */
switch (ch) {
case '"':
case '\'':
case '#':
/* Beginning of quote or maybe comment. */
if (ch != '#' || !token)
quote = ch;
token = 1;
break;
case ' ':
case '\t':
case ';':
/* Delimiter - token resets. */
token = 0;
break;
case '{':
nesting++;
token = 0; /* new commands set - token resets */
break;
case '}':
nesting--;
token = 1; /* same as after quotes */
if (nesting == 0) {
(*len)--; /* remove closing } */
ps->input->line += lines;
return (1);
}
break;
default:
token = 1;
break;
}
}
}
/*
* Update line count after error as reporting the opening line is more
* useful than EOF.
*/
yyerror("unterminated brace string");
ps->input->line += lines;
return (0);
}
static char *
yylex_token(int ch)
{
char *buf;
size_t len;
enum { START,
NONE,
DOUBLE_QUOTES,
SINGLE_QUOTES } state = NONE, last = START;
len = 0;
buf = xmalloc(1);
for (;;) {
/*
* EOF or \n are always the end of the token. If inside quotes
* they are an error.
*/
if (ch == EOF || ch == '\n') {
if (state != NONE)
goto error;
break;
}
/* Whitespace or ; ends a token unless inside quotes. */
if ((ch == ' ' || ch == '\t' || ch == ';') && state == NONE)
break;
/*
* \ ~ and $ are expanded except in single quotes.
*/
if (ch == '\\' && state != SINGLE_QUOTES) {
if (!yylex_token_escape(&buf, &len))
goto error;
goto skip;
}
if (ch == '~' && last != state && state != SINGLE_QUOTES) {
if (!yylex_token_tilde(&buf, &len))
goto error;
goto skip;
}
if (ch == '$' && state != SINGLE_QUOTES) {
if (!yylex_token_variable(&buf, &len))
goto error;
goto skip;
}
if (ch == '{' && state == NONE) {
if (!yylex_token_brace(&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);
}