tmux/cmd-parse.y
2024-11-18 08:29:35 +00:00

1749 lines
35 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 <wchar.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;
};
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 *arg, *arg1, *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_FOREACH_SAFE(arg, &cmd->arguments, entry, arg1) {
TAILQ_REMOVE(&cmd->arguments, arg, entry);
TAILQ_INSERT_TAIL(&last->arguments, arg, 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, &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 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 == '\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 != '\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)
{
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 (state == NONE && ch == '\n') {
log_debug("%s: end at EOL", __func__);
break;
}
/* 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);
}