mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-27 05:13:10 +00:00
Very early work in adopting new parser for actual execution of commands.
Not turned on yet.
This commit is contained in:
parent
ebc8bd6ff5
commit
b6af3e51ab
7 changed files with 461 additions and 208 deletions
|
@ -1862,7 +1862,7 @@ void highlighter_t::color_redirection(const parse_node_t &redirection_node)
|
||||||
if (redirection_primitive != NULL)
|
if (redirection_primitive != NULL)
|
||||||
{
|
{
|
||||||
wcstring target;
|
wcstring target;
|
||||||
const enum token_type redirect_type = this->parse_tree.type_for_redirection(redirection_node, this->buff, &target);
|
const enum token_type redirect_type = this->parse_tree.type_for_redirection(redirection_node, this->buff, NULL, &target);
|
||||||
|
|
||||||
/* We may get a TOK_NONE redirection type, e.g. if the redirection is invalid */
|
/* We may get a TOK_NONE redirection type, e.g. if the redirection is invalid */
|
||||||
this->color_node(*redirection_primitive, redirect_type == TOK_NONE ? HIGHLIGHT_ERROR : HIGHLIGHT_REDIRECTION);
|
this->color_node(*redirection_primitive, redirect_type == TOK_NONE ? HIGHLIGHT_ERROR : HIGHLIGHT_REDIRECTION);
|
||||||
|
|
|
@ -1083,6 +1083,20 @@ const parse_node_t *parse_node_tree_t::get_child(const parse_node_t &parent, nod
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parse_node_t &parse_node_tree_t::find_child(const parse_node_t &parent, parse_token_type_t type) const
|
||||||
|
{
|
||||||
|
for (size_t i=0; i < parent.child_count; i++)
|
||||||
|
{
|
||||||
|
const parse_node_t *child = this->get_child(parent, i);
|
||||||
|
if (child->type == type)
|
||||||
|
{
|
||||||
|
return *child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PARSE_ASSERT(0);
|
||||||
|
return *(parse_node_t *)(NULL); //unreachable
|
||||||
|
}
|
||||||
|
|
||||||
const parse_node_t *parse_node_tree_t::get_parent(const parse_node_t &node, parse_token_type_t expected_type) const
|
const parse_node_t *parse_node_tree_t::get_parent(const parse_node_t &node, parse_token_type_t expected_type) const
|
||||||
{
|
{
|
||||||
const parse_node_t *result = NULL;
|
const parse_node_t *result = NULL;
|
||||||
|
@ -1277,7 +1291,7 @@ bool parse_node_tree_t::plain_statement_is_in_pipeline(const parse_node_t &node,
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum token_type parse_node_tree_t::type_for_redirection(const parse_node_t &redirection_node, const wcstring &src, wcstring *out_target) const
|
enum token_type parse_node_tree_t::type_for_redirection(const parse_node_t &redirection_node, const wcstring &src, int *out_fd, wcstring *out_target) const
|
||||||
{
|
{
|
||||||
assert(redirection_node.type == symbol_redirection);
|
assert(redirection_node.type == symbol_redirection);
|
||||||
enum token_type result = TOK_NONE;
|
enum token_type result = TOK_NONE;
|
||||||
|
@ -1286,7 +1300,7 @@ enum token_type parse_node_tree_t::type_for_redirection(const parse_node_t &redi
|
||||||
|
|
||||||
if (redirection_primitive != NULL && redirection_primitive->has_source())
|
if (redirection_primitive != NULL && redirection_primitive->has_source())
|
||||||
{
|
{
|
||||||
result = redirection_type_for_string(redirection_primitive->get_source(src));
|
result = redirection_type_for_string(redirection_primitive->get_source(src), out_fd);
|
||||||
}
|
}
|
||||||
if (out_target != NULL)
|
if (out_target != NULL)
|
||||||
{
|
{
|
||||||
|
|
|
@ -166,6 +166,10 @@ public:
|
||||||
*/
|
*/
|
||||||
const parse_node_t *get_child(const parse_node_t &parent, node_offset_t which, parse_token_type_t expected_type = token_type_invalid) const;
|
const parse_node_t *get_child(const parse_node_t &parent, node_offset_t which, parse_token_type_t expected_type = token_type_invalid) const;
|
||||||
|
|
||||||
|
/* Find the first direct child of the given node of the given type. asserts on failure
|
||||||
|
*/
|
||||||
|
const parse_node_t &find_child(const parse_node_t &parent, parse_token_type_t type) const;
|
||||||
|
|
||||||
/* Get the node corresponding to the parent of the given node, or NULL if there is no such child. If expected_type is provided, only returns the parent if it is of that type. Note the asymmetry: get_child asserts since the children are known, but get_parent does not, since the parent may not be known. */
|
/* Get the node corresponding to the parent of the given node, or NULL if there is no such child. If expected_type is provided, only returns the parent if it is of that type. Note the asymmetry: get_child asserts since the children are known, but get_parent does not, since the parent may not be known. */
|
||||||
const parse_node_t *get_parent(const parse_node_t &node, parse_token_type_t expected_type = token_type_invalid) const;
|
const parse_node_t *get_parent(const parse_node_t &node, parse_token_type_t expected_type = token_type_invalid) const;
|
||||||
|
|
||||||
|
@ -197,7 +201,7 @@ public:
|
||||||
bool plain_statement_is_in_pipeline(const parse_node_t &node, bool include_first) const;
|
bool plain_statement_is_in_pipeline(const parse_node_t &node, bool include_first) const;
|
||||||
|
|
||||||
/* Given a redirection, get the redirection type (or TOK_NONE) and target (file path, or fd) */
|
/* Given a redirection, get the redirection type (or TOK_NONE) and target (file path, or fd) */
|
||||||
enum token_type type_for_redirection(const parse_node_t &node, const wcstring &src, wcstring *out_target) const;
|
enum token_type type_for_redirection(const parse_node_t &node, const wcstring &src, int *out_fd, wcstring *out_target) const;
|
||||||
|
|
||||||
/* If the given node is a block statement, returns the header node (for_header, while_header, begin_header, or function_header). Otherwise returns NULL */
|
/* If the given node is a block statement, returns the header node (for_header, while_header, begin_header, or function_header). Otherwise returns NULL */
|
||||||
const parse_node_t *header_node_for_block_statement(const parse_node_t &node);
|
const parse_node_t *header_node_for_block_statement(const parse_node_t &node);
|
||||||
|
|
574
parser.cpp
574
parser.cpp
|
@ -1648,13 +1648,12 @@ void parser_t::parse_job_argument_list(process_t *p,
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#if 0
|
|
||||||
process_t *parser_t::create_boolean_process(job_t *job, const parse_node_t &bool_statement, const parser_context_t &ctx)
|
process_t *parser_t::create_boolean_process(job_t *job, const parse_node_t &bool_statement, const parser_context_t &ctx)
|
||||||
{
|
{
|
||||||
// Handle a boolean statement
|
// Handle a boolean statement
|
||||||
bool skip_job = false;
|
bool skip_job = false;
|
||||||
assert(bool_statement.type == symbol_boolean_statement);
|
assert(bool_statement.type == symbol_boolean_statement);
|
||||||
switch (specific_statement.production_idx)
|
switch (bool_statement.production_idx)
|
||||||
{
|
{
|
||||||
// These magic numbers correspond to productions for boolean_statement
|
// These magic numbers correspond to productions for boolean_statement
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -1689,15 +1688,280 @@ process_t *parser_t::create_boolean_process(job_t *job, const parse_node_t &bool
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process_t *parser_t::create_for_process(job_t *job, const parse_node_t &header, const parse_node_t &statement, const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t *parser_t::create_while_process(job_t *job, const parse_node_t &header, const parse_node_t &statement, const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t *parser_t::create_begin_process(job_t *job, const parse_node_t &header, const parse_node_t &statement, const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t *parser_t::create_plain_process(job_t *job, const parse_node_t &statement, const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
/* Get the decoration */
|
||||||
|
assert(statement.type == symbol_plain_statement);
|
||||||
|
|
||||||
|
/* Get the command. We expect to always get it here. */
|
||||||
|
wcstring cmd;
|
||||||
|
bool got_cmd = ctx.tree.command_for_plain_statement(statement, ctx.src, &cmd);
|
||||||
|
assert(got_cmd);
|
||||||
|
|
||||||
|
/* Expand it as a command */
|
||||||
|
bool expanded = expand_one(cmd, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES);
|
||||||
|
if (! expanded)
|
||||||
|
{
|
||||||
|
error(SYNTAX_ERROR,
|
||||||
|
statement.source_start,
|
||||||
|
ILLEGAL_CMD_ERR_MSG,
|
||||||
|
cmd.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The list of arguments. The command is the first argument. TODO: count hack */
|
||||||
|
const parse_node_t *unmatched_wildcard = NULL;
|
||||||
|
wcstring_list_t argument_list = this->determine_arguments(statement, &unmatched_wildcard, ctx);
|
||||||
|
argument_list.insert(argument_list.begin(), cmd);
|
||||||
|
|
||||||
|
/* We were not able to expand any wildcards. Here is the first one that failed */
|
||||||
|
if (unmatched_wildcard != NULL)
|
||||||
|
{
|
||||||
|
job_set_flag(job, JOB_WILDCARD_ERROR, 1);
|
||||||
|
proc_set_last_status(STATUS_UNMATCHED_WILDCARD);
|
||||||
|
error(EVAL_ERROR, unmatched_wildcard->source_start, WILDCARD_ERR_MSG, unmatched_wildcard->get_source(ctx.src).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The set of IO redirections that we construct for the process */
|
||||||
|
const io_chain_t process_io_chain = this->determine_io_chain(statement, ctx);
|
||||||
|
|
||||||
|
/* Determine the process type, which depends on the statement decoration (command, builtin, etc) */
|
||||||
|
enum parse_statement_decoration_t decoration = ctx.tree.decoration_for_plain_statement(statement);
|
||||||
|
enum process_type_t process_type = EXTERNAL;
|
||||||
|
|
||||||
|
/* exec hack */
|
||||||
|
if (decoration != parse_statement_decoration_command && cmd == L"exec")
|
||||||
|
{
|
||||||
|
/* Either 'builtin exec' or just plain 'exec', and definitely not 'command exec'. Note we don't allow overriding exec with a function. */
|
||||||
|
process_type = INTERNAL_EXEC;
|
||||||
|
}
|
||||||
|
else if (decoration == parse_statement_decoration_command)
|
||||||
|
{
|
||||||
|
/* Always a command */
|
||||||
|
process_type = EXTERNAL;
|
||||||
|
}
|
||||||
|
else if (decoration == parse_statement_decoration_builtin)
|
||||||
|
{
|
||||||
|
/* What happens if this builtin is not valid? */
|
||||||
|
process_type = INTERNAL_BUILTIN;
|
||||||
|
}
|
||||||
|
else if (function_exists(cmd))
|
||||||
|
{
|
||||||
|
process_type = INTERNAL_FUNCTION;
|
||||||
|
}
|
||||||
|
else if (builtin_exists(cmd))
|
||||||
|
{
|
||||||
|
process_type = INTERNAL_BUILTIN;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
process_type = EXTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
wcstring actual_cmd;
|
||||||
|
if (process_type == EXTERNAL)
|
||||||
|
{
|
||||||
|
/* Determine the actual command. Need to support implicit cd here */
|
||||||
|
bool has_command = path_get_path(cmd, &actual_cmd);
|
||||||
|
|
||||||
|
if (! has_command)
|
||||||
|
{
|
||||||
|
/* TODO: support fish_command_not_found, implicit cd, etc. here */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the process */
|
||||||
|
process_t *result = new process_t();
|
||||||
|
result->type = process_type;
|
||||||
|
result->set_argv(argument_list);
|
||||||
|
result->set_io_chain(process_io_chain);
|
||||||
|
result->actual_cmd = actual_cmd;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Determine the list of arguments, expanding stuff. If we have a wildcard and none could be expanded, return the unexpandable wildcard node by reference. */
|
||||||
|
wcstring_list_t parser_t::determine_arguments(const parse_node_t &statement, const parse_node_t **out_unmatched_wildcard_node, const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
wcstring_list_t argument_list;
|
||||||
|
|
||||||
|
/* Whether we failed to match any wildcards, and succeeded in matching any wildcards */
|
||||||
|
bool unmatched_wildcard = false, matched_wildcard = false;
|
||||||
|
|
||||||
|
/* First node that failed to expand as a wildcard (if any) */
|
||||||
|
const parse_node_t *unmatched_wildcard_node = NULL;
|
||||||
|
|
||||||
|
/* Get all argument nodes underneath the statement */
|
||||||
|
const parse_node_tree_t::parse_node_list_t argument_nodes = ctx.tree.find_nodes(statement, symbol_argument);
|
||||||
|
argument_list.reserve(argument_nodes.size());
|
||||||
|
for (size_t i=0; i < argument_nodes.size(); i++)
|
||||||
|
{
|
||||||
|
const parse_node_t &arg_node = *argument_nodes.at(i);
|
||||||
|
|
||||||
|
/* Expect all arguments to have source */
|
||||||
|
assert(arg_node.has_source());
|
||||||
|
const wcstring arg_str = arg_node.get_source(ctx.src);
|
||||||
|
|
||||||
|
/* Expand this string */
|
||||||
|
std::vector<completion_t> arg_expanded;
|
||||||
|
int expand_ret = expand_string(arg_str, arg_expanded, 0);
|
||||||
|
switch (expand_ret)
|
||||||
|
{
|
||||||
|
case EXPAND_ERROR:
|
||||||
|
{
|
||||||
|
error(SYNTAX_ERROR,
|
||||||
|
arg_node.source_start,
|
||||||
|
_(L"Could not expand string '%ls'"),
|
||||||
|
arg_str.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EXPAND_WILDCARD_NO_MATCH:
|
||||||
|
{
|
||||||
|
/* Store the node that failed to expand */
|
||||||
|
unmatched_wildcard = true;
|
||||||
|
if (! unmatched_wildcard_node)
|
||||||
|
{
|
||||||
|
unmatched_wildcard_node = &arg_node;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EXPAND_WILDCARD_MATCH:
|
||||||
|
{
|
||||||
|
matched_wildcard = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EXPAND_OK:
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now copy over any expanded arguments */
|
||||||
|
for (size_t i=0; i < arg_expanded.size(); i++)
|
||||||
|
{
|
||||||
|
argument_list.push_back(arg_expanded.at(i).completion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return if we had a wildcard problem */
|
||||||
|
if (unmatched_wildcard && ! matched_wildcard)
|
||||||
|
{
|
||||||
|
*out_unmatched_wildcard_node = unmatched_wildcard_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return argument_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
io_chain_t parser_t::determine_io_chain(const parse_node_t &statement,const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
io_chain_t result;
|
||||||
|
|
||||||
|
/* Get all redirection nodes underneath the statement */
|
||||||
|
const parse_node_tree_t::parse_node_list_t redirect_nodes = ctx.tree.find_nodes(statement, symbol_redirection);
|
||||||
|
for (size_t i=0; i < redirect_nodes.size(); i++)
|
||||||
|
{
|
||||||
|
const parse_node_t &redirect_node = *redirect_nodes.at(i);
|
||||||
|
|
||||||
|
int source_fd = -1; /* source fd */
|
||||||
|
wcstring target; /* file path or target fd */
|
||||||
|
enum token_type redirect_type = ctx.tree.type_for_redirection(redirect_node, ctx.src, &source_fd, &target);
|
||||||
|
|
||||||
|
/* PCA: I can't justify this EXPAND_SKIP_VARIABLES flag. It was like this when I got here. */
|
||||||
|
bool target_expanded = expand_one(target, no_exec ? EXPAND_SKIP_VARIABLES : 0);
|
||||||
|
if (! target_expanded || target.empty())
|
||||||
|
{
|
||||||
|
/* Should improve this error message */
|
||||||
|
error(SYNTAX_ERROR,
|
||||||
|
redirect_node.source_start,
|
||||||
|
_(L"Invalid redirection target: %ls"),
|
||||||
|
target.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Generate the actual IO redirection */
|
||||||
|
shared_ptr<io_data_t> new_io;
|
||||||
|
assert(redirect_type != TOK_NONE);
|
||||||
|
switch (redirect_type)
|
||||||
|
{
|
||||||
|
case TOK_REDIRECT_FD:
|
||||||
|
{
|
||||||
|
if (target == L"-")
|
||||||
|
{
|
||||||
|
new_io.reset(new io_close_t(source_fd));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wchar_t *end = NULL;
|
||||||
|
errno = 0;
|
||||||
|
int old_fd = fish_wcstoi(target.c_str(), &end, 10);
|
||||||
|
if (old_fd < 0 || errno || *end)
|
||||||
|
{
|
||||||
|
error(SYNTAX_ERROR,
|
||||||
|
redirect_node.source_start,
|
||||||
|
_(L"Requested redirection to something that is not a file descriptor %ls"),
|
||||||
|
target.c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
new_io.reset(new io_fd_t(source_fd, old_fd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TOK_REDIRECT_OUT:
|
||||||
|
case TOK_REDIRECT_APPEND:
|
||||||
|
case TOK_REDIRECT_IN:
|
||||||
|
case TOK_REDIRECT_NOCLOB:
|
||||||
|
{
|
||||||
|
int oflags = oflags_for_redirection_type(redirect_type);
|
||||||
|
io_file_t *new_io_file = new io_file_t(source_fd, target, oflags);
|
||||||
|
new_io.reset(new_io_file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// Should be unreachable
|
||||||
|
fprintf(stderr, "Unexpected redirection type %ld. aborting.\n", (long)redirect_type);
|
||||||
|
PARSER_DIE();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Append the new_io if we got one */
|
||||||
|
if (new_io.get() != NULL)
|
||||||
|
{
|
||||||
|
result.push_back(new_io);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/* Returns a process_t allocated with new. It's the caller's responsibility to delete it (!) */
|
/* Returns a process_t allocated with new. It's the caller's responsibility to delete it (!) */
|
||||||
process_t *parser_t::create_job_process(job_t *job, const parse_node_t &statement_node, const parser_context_t &ctx)
|
process_t *parser_t::create_job_process(job_t *job, const parse_node_t &statement_node, const parser_context_t &ctx)
|
||||||
{
|
{
|
||||||
assert(statement_node.type == symbol_statement);
|
assert(statement_node.type == symbol_statement);
|
||||||
assert(statement_node.child_count == 1);
|
assert(statement_node.child_count == 1);
|
||||||
|
|
||||||
// We may skip this job entirely, e.g. with an 'and' statement
|
|
||||||
bool skip_job = false;
|
|
||||||
|
|
||||||
// Get the "specific statement" which is boolean / block / if / switch / decorated
|
// Get the "specific statement" which is boolean / block / if / switch / decorated
|
||||||
const parse_node_t &specific_statement = *ctx.tree.get_child(statement_node, 0);
|
const parse_node_t &specific_statement = *ctx.tree.get_child(statement_node, 0);
|
||||||
|
|
||||||
|
@ -1732,7 +1996,7 @@ process_t *parser_t::create_job_process(job_t *job, const parse_node_t &statemen
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case symbol_begin_header:
|
case symbol_begin_header:
|
||||||
|
result = this->create_begin_process(job, specific_header, specific_statement, ctx);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -1740,17 +2004,22 @@ process_t *parser_t::create_job_process(job_t *job, const parse_node_t &statemen
|
||||||
PARSER_DIE();
|
PARSER_DIE();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case symbol_decorated_statement:
|
||||||
|
{
|
||||||
|
const parse_node_t &plain_statement = ctx.tree.find_child(specific_statement, symbol_plain_statement);
|
||||||
|
result = this->create_plain_process(job, plain_statement, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "'%ls' not handled by new parser yet\n", specific_statement.describe().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand_one command
|
return result;
|
||||||
// handle booleans (and, not, or)
|
|
||||||
// set INTERNAL_EXEC
|
|
||||||
// implicit CD
|
|
||||||
|
|
||||||
return proc;
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Fully parse a single job. Does not call exec on it, but any command substitutions in the job will be executed.
|
Fully parse a single job. Does not call exec on it, but any command substitutions in the job will be executed.
|
||||||
|
@ -2456,7 +2725,6 @@ static bool job_should_skip_elseif(const job_t *job, const block_t *current_bloc
|
||||||
Evaluates a job from a node tree.
|
Evaluates a job from a node tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#if 0
|
|
||||||
void parser_t::eval_job(const parse_node_t &job_node, const parser_context_t &ctx)
|
void parser_t::eval_job(const parse_node_t &job_node, const parser_context_t &ctx)
|
||||||
{
|
{
|
||||||
assert(job_node.type == symbol_job);
|
assert(job_node.type == symbol_job);
|
||||||
|
@ -2499,7 +2767,7 @@ void parser_t::eval_job(const parse_node_t &job_node, const parser_context_t &ct
|
||||||
|| is_event \
|
|| is_event \
|
||||||
|| (!get_is_interactive()));
|
|| (!get_is_interactive()));
|
||||||
|
|
||||||
current_block->job = j;
|
current_block()->job = j;
|
||||||
|
|
||||||
/* Tell the job what its command is */
|
/* Tell the job what its command is */
|
||||||
j->set_command(job_node.get_source(ctx.src));
|
j->set_command(job_node.get_source(ctx.src));
|
||||||
|
@ -2533,124 +2801,7 @@ void parser_t::eval_job(const parse_node_t &job_node, const parser_context_t &ct
|
||||||
last_process = last_process->next;
|
last_process = last_process->next;
|
||||||
job_cont = ctx.tree.get_child(*job_cont, 2, symbol_job_continuation);
|
job_cont = ctx.tree.get_child(*job_cont, 2, symbol_job_continuation);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool skip = false;
|
|
||||||
if (this->parse_job(j->first_process, j, job_node, ctx) && j->first_process->get_argv())
|
|
||||||
{
|
|
||||||
if (do_profile)
|
|
||||||
{
|
|
||||||
t2 = get_time();
|
|
||||||
profile_item->cmd = j->command();
|
|
||||||
profile_item->skipped=current_block->skip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If we're an ELSEIF, then we may want to unskip, if we're skipping because of an IF */
|
|
||||||
if (job_get_flag(j, JOB_ELSEIF))
|
|
||||||
{
|
|
||||||
bool skip_elseif = job_should_skip_elseif(j, current_block);
|
|
||||||
|
|
||||||
/* Record that we're entering an elseif */
|
|
||||||
if (! skip_elseif)
|
|
||||||
{
|
|
||||||
/* We must be an IF block here */
|
|
||||||
assert(current_block->type() == IF);
|
|
||||||
static_cast<if_block_t *>(current_block)->is_elseif_entry = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Record that in the block too. This is similar to what builtin_else does. */
|
|
||||||
current_block->skip = skip_elseif;
|
|
||||||
}
|
|
||||||
|
|
||||||
skip = skip || current_block->skip;
|
|
||||||
skip = skip || job_get_flag(j, JOB_WILDCARD_ERROR);
|
|
||||||
skip = skip || job_get_flag(j, JOB_SKIP);
|
|
||||||
|
|
||||||
if (!skip)
|
|
||||||
{
|
|
||||||
int was_builtin = 0;
|
|
||||||
if (j->first_process->type==INTERNAL_BUILTIN && !j->first_process->next)
|
|
||||||
was_builtin = 1;
|
|
||||||
scoped_push<int> tokenizer_pos_push(¤t_tokenizer_pos, job_begin_pos);
|
|
||||||
exec_job(*this, j);
|
|
||||||
|
|
||||||
/* Only external commands require a new fishd barrier */
|
|
||||||
if (!was_builtin)
|
|
||||||
set_proc_had_barrier(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->skipped_exec(j);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (do_profile)
|
|
||||||
{
|
|
||||||
t3 = get_time();
|
|
||||||
profile_item->level=eval_level;
|
|
||||||
profile_item->parse = (int)(t2-t1);
|
|
||||||
profile_item->exec=(int)(t3-t2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_block->type() == WHILE)
|
|
||||||
{
|
|
||||||
while_block_t *wb = static_cast<while_block_t *>(current_block);
|
|
||||||
switch (wb->status)
|
|
||||||
{
|
|
||||||
case WHILE_TEST_FIRST:
|
|
||||||
{
|
|
||||||
// PCA I added the 'wb->skip ||' part because we couldn't reliably
|
|
||||||
// control-C out of loops like this: while test 1 -eq 1; end
|
|
||||||
wb->skip = wb->skip || proc_get_last_status()!= 0;
|
|
||||||
wb->status = WHILE_TESTED;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_block->type() == IF)
|
|
||||||
{
|
|
||||||
if_block_t *ib = static_cast<if_block_t *>(current_block);
|
|
||||||
|
|
||||||
if (ib->skip)
|
|
||||||
{
|
|
||||||
/* Nothing */
|
|
||||||
}
|
|
||||||
else if (! ib->if_expr_evaluated)
|
|
||||||
{
|
|
||||||
/* Execute the IF */
|
|
||||||
bool if_result = (proc_get_last_status() == 0);
|
|
||||||
ib->any_branch_taken = if_result;
|
|
||||||
|
|
||||||
/* Don't execute if the expression failed */
|
|
||||||
current_block->skip = ! if_result;
|
|
||||||
ib->if_expr_evaluated = true;
|
|
||||||
}
|
|
||||||
else if (ib->is_elseif_entry && ! ib->any_branch_taken)
|
|
||||||
{
|
|
||||||
/* Maybe mark an ELSEIF branch as taken */
|
|
||||||
bool elseif_taken = (proc_get_last_status() == 0);
|
|
||||||
ib->any_branch_taken = elseif_taken;
|
|
||||||
current_block->skip = ! elseif_taken;
|
|
||||||
ib->is_elseif_entry = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
This job could not be properly parsed. We free it
|
|
||||||
instead, and set the status to 1. This should be
|
|
||||||
rare, since most errors should be detected by the
|
|
||||||
ahead of time validator.
|
|
||||||
*/
|
|
||||||
job_free(j);
|
|
||||||
|
|
||||||
proc_set_last_status(1);
|
|
||||||
}
|
|
||||||
current_block->job = 0;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Evaluates a job from the specified tokenizer. First calls
|
Evaluates a job from the specified tokenizer. First calls
|
||||||
|
@ -2889,57 +3040,85 @@ void parser_t::eval_job(tokenizer_t *tok)
|
||||||
}
|
}
|
||||||
|
|
||||||
job_reap(0);
|
job_reap(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
static void push_all_children(std::vector<node_offset_t> *execution_stack, const parse_node_t &node)
|
||||||
int parser_t::eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_type_t block_type)
|
|
||||||
{
|
{
|
||||||
parser_context_t mut_ctx;
|
// push nodes in reverse order, so the first node ends up on top
|
||||||
mut_ctx.src = cmd_str;
|
unsigned child_idx = node.child_count;
|
||||||
|
while (child_idx--)
|
||||||
/* Parse the tree */
|
|
||||||
if (! parse_t::parse(cmd_str, parse_flag_none, &mut_ctx.tree, NULL))
|
|
||||||
{
|
{
|
||||||
return 1;
|
execution_stack->push_back(node.child_offset(child_idx));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Make a const version for safety's sake */
|
void parser_t::execute_next(std::vector<node_offset_t> *execution_stack, const parser_context_t &ctx)
|
||||||
const parser_context_t &ctx = mut_ctx;
|
{
|
||||||
|
assert(execution_stack != NULL);
|
||||||
|
assert(! execution_stack->empty());
|
||||||
|
|
||||||
CHECK_BLOCK(1);
|
/* Get the offset of the next node and remove it from the stack */
|
||||||
|
node_offset_t next_offset = execution_stack->back();
|
||||||
|
execution_stack->pop_back();
|
||||||
|
|
||||||
/* Record the current chain so we can put it back later */
|
/* Get the node */
|
||||||
scoped_push<io_chain_t> block_io_push(&block_io, io);
|
assert(next_offset < ctx.tree.size());
|
||||||
scoped_push<wcstring_list_t> forbidden_function_push(&forbidden_function);
|
const parse_node_t &node = ctx.tree.at(next_offset);
|
||||||
const size_t forbid_count = forbidden_function.size();
|
|
||||||
const block_t *start_current_block = current_block;
|
|
||||||
|
|
||||||
/* Do some stuff I haven't figured out yet */
|
/* Do something with it */
|
||||||
job_reap(0);
|
switch (node.type)
|
||||||
|
|
||||||
/* Only certain blocks are allowed */
|
|
||||||
if ((block_type != TOP) &&
|
|
||||||
(block_type != SUBST))
|
|
||||||
{
|
{
|
||||||
debug(1,
|
case symbol_job_list:
|
||||||
INVALID_SCOPE_ERR_MSG,
|
// These correspond to the three productions of job_list
|
||||||
parser_t::get_block_desc(block_type));
|
switch (node.production_idx)
|
||||||
bugreport();
|
{
|
||||||
return 1;
|
case 0: // empty
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: //job, job_list
|
||||||
|
push_all_children(execution_stack, node);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: //blank line, job_list
|
||||||
|
execution_stack->push_back(node.child_offset(1));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: //if we get here, it means more productions have been added to job_list, which is bad
|
||||||
|
PARSER_DIE();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case symbol_job: //statement, job_continuation
|
||||||
|
push_all_children(execution_stack, node);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case symbol_job_continuation:
|
||||||
|
switch (node.production_idx)
|
||||||
|
{
|
||||||
|
case 0: //empty
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: //pipe, statement, job_continuation
|
||||||
|
execution_stack->push_back(node.child_offset(2));
|
||||||
|
execution_stack->push_back(node.child_offset(1));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
PARSER_DIE();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eval_level++;
|
/* Executes the job list at the given node offset */
|
||||||
|
void parser_t::execute_job_list(node_offset_t idx, const parser_context_t &ctx)
|
||||||
|
{
|
||||||
|
assert(idx < ctx.tree.size());
|
||||||
|
|
||||||
this->push_block(new scope_block_t(block_type));
|
const parse_node_t *job_list = &ctx.tree.at(idx);
|
||||||
|
|
||||||
error_code = 0;
|
|
||||||
|
|
||||||
event_fire(NULL);
|
|
||||||
|
|
||||||
/* Execute the top job list */
|
|
||||||
assert(! ctx.tree.empty());
|
|
||||||
const parse_node_t *job_list = &ctx.tree.at(0);
|
|
||||||
assert(job_list->type == symbol_job_list);
|
assert(job_list->type == symbol_job_list);
|
||||||
while (job_list != NULL)
|
while (job_list != NULL)
|
||||||
{
|
{
|
||||||
|
@ -2971,12 +3150,60 @@ int parser_t::eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_ty
|
||||||
this->eval_job(*job, ctx);
|
this->eval_job(*job, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int parser_t::eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_type_t block_type)
|
||||||
|
{
|
||||||
|
parser_context_t mut_ctx;
|
||||||
|
mut_ctx.src = cmd_str;
|
||||||
|
|
||||||
|
/* Parse the tree */
|
||||||
|
if (! parse_t::parse(cmd_str, parse_flag_none, &mut_ctx.tree, NULL))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make a const version for safety's sake */
|
||||||
|
const parser_context_t &ctx = mut_ctx;
|
||||||
|
|
||||||
|
CHECK_BLOCK(1);
|
||||||
|
|
||||||
|
/* Record the current chain so we can put it back later */
|
||||||
|
scoped_push<io_chain_t> block_io_push(&block_io, io);
|
||||||
|
scoped_push<wcstring_list_t> forbidden_function_push(&forbidden_function);
|
||||||
|
const size_t forbid_count = forbidden_function.size();
|
||||||
|
const block_t * const start_current_block = this->current_block();
|
||||||
|
|
||||||
|
/* Do some stuff I haven't figured out yet */
|
||||||
|
job_reap(0);
|
||||||
|
|
||||||
|
/* Only certain blocks are allowed */
|
||||||
|
if ((block_type != TOP) &&
|
||||||
|
(block_type != SUBST))
|
||||||
|
{
|
||||||
|
debug(1,
|
||||||
|
INVALID_SCOPE_ERR_MSG,
|
||||||
|
parser_t::get_block_desc(block_type));
|
||||||
|
bugreport();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
eval_level++;
|
||||||
|
|
||||||
|
this->push_block(new scope_block_t(block_type));
|
||||||
|
|
||||||
|
error_code = 0;
|
||||||
|
|
||||||
|
event_fire(NULL);
|
||||||
|
|
||||||
|
/* Execute the top level job list */
|
||||||
|
execute_job_list(0, ctx);
|
||||||
|
|
||||||
parser_t::pop_block();
|
parser_t::pop_block();
|
||||||
|
|
||||||
while (start_current_block != current_block)
|
while (start_current_block != this->current_block())
|
||||||
{
|
{
|
||||||
if (current_block == 0)
|
if (this->current_block() == NULL)
|
||||||
{
|
{
|
||||||
debug(0,
|
debug(0,
|
||||||
_(L"End of block mismatch. Program terminating."));
|
_(L"End of block mismatch. Program terminating."));
|
||||||
|
@ -2991,7 +3218,7 @@ int parser_t::eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_ty
|
||||||
//debug( 2, L"Status %d\n", proc_get_last_status() );
|
//debug( 2, L"Status %d\n", proc_get_last_status() );
|
||||||
|
|
||||||
debug(1,
|
debug(1,
|
||||||
L"%ls", parser_t::get_block_desc(current_block->type()));
|
L"%ls", parser_t::get_block_desc(current_block()->type()));
|
||||||
debug(1,
|
debug(1,
|
||||||
BLOCK_END_ERR_MSG);
|
BLOCK_END_ERR_MSG);
|
||||||
fwprintf(stderr, L"%ls", parser_t::current_line());
|
fwprintf(stderr, L"%ls", parser_t::current_line());
|
||||||
|
@ -3022,7 +3249,6 @@ int parser_t::eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_ty
|
||||||
|
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
int parser_t::eval(const wcstring &cmd_str, const io_chain_t &io, enum block_type_t block_type)
|
int parser_t::eval(const wcstring &cmd_str, const io_chain_t &io, enum block_type_t block_type)
|
||||||
{
|
{
|
||||||
|
|
40
parser.h
40
parser.h
|
@ -96,37 +96,16 @@ public:
|
||||||
bool had_command; /**< Set to non-zero once a command has been executed in this block */
|
bool had_command; /**< Set to non-zero once a command has been executed in this block */
|
||||||
int tok_pos; /**< The start index of the block */
|
int tok_pos; /**< The start index of the block */
|
||||||
|
|
||||||
/**
|
/** Status for the current loop block. Can be any of the values from the loop_status enum. */
|
||||||
Status for the current loop block. Can be any of the values from the loop_status enum.
|
|
||||||
*/
|
|
||||||
int loop_status;
|
int loop_status;
|
||||||
|
|
||||||
/**
|
/** The job that is currently evaluated in the specified block. */
|
||||||
The job that is currently evaluated in the specified block.
|
|
||||||
*/
|
|
||||||
job_t *job;
|
job_t *job;
|
||||||
|
|
||||||
#if 0
|
/** Name of file that created this block */
|
||||||
union
|
|
||||||
{
|
|
||||||
int while_state; /**< True if the loop condition has not yet been evaluated*/
|
|
||||||
wchar_t *for_variable; /**< Name of the variable to loop over */
|
|
||||||
int if_state; /**< The state of the if block, can be one of IF_STATE_UNTESTED, IF_STATE_FALSE, IF_STATE_TRUE */
|
|
||||||
wchar_t *switch_value; /**< The value to test in a switch block */
|
|
||||||
const wchar_t *source_dest; /**< The name of the file to source*/
|
|
||||||
event_t *event; /**<The event that triggered this block */
|
|
||||||
wchar_t *function_call_name;
|
|
||||||
} param1;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
Name of file that created this block
|
|
||||||
*/
|
|
||||||
const wchar_t *src_filename;
|
const wchar_t *src_filename;
|
||||||
|
|
||||||
/**
|
/** Line number where this block was created */
|
||||||
Line number where this block was created
|
|
||||||
*/
|
|
||||||
int src_lineno;
|
int src_lineno;
|
||||||
|
|
||||||
/** Whether we should pop the environment variable stack when we're popped off of the block stack */
|
/** Whether we should pop the environment variable stack when we're popped off of the block stack */
|
||||||
|
@ -347,6 +326,14 @@ private:
|
||||||
|
|
||||||
process_t *create_job_process(job_t *job, const parse_node_t &statement_node, const parser_context_t &ctx);
|
process_t *create_job_process(job_t *job, const parse_node_t &statement_node, const parser_context_t &ctx);
|
||||||
process_t *create_boolean_process(job_t *job, const parse_node_t &bool_statement, const parser_context_t &ctx);
|
process_t *create_boolean_process(job_t *job, const parse_node_t &bool_statement, const parser_context_t &ctx);
|
||||||
|
process_t *create_for_process(job_t *job, const parse_node_t &header, const parse_node_t &statement, const parser_context_t &ctx);
|
||||||
|
process_t *create_while_process(job_t *job, const parse_node_t &header, const parse_node_t &statement, const parser_context_t &ctx);
|
||||||
|
process_t *create_begin_process(job_t *job, const parse_node_t &header, const parse_node_t &statement, const parser_context_t &ctx);
|
||||||
|
process_t *create_plain_process(job_t *job, const parse_node_t &statement, const parser_context_t &ctx);
|
||||||
|
|
||||||
|
wcstring_list_t determine_arguments(const parse_node_t &statement, const parse_node_t **out_unmatched_wildcard_node, const parser_context_t &ctx);
|
||||||
|
io_chain_t determine_io_chain(const parse_node_t &statement,const parser_context_t &ctx);
|
||||||
|
|
||||||
|
|
||||||
void parse_job_argument_list(process_t *p, job_t *j, tokenizer_t *tok, std::vector<completion_t>&, bool);
|
void parse_job_argument_list(process_t *p, job_t *j, tokenizer_t *tok, std::vector<completion_t>&, bool);
|
||||||
int parse_job(process_t *p, job_t *j, tokenizer_t *tok);
|
int parse_job(process_t *p, job_t *j, tokenizer_t *tok);
|
||||||
|
@ -400,6 +387,9 @@ public:
|
||||||
int eval(const wcstring &cmdStr, const io_chain_t &io, enum block_type_t block_type);
|
int eval(const wcstring &cmdStr, const io_chain_t &io, enum block_type_t block_type);
|
||||||
int eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_type_t block_type);
|
int eval2(const wcstring &cmd_str, const io_chain_t &io, enum block_type_t block_type);
|
||||||
|
|
||||||
|
void execute_job_list(node_offset_t idx, const parser_context_t &ctx);
|
||||||
|
void execute_next(std::vector<node_offset_t> *execution_stack, const parser_context_t &ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and cmdsubst execution on the tokens.
|
Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and cmdsubst execution on the tokens.
|
||||||
The output is inserted into output.
|
The output is inserted into output.
|
||||||
|
|
|
@ -14,7 +14,7 @@ segments.
|
||||||
#include <wctype.h>
|
#include <wctype.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
#include "fallback.h"
|
#include "fallback.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
@ -522,7 +522,7 @@ static size_t read_redirection_or_fd_pipe(const wchar_t *buff, enum token_type *
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum token_type redirection_type_for_string(const wcstring &str)
|
enum token_type redirection_type_for_string(const wcstring &str, int *out_fd)
|
||||||
{
|
{
|
||||||
enum token_type mode = TOK_NONE;
|
enum token_type mode = TOK_NONE;
|
||||||
int fd = 0;
|
int fd = 0;
|
||||||
|
@ -530,9 +530,25 @@ enum token_type redirection_type_for_string(const wcstring &str)
|
||||||
/* Redirections only, no pipes */
|
/* Redirections only, no pipes */
|
||||||
if (mode == TOK_PIPE || fd < 0)
|
if (mode == TOK_PIPE || fd < 0)
|
||||||
mode = TOK_NONE;
|
mode = TOK_NONE;
|
||||||
|
if (out_fd != NULL)
|
||||||
|
*out_fd = fd;
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int oflags_for_redirection_type(enum token_type type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case TOK_REDIRECT_APPEND: return O_CREAT | O_APPEND | O_WRONLY;
|
||||||
|
case TOK_REDIRECT_OUT: return O_CREAT | O_WRONLY | O_TRUNC;
|
||||||
|
case TOK_REDIRECT_NOCLOB: return O_CREAT | O_EXCL | O_WRONLY;
|
||||||
|
case TOK_REDIRECT_IN: return O_RDONLY;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wchar_t tok_last_quote(tokenizer_t *tok)
|
wchar_t tok_last_quote(tokenizer_t *tok)
|
||||||
{
|
{
|
||||||
CHECK(tok, 0);
|
CHECK(tok, 0);
|
||||||
|
|
|
@ -187,8 +187,11 @@ const wchar_t *tok_get_desc(int type);
|
||||||
*/
|
*/
|
||||||
int tok_get_error(tokenizer_t *tok);
|
int tok_get_error(tokenizer_t *tok);
|
||||||
|
|
||||||
/* Helper function to determine redirection type from a string, or TOK_NONE if the redirection is invalid */
|
/* Helper function to determine redirection type from a string, or TOK_NONE if the redirection is invalid. Also returns the fd by reference. */
|
||||||
enum token_type redirection_type_for_string(const wcstring &str);
|
enum token_type redirection_type_for_string(const wcstring &str, int *out_fd = NULL);
|
||||||
|
|
||||||
|
/* Helper function to return oflags (as in open(2)) for a redirection type */
|
||||||
|
int oflags_for_redirection_type(enum token_type type);
|
||||||
|
|
||||||
enum move_word_style_t
|
enum move_word_style_t
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue