mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-29 05:03:46 +08:00
Support FOO=bar syntax for passing variables to individual commands
This adds initial support for statements with prefixed variable assignments. Statments like this are supported: a=1 b=$a echo $b # outputs 1 Just like in other shells, the left-hand side of each assignment must be a valid variable identifier (no quoting/escaping). Array indexing (PATH[1]=/bin ls $PATH) is *not* yet supported, but can be added fairly easily. The right hand side may be any valid string token, like a command substitution, or a brace expansion. Since `a=* foo` is equivalent to `begin set -lx a *; foo; end`, the assignment, like `set`, uses nullglob behavior, e.g. below command can safely be used to check if a directory is empty. x=/nothing/{,.}* test (count $x) -eq 0 Generic file completion is done after the equal sign, so for example pressing tab after something like `HOME=/` completes files in the root directory Subcommand completion works, so something like `GIT_DIR=repo.git and command git ` correctly calls git completions (but the git completion does not use the variable as of now). The variable assignment is highlighted like an argument. Closes #6048
This commit is contained in:
parent
3b0f642de9
commit
7d5b44e828
|
@ -16,6 +16,7 @@
|
|||
- Brace expansion now only takes place if the braces include a "," or a variable expansion, meaning common commands such as `git reset HEAD@{0}` do not require escaping (#5869).
|
||||
- New redirections `&>` and `&|` may be used to redirect or pipe stdout, and also redirect stderr to stdout (#6192).
|
||||
- `switch` now allows arguments that expand to nothing, like empty variables (#5677).
|
||||
- The `VAR=val cmd` syntax can now be used to run a command in a modified environment (#6287).
|
||||
|
||||
### Scripting improvements
|
||||
- `string split0` now returns 0 if it split something (#5701).
|
||||
|
|
|
@ -119,6 +119,12 @@ Examples
|
|||
echo "Python is at $python_path"
|
||||
end
|
||||
|
||||
# Like other shells, fish 3.1 supports this syntax for passing a variable to just one command:
|
||||
# Run fish with a temporary home directory.
|
||||
HOME=(mktemp -d) fish
|
||||
# Which is essentially the same as:
|
||||
begin; set -lx HOME (mktemp -d); fish; end
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ Use the :ref:`set <cmd-set>` command::
|
|||
set -x key value
|
||||
set -e key
|
||||
|
||||
Since fish 3.1 you can set an environment variable for just one command using the ``key=value some command`` syntax, like in other shells. The two lines below behave identically - unlike other shells, fish will output ``value`` both times::
|
||||
|
||||
key=value echo $key
|
||||
begin; set -lx key value; echo $key; end
|
||||
|
||||
How do I run a command every login? What's fish's equivalent to .bashrc?
|
||||
------------------------------------------------------------------------
|
||||
|
@ -95,22 +99,6 @@ If you are just interested in success or failure, you can run the command direct
|
|||
|
||||
See the documentation for :ref:`test <cmd-test>` and :ref:`if <cmd-if>` for more information.
|
||||
|
||||
How do I set an environment variable for just one command?
|
||||
----------------------------------------------------------
|
||||
``SOME_VAR=1 command`` produces an error: ``Unknown command "SOME_VAR=1"``.
|
||||
|
||||
Use the ``env`` command.
|
||||
|
||||
``env SOME_VAR=1 command``
|
||||
|
||||
You can also declare a local variable in a block::
|
||||
|
||||
begin
|
||||
set -lx SOME_VAR 1
|
||||
command
|
||||
end
|
||||
|
||||
|
||||
How do I check whether a variable is defined?
|
||||
---------------------------------------------
|
||||
|
||||
|
|
|
@ -1511,6 +1511,11 @@ void completer_t::perform() {
|
|||
}
|
||||
|
||||
if (cmd_tok.location_in_or_at_end_of_source_range(cursor_pos)) {
|
||||
maybe_t<size_t> equal_sign_pos = variable_assignment_equals_pos(current_token);
|
||||
if (equal_sign_pos) {
|
||||
complete_param_expand(current_token.substr(*equal_sign_pos + 1), true /* do_file */);
|
||||
return;
|
||||
}
|
||||
// Complete command filename.
|
||||
complete_cmd(current_token);
|
||||
complete_abbr(current_token);
|
||||
|
@ -1570,9 +1575,30 @@ void completer_t::perform() {
|
|||
if (wants_transient) {
|
||||
parser->libdata().transient_commandlines.push_back(cmdline);
|
||||
}
|
||||
// Now invoke any custom completions for this command.
|
||||
if (!complete_param(cmd, previous_argument_unescape, current_argument_unescape,
|
||||
!had_ddash)) {
|
||||
bool is_variable_assignment = bool(variable_assignment_equals_pos(cmd));
|
||||
if (is_variable_assignment && parser) {
|
||||
// To avoid issues like #2705 we complete commands starting with variable
|
||||
// assignments by recursively calling complete for the command suffix
|
||||
// without the first variable assignment token.
|
||||
wcstring unaliased_cmd;
|
||||
if (parser->libdata().transient_commandlines.empty()) {
|
||||
unaliased_cmd = cmdline;
|
||||
} else {
|
||||
unaliased_cmd = parser->libdata().transient_commandlines.back();
|
||||
}
|
||||
tokenizer_t tok(unaliased_cmd.c_str(), TOK_ACCEPT_UNFINISHED);
|
||||
maybe_t<tok_t> cmd_tok = tok.next();
|
||||
assert(cmd_tok);
|
||||
unaliased_cmd = unaliased_cmd.replace(0, cmd_tok->offset + cmd_tok->length, L"");
|
||||
parser->libdata().transient_commandlines.push_back(unaliased_cmd);
|
||||
cleanup_t remove_transient([&] { parser->libdata().transient_commandlines.pop_back(); });
|
||||
std::vector<completion_t> comp;
|
||||
complete(unaliased_cmd, &comp,
|
||||
completion_request_t::fuzzy_match, parser->vars(), parser->shared());
|
||||
this->completions.insert(completions.end(), comp.begin(), comp.end());
|
||||
do_file = false;
|
||||
} else if (!complete_param(cmd, previous_argument_unescape, current_argument_unescape,
|
||||
!had_ddash)) { // Invoke any custom completions for this command.
|
||||
do_file = false;
|
||||
}
|
||||
if (wants_transient) {
|
||||
|
|
11
src/exec.cpp
11
src/exec.cpp
|
@ -961,6 +961,17 @@ static bool exec_process_in_job(parser_t &parser, process_t *p, std::shared_ptr<
|
|||
parser.libdata().exec_count++;
|
||||
}
|
||||
|
||||
const block_t *block = nullptr;
|
||||
cleanup_t pop_block([&]() {
|
||||
if (block) parser.pop_block(block);
|
||||
});
|
||||
if (!p->variable_assignments.empty()) {
|
||||
block = parser.push_block(block_t::variable_assignment_block());
|
||||
}
|
||||
for (const auto &assignment : p->variable_assignments) {
|
||||
parser.vars().set(assignment.variable_name, ENV_LOCAL | ENV_EXPORT, assignment.values);
|
||||
}
|
||||
|
||||
// Execute the process.
|
||||
p->check_generations_before_launch();
|
||||
switch (p->type) {
|
||||
|
|
|
@ -4626,6 +4626,7 @@ static void test_highlighting() {
|
|||
});
|
||||
|
||||
highlight_tests.push_back({
|
||||
{L"HOME=.", highlight_role_t::param},
|
||||
{L"false", highlight_role_t::command},
|
||||
{L"|&", highlight_role_t::error},
|
||||
{L"true", highlight_role_t::command},
|
||||
|
|
|
@ -1200,6 +1200,12 @@ highlighter_t::color_array_t highlighter_t::highlight() {
|
|||
this->color_node(node, highlight_role_t::operat);
|
||||
break;
|
||||
|
||||
case symbol_variable_assignment: {
|
||||
tnode_t<g::variable_assignment> variable_assignment = {&parse_tree, &node};
|
||||
this->color_argument(variable_assignment.child<0>());
|
||||
break;
|
||||
}
|
||||
|
||||
case parse_token_type_pipe:
|
||||
case parse_token_type_background:
|
||||
case parse_token_type_end:
|
||||
|
@ -1223,6 +1229,8 @@ highlighter_t::color_array_t highlighter_t::highlight() {
|
|||
if (!this->io_ok) {
|
||||
// We cannot check if the command is invalid, so just assume it's valid.
|
||||
is_valid_cmd = true;
|
||||
} else if (variable_assignment_equals_pos(*cmd)) {
|
||||
is_valid_cmd = true;
|
||||
} else {
|
||||
wcstring expanded_cmd;
|
||||
// Check to see if the command is valid.
|
||||
|
|
|
@ -40,6 +40,8 @@ enum parse_token_type_t : uint8_t {
|
|||
symbol_not_statement,
|
||||
symbol_decorated_statement,
|
||||
symbol_plain_statement,
|
||||
symbol_variable_assignment,
|
||||
symbol_variable_assignments,
|
||||
symbol_arguments_or_redirections_list,
|
||||
symbol_andor_job_list,
|
||||
symbol_argument_list,
|
||||
|
@ -277,11 +279,6 @@ void parse_error_offset_source_start(parse_error_list_t *errors, size_t amt);
|
|||
/// Error issued on $.
|
||||
#define ERROR_NO_VAR_NAME _(L"Expected a variable name after this $.")
|
||||
|
||||
/// Error on foo=bar.
|
||||
#define ERROR_BAD_EQUALS_IN_COMMAND5 \
|
||||
_(L"Unsupported use of '='. To run '%ls' with a modified environment, please use 'env " \
|
||||
L"%ls=%ls %ls%ls'")
|
||||
|
||||
/// Error message for Posix-style assignment: foo=bar.
|
||||
#define ERROR_BAD_COMMAND_ASSIGN_ERR_MSG \
|
||||
_(L"Unsupported use of '='. In fish, please use 'set %ls %ls'.")
|
||||
|
|
|
@ -123,8 +123,8 @@ tnode_t<g::plain_statement> parse_execution_context_t::infinite_recursive_statem
|
|||
// Get the list of plain statements.
|
||||
// Ignore statements with decorations like 'builtin' or 'command', since those
|
||||
// are not infinite recursion. In particular that is what enables 'wrapper functions'.
|
||||
tnode_t<g::statement> statement = first_job.child<0>();
|
||||
tnode_t<g::job_continuation> continuation = first_job.child<1>();
|
||||
tnode_t<g::statement> statement = first_job.child<1>();
|
||||
tnode_t<g::job_continuation> continuation = first_job.child<2>();
|
||||
const null_environment_t nullenv{};
|
||||
while (statement) {
|
||||
tnode_t<g::plain_statement> plain_statement =
|
||||
|
@ -207,10 +207,10 @@ parse_execution_context_t::cancellation_reason(const block_t *block) const {
|
|||
|
||||
/// Return whether the job contains a single statement, of block type, with no redirections.
|
||||
bool parse_execution_context_t::job_is_simple_block(tnode_t<g::job> job_node) const {
|
||||
tnode_t<g::statement> statement = job_node.child<0>();
|
||||
tnode_t<g::statement> statement = job_node.child<1>();
|
||||
|
||||
// Must be no pipes.
|
||||
if (job_node.child<1>().try_get_child<g::tok_pipe, 0>()) {
|
||||
if (job_node.child<2>().try_get_child<g::tok_pipe, 0>()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -713,27 +713,7 @@ parse_execution_result_t parse_execution_context_t::handle_command_not_found(
|
|||
// status to 127, which is the standard number used by other shells like bash and zsh.
|
||||
|
||||
const wchar_t *const cmd = cmd_str.c_str();
|
||||
const wchar_t *const equals_ptr = std::wcschr(cmd, L'=');
|
||||
if (equals_ptr != NULL) {
|
||||
// Try to figure out if this is a pure variable assignment (foo=bar), or if this appears to
|
||||
// be running a command (foo=bar ruby...).
|
||||
const wcstring name_str = wcstring(cmd, equals_ptr - cmd); // variable name, up to the =
|
||||
const wcstring val_str = wcstring(equals_ptr + 1); // variable value, past the =
|
||||
|
||||
auto args = statement.descendants<g::argument>(1);
|
||||
if (!args.empty()) {
|
||||
const wcstring argument = get_source(args.at(0));
|
||||
|
||||
// Looks like a command.
|
||||
this->report_error(statement, ERROR_BAD_EQUALS_IN_COMMAND5, argument.c_str(),
|
||||
name_str.c_str(), val_str.c_str(), argument.c_str(),
|
||||
get_ellipsis_str());
|
||||
} else {
|
||||
wcstring assigned_val = reconstruct_orig_str(val_str);
|
||||
this->report_error(statement, ERROR_BAD_COMMAND_ASSIGN_ERR_MSG, name_str.c_str(),
|
||||
assigned_val.c_str());
|
||||
}
|
||||
} else if (err_code != ENOENT) {
|
||||
if (err_code != ENOENT) {
|
||||
this->report_error(statement, _(L"The file '%ls' is not executable by this user"), cmd);
|
||||
} else {
|
||||
// Handle unrecognized commands with standard command not found handler that can make better
|
||||
|
@ -1049,8 +1029,9 @@ parse_execution_result_t parse_execution_context_t::populate_not_process(
|
|||
job_t *job, process_t *proc, tnode_t<g::not_statement> not_statement) {
|
||||
auto &flags = job->mut_flags();
|
||||
flags.negate = !flags.negate;
|
||||
return this->populate_job_process(job, proc,
|
||||
not_statement.require_get_child<g::statement, 1>());
|
||||
return this->populate_job_process(
|
||||
job, proc, not_statement.require_get_child<g::statement, 2>(),
|
||||
not_statement.require_get_child<g::variable_assignments, 1>());
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
|
@ -1080,12 +1061,65 @@ parse_execution_result_t parse_execution_context_t::populate_block_process(
|
|||
return parse_execution_success;
|
||||
}
|
||||
|
||||
parse_execution_result_t parse_execution_context_t::apply_variable_assignments(
|
||||
process_t *proc, tnode_t<grammar::variable_assignments> variable_assignments,
|
||||
const block_t **block) {
|
||||
variable_assignment_node_list_t assignment_list =
|
||||
get_variable_assignment_nodes(variable_assignments);
|
||||
if (assignment_list.empty()) return parse_execution_success;
|
||||
*block = parser->push_block(block_t::variable_assignment_block());
|
||||
for (const auto &variable_assignment : assignment_list) {
|
||||
const wcstring &source = variable_assignment.get_source(pstree->src);
|
||||
auto equals_pos = variable_assignment_equals_pos(source);
|
||||
assert(equals_pos);
|
||||
const wcstring &variable_name = source.substr(0, *equals_pos);
|
||||
const wcstring expression = source.substr(*equals_pos + 1);
|
||||
std::vector<completion_t> expression_expanded;
|
||||
parse_error_list_t errors;
|
||||
// TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function
|
||||
auto expand_ret =
|
||||
expand_string(expression, &expression_expanded, expand_flag::no_descriptions,
|
||||
parser->vars(), parser->shared(), &errors);
|
||||
parse_error_offset_source_start(
|
||||
&errors, variable_assignment.source_range()->start + *equals_pos + 1);
|
||||
switch (expand_ret) {
|
||||
case expand_result_t::error: {
|
||||
this->report_errors(errors);
|
||||
return parse_execution_errored;
|
||||
}
|
||||
case expand_result_t::wildcard_no_match: // nullglob (equivalent to set)
|
||||
case expand_result_t::wildcard_match:
|
||||
case expand_result_t::ok: {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("unexpected expand_string() return value");
|
||||
break;
|
||||
}
|
||||
}
|
||||
wcstring_list_t vals;
|
||||
for (auto &completion : expression_expanded) {
|
||||
vals.emplace_back(std::move(completion.completion));
|
||||
}
|
||||
if (proc) proc->variable_assignments.push_back({variable_name, vals});
|
||||
parser->vars().set(std::move(variable_name), ENV_LOCAL | ENV_EXPORT, std::move(vals));
|
||||
}
|
||||
return parse_execution_success;
|
||||
}
|
||||
|
||||
parse_execution_result_t parse_execution_context_t::populate_job_process(
|
||||
job_t *job, process_t *proc, tnode_t<grammar::statement> statement) {
|
||||
job_t *job, process_t *proc, tnode_t<grammar::statement> statement,
|
||||
tnode_t<grammar::variable_assignments> variable_assignments) {
|
||||
// Get the "specific statement" which is boolean / block / if / switch / decorated.
|
||||
const parse_node_t &specific_statement = statement.get_child_node<0>();
|
||||
|
||||
parse_execution_result_t result = parse_execution_success;
|
||||
const block_t *block = nullptr;
|
||||
parse_execution_result_t result =
|
||||
this->apply_variable_assignments(proc, variable_assignments, &block);
|
||||
cleanup_t scope([&]() {
|
||||
if (block) parser->pop_block(block);
|
||||
});
|
||||
if (result != parse_execution_success) return parse_execution_errored;
|
||||
|
||||
switch (specific_statement.type) {
|
||||
case symbol_not_statement: {
|
||||
|
@ -1131,25 +1165,25 @@ parse_execution_result_t parse_execution_context_t::populate_job_from_job_node(
|
|||
|
||||
// We are going to construct process_t structures for every statement in the job. Get the first
|
||||
// statement.
|
||||
tnode_t<g::statement> statement = job_node.child<0>();
|
||||
assert(statement);
|
||||
|
||||
parse_execution_result_t result = parse_execution_success;
|
||||
tnode_t<g::statement> statement = job_node.child<1>();
|
||||
tnode_t<g::variable_assignments> variable_assignments = job_node.child<0>();
|
||||
|
||||
// Create processes. Each one may fail.
|
||||
process_list_t processes;
|
||||
processes.emplace_back(new process_t());
|
||||
result = this->populate_job_process(j, processes.back().get(), statement);
|
||||
parse_execution_result_t result =
|
||||
this->populate_job_process(j, processes.back().get(), statement, variable_assignments);
|
||||
|
||||
// Construct process_ts for job continuations (pipelines), by walking the list until we hit the
|
||||
// terminal (empty) job continuation.
|
||||
tnode_t<g::job_continuation> job_cont = job_node.child<1>();
|
||||
tnode_t<g::job_continuation> job_cont = job_node.child<2>();
|
||||
assert(job_cont);
|
||||
while (auto pipe = job_cont.try_get_child<g::tok_pipe, 0>()) {
|
||||
if (result != parse_execution_success) {
|
||||
break;
|
||||
}
|
||||
tnode_t<g::statement> statement = job_cont.require_get_child<g::statement, 2>();
|
||||
auto variable_assignments = job_cont.require_get_child<g::variable_assignments, 2>();
|
||||
auto statement = job_cont.require_get_child<g::statement, 3>();
|
||||
|
||||
// Handle the pipe, whose fd may not be the obvious stdout.
|
||||
auto parsed_pipe = pipe_or_redir_t::from_string(get_source(pipe));
|
||||
|
@ -1169,10 +1203,11 @@ parse_execution_result_t parse_execution_context_t::populate_job_from_job_node(
|
|||
|
||||
// Store the new process (and maybe with an error).
|
||||
processes.emplace_back(new process_t());
|
||||
result = this->populate_job_process(j, processes.back().get(), statement);
|
||||
result =
|
||||
this->populate_job_process(j, processes.back().get(), statement, variable_assignments);
|
||||
|
||||
// Get the next continuation.
|
||||
job_cont = job_cont.require_get_child<g::job_continuation, 3>();
|
||||
job_cont = job_cont.require_get_child<g::job_continuation, 4>();
|
||||
assert(job_cont);
|
||||
}
|
||||
|
||||
|
@ -1231,30 +1266,39 @@ parse_execution_result_t parse_execution_context_t::run_1_job(tnode_t<g::job> jo
|
|||
// However, if there are no redirections, then we can just jump into the block directly, which
|
||||
// is significantly faster.
|
||||
if (job_is_simple_block(job_node)) {
|
||||
parse_execution_result_t result = parse_execution_success;
|
||||
tnode_t<g::variable_assignments> variable_assignments = job_node.child<0>();
|
||||
const block_t *block = nullptr;
|
||||
parse_execution_result_t result =
|
||||
this->apply_variable_assignments(nullptr, variable_assignments, &block);
|
||||
cleanup_t scope([&]() {
|
||||
if (block) parser->pop_block(block);
|
||||
});
|
||||
|
||||
tnode_t<g::statement> statement = job_node.child<0>();
|
||||
tnode_t<g::statement> statement = job_node.child<1>();
|
||||
const parse_node_t &specific_statement = statement.get_child_node<0>();
|
||||
assert(specific_statement_type_is_redirectable_block(specific_statement));
|
||||
switch (specific_statement.type) {
|
||||
case symbol_block_statement: {
|
||||
result =
|
||||
this->run_block_statement({&tree(), &specific_statement}, associated_block);
|
||||
break;
|
||||
}
|
||||
case symbol_if_statement: {
|
||||
result = this->run_if_statement({&tree(), &specific_statement}, associated_block);
|
||||
break;
|
||||
}
|
||||
case symbol_switch_statement: {
|
||||
result = this->run_switch_statement({&tree(), &specific_statement});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Other types should be impossible due to the
|
||||
// specific_statement_type_is_redirectable_block check.
|
||||
PARSER_DIE();
|
||||
break;
|
||||
if (result == parse_execution_success) {
|
||||
switch (specific_statement.type) {
|
||||
case symbol_block_statement: {
|
||||
result =
|
||||
this->run_block_statement({&tree(), &specific_statement}, associated_block);
|
||||
break;
|
||||
}
|
||||
case symbol_if_statement: {
|
||||
result =
|
||||
this->run_if_statement({&tree(), &specific_statement}, associated_block);
|
||||
break;
|
||||
}
|
||||
case symbol_switch_statement: {
|
||||
result = this->run_switch_statement({&tree(), &specific_statement});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Other types should be impossible due to the
|
||||
// specific_statement_type_is_redirectable_block check.
|
||||
PARSER_DIE();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,10 +87,14 @@ class parse_execution_context_t {
|
|||
|
||||
enum process_type_t process_type_for_command(tnode_t<grammar::plain_statement> statement,
|
||||
const wcstring &cmd) const;
|
||||
parse_execution_result_t apply_variable_assignments(
|
||||
process_t *proc, tnode_t<grammar::variable_assignments> variable_assignments,
|
||||
const block_t **block);
|
||||
|
||||
// These create process_t structures from statements.
|
||||
parse_execution_result_t populate_job_process(job_t *job, process_t *proc,
|
||||
tnode_t<grammar::statement> statement);
|
||||
parse_execution_result_t populate_job_process(
|
||||
job_t *job, process_t *proc, tnode_t<grammar::statement> statement,
|
||||
tnode_t<grammar::variable_assignments> variable_assignments);
|
||||
parse_execution_result_t populate_not_process(job_t *job, process_t *proc,
|
||||
tnode_t<grammar::not_statement> not_statement);
|
||||
parse_execution_result_t populate_plain_process(job_t *job, process_t *proc,
|
||||
|
|
|
@ -228,15 +228,27 @@ DEF_ALT(job_conjunction_continuation) {
|
|||
// like if statements, where we require a command). To represent "non-empty", we require a
|
||||
// statement, followed by a possibly empty job_continuation, and then optionally a background
|
||||
// specifier '&'
|
||||
DEF(job) produces_sequence<statement, job_continuation, optional_background>{BODY(job)};
|
||||
DEF(job)
|
||||
produces_sequence<variable_assignments, statement, job_continuation, optional_background>{
|
||||
BODY(job)};
|
||||
|
||||
DEF_ALT(job_continuation) {
|
||||
using piped = seq<tok_pipe, optional_newlines, statement, job_continuation>;
|
||||
using piped =
|
||||
seq<tok_pipe, optional_newlines, variable_assignments, statement, job_continuation>;
|
||||
using empty = grammar::empty;
|
||||
ALT_BODY(job_continuation, piped, empty);
|
||||
};
|
||||
|
||||
// A statement is a normal command, or an if / while / not etc.
|
||||
// A list of assignments like HOME=$PWD
|
||||
DEF_ALT(variable_assignments) {
|
||||
using empty = grammar::empty;
|
||||
using var = seq<variable_assignment, variable_assignments>;
|
||||
ALT_BODY(variable_assignments, empty, var);
|
||||
};
|
||||
// A string token like VAR=value
|
||||
DEF(variable_assignment) produces_single<tok_string>{BODY(variable_assignment)};
|
||||
|
||||
// A statement is a normal command, or an if / while / and etc
|
||||
DEF_ALT(statement) {
|
||||
using nots = single<not_statement>;
|
||||
using block = single<block_statement>;
|
||||
|
@ -309,8 +321,8 @@ produces_sequence<keyword<parse_keyword_function>, argument, argument_list, tok_
|
|||
BODY(function_header)};
|
||||
|
||||
DEF_ALT(not_statement) {
|
||||
using nots = seq<keyword<parse_keyword_not>, statement>;
|
||||
using exclams = seq<keyword<parse_keyword_exclam>, statement>;
|
||||
using nots = seq<keyword<parse_keyword_not>, variable_assignments, statement>;
|
||||
using exclams = seq<keyword<parse_keyword_exclam>, variable_assignments, statement>;
|
||||
ALT_BODY(not_statement, nots, exclams);
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ ELEM(function_header)
|
|||
ELEM(not_statement)
|
||||
ELEM(andor_job_list)
|
||||
ELEM(decorated_statement)
|
||||
ELEM(variable_assignment)
|
||||
ELEM(variable_assignments)
|
||||
ELEM(plain_statement)
|
||||
ELEM(argument_list)
|
||||
ELEM(arguments_or_redirections_list)
|
||||
|
|
|
@ -314,6 +314,16 @@ RESOLVE(block_header) {
|
|||
}
|
||||
}
|
||||
|
||||
RESOLVE(variable_assignments) {
|
||||
UNUSED(token2);
|
||||
UNUSED(out_tag);
|
||||
if (token1.may_be_variable_assignment) {
|
||||
assert(token1.type == parse_token_type_string);
|
||||
return production_for<var>();
|
||||
}
|
||||
return production_for<empty>();
|
||||
}
|
||||
|
||||
RESOLVE(decorated_statement) {
|
||||
// and/or are typically parsed in job_conjunction at the beginning of a job
|
||||
// However they may be reached here through e.g. true && and false.
|
||||
|
|
|
@ -1007,6 +1007,26 @@ static inline bool is_help_argument(const wcstring &txt) {
|
|||
return txt == L"-h" || txt == L"--help";
|
||||
}
|
||||
|
||||
// Return the location of the equals sign, or npos if the string does
|
||||
// not look like a variable assignment like FOO=bar. The detection
|
||||
// works similar as in some POSIX shells: only letters and numbers qre
|
||||
// allowed on the left hand side, no quotes or escaping.
|
||||
maybe_t<size_t> variable_assignment_equals_pos(const wcstring &txt) {
|
||||
enum { init, has_some_variable_identifier } state = init;
|
||||
// TODO bracket indexing
|
||||
for (size_t i = 0; i < txt.size(); i++) {
|
||||
wchar_t c = txt[i];
|
||||
if (state == init) {
|
||||
if (!valid_var_name_char(c)) return {};
|
||||
state = has_some_variable_identifier;
|
||||
} else {
|
||||
if (c == '=') return {i};
|
||||
if (!valid_var_name_char(c)) return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Return a new parse token, advancing the tokenizer.
|
||||
static inline parse_token_t next_parse_token(tokenizer_t *tok, maybe_t<tok_t> *out_token,
|
||||
wcstring *storage) {
|
||||
|
@ -1028,6 +1048,7 @@ static inline parse_token_t next_parse_token(tokenizer_t *tok, maybe_t<tok_t> *o
|
|||
result.is_help_argument = result.has_dash_prefix && is_help_argument(text);
|
||||
result.is_newline = (result.type == parse_token_type_end && text == L"\n");
|
||||
result.preceding_escaped_nl = token.preceding_escaped_nl;
|
||||
result.may_be_variable_assignment = bool(variable_assignment_equals_pos(text));
|
||||
|
||||
// These assertions are totally bogus. Basically our tokenizer works in size_t but we work in
|
||||
// uint32_t to save some space. If we have a source file larger than 4 GB, we'll probably just
|
||||
|
|
|
@ -38,6 +38,7 @@ struct parse_token_t {
|
|||
bool is_help_argument{false}; // Hackish: whether the source looks like '-h' or '--help'
|
||||
bool is_newline{false}; // Hackish: if TOK_END, whether the source is a newline.
|
||||
bool preceding_escaped_nl{false}; // Whether there was an escaped newline preceding this token.
|
||||
bool may_be_variable_assignment{false}; // Hackish: whether this token is a string like FOO=bar
|
||||
source_offset_t source_start{SOURCE_OFFSET_INVALID};
|
||||
source_offset_t source_length{0};
|
||||
|
||||
|
@ -234,4 +235,7 @@ using parsed_source_ref_t = std::shared_ptr<const parsed_source_t>;
|
|||
parsed_source_ref_t parse_source(wcstring src, parse_tree_flags_t flags, parse_error_list_t *errors,
|
||||
parse_token_type_t goal = symbol_job_list);
|
||||
|
||||
/// The position of the equal sign in a variable assignment like foo=bar.
|
||||
maybe_t<size_t> variable_assignment_equals_pos(const wcstring &txt);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -41,6 +41,9 @@ class io_chain_t;
|
|||
/// Breakpoint block.
|
||||
#define BREAKPOINT_BLOCK N_(L"block created by breakpoint")
|
||||
|
||||
/// Variable assignment block.
|
||||
#define VARIABLE_ASSIGNMENT_BLOCK N_(L"block created by variable assignment prefixing a command")
|
||||
|
||||
/// If block description.
|
||||
#define IF_BLOCK N_(L"'if' conditional block")
|
||||
|
||||
|
@ -95,6 +98,7 @@ static const struct block_lookup_entry block_lookup[] = {
|
|||
{SOURCE, L"source", SOURCE_BLOCK},
|
||||
{EVENT, 0, EVENT_BLOCK},
|
||||
{BREAKPOINT, L"breakpoint", BREAKPOINT_BLOCK},
|
||||
{VARIABLE_ASSIGNMENT, L"variable assignment", VARIABLE_ASSIGNMENT_BLOCK},
|
||||
{(block_type_t)0, 0, 0}};
|
||||
|
||||
// Given a file path, return something nicer. Currently we just "unexpand" tildes.
|
||||
|
@ -789,6 +793,10 @@ wcstring block_t::description() const {
|
|||
result.append(L"breakpoint");
|
||||
break;
|
||||
}
|
||||
case VARIABLE_ASSIGNMENT: {
|
||||
result.append(L"variable_assignment");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->src_lineno >= 0) {
|
||||
|
@ -831,3 +839,4 @@ block_t block_t::scope_block(block_type_t type) {
|
|||
return block_t(type);
|
||||
}
|
||||
block_t block_t::breakpoint_block() { return block_t(BREAKPOINT); }
|
||||
block_t block_t::variable_assignment_block() { return block_t(VARIABLE_ASSIGNMENT); }
|
||||
|
|
|
@ -43,6 +43,7 @@ enum block_type_t {
|
|||
SOURCE, /// Block created by the . (source) builtin
|
||||
EVENT, /// Block created on event notifier invocation
|
||||
BREAKPOINT, /// Breakpoint block
|
||||
VARIABLE_ASSIGNMENT, /// Variable assignment before a command
|
||||
};
|
||||
|
||||
/// Possible states for a loop.
|
||||
|
@ -98,6 +99,7 @@ class block_t {
|
|||
static block_t switch_block();
|
||||
static block_t scope_block(block_type_t type);
|
||||
static block_t breakpoint_block();
|
||||
static block_t variable_assignment_block();
|
||||
|
||||
~block_t();
|
||||
};
|
||||
|
|
|
@ -192,6 +192,13 @@ class process_t {
|
|||
parsed_source_ref_t block_node_source{};
|
||||
tnode_t<grammar::statement> internal_block_node{};
|
||||
|
||||
struct concrete_assignment {
|
||||
wcstring variable_name;
|
||||
wcstring_list_t values;
|
||||
};
|
||||
/// The expanded variable assignments for this process, as specified by the `a=b cmd` syntax.
|
||||
std::vector<concrete_assignment> variable_assignments;
|
||||
|
||||
/// Sets argv.
|
||||
void set_argv(const wcstring_list_t &argv) { argv_array.set(argv); }
|
||||
|
||||
|
|
|
@ -90,6 +90,11 @@ std::vector<tnode_t<grammar::comment>> parse_node_tree_t::comment_nodes_for_node
|
|||
return result;
|
||||
}
|
||||
|
||||
variable_assignment_node_list_t get_variable_assignment_nodes(
|
||||
tnode_t<grammar::variable_assignments> list, size_t max) {
|
||||
return list.descendants<grammar::variable_assignment>(max);
|
||||
}
|
||||
|
||||
maybe_t<wcstring> command_for_plain_statement(tnode_t<grammar::plain_statement> stmt,
|
||||
const wcstring &src) {
|
||||
tnode_t<grammar::tok_string> cmd = stmt.child<0>();
|
||||
|
@ -109,7 +114,7 @@ arguments_node_list_t get_argument_nodes(tnode_t<grammar::arguments_or_redirecti
|
|||
}
|
||||
|
||||
bool job_node_is_background(tnode_t<grammar::job> job) {
|
||||
tnode_t<grammar::optional_background> bg = job.child<2>();
|
||||
tnode_t<grammar::optional_background> bg = job.child<3>();
|
||||
return bg.tag() == parse_background;
|
||||
}
|
||||
|
||||
|
@ -139,8 +144,8 @@ pipeline_position_t get_pipeline_position(tnode_t<grammar::statement> st) {
|
|||
|
||||
// Check if we're the beginning of a job, and if so, whether that job
|
||||
// has a non-empty continuation.
|
||||
tnode_t<job_continuation> jc = st.try_get_parent<job>().child<1>();
|
||||
if (jc.try_get_child<statement, 2>()) {
|
||||
tnode_t<job_continuation> jc = st.try_get_parent<job>().child<2>();
|
||||
if (jc.try_get_child<statement, 3>()) {
|
||||
return pipeline_position_t::first;
|
||||
}
|
||||
return pipeline_position_t::none;
|
||||
|
|
|
@ -218,6 +218,12 @@ tnode_t<Type> parse_node_tree_t::find_child(const parse_node_t &parent) const {
|
|||
return tnode_t<Type>(this, &this->find_child(parent, Type::token));
|
||||
}
|
||||
|
||||
/// Return the arguments under an arguments_list or arguments_or_redirection_list
|
||||
/// Do not return more than max.
|
||||
using variable_assignment_node_list_t = std::vector<tnode_t<grammar::variable_assignment>>;
|
||||
variable_assignment_node_list_t get_variable_assignment_nodes(
|
||||
tnode_t<grammar::variable_assignments>, size_t max = -1);
|
||||
|
||||
/// Given a plain statement, get the command from the child node. Returns the command string on
|
||||
/// success, none on failure.
|
||||
maybe_t<wcstring> command_for_plain_statement(tnode_t<grammar::plain_statement> stmt,
|
||||
|
|
78
tests/checks/variable-assignment.fish
Normal file
78
tests/checks/variable-assignment.fish
Normal file
|
@ -0,0 +1,78 @@
|
|||
# RUN: %fish %s
|
||||
|
||||
# erase all lowercase variables to make sure they don't break our tests
|
||||
for varname in (set -xn | string match -r '^[a-z].*')
|
||||
while set -q $varname
|
||||
set -e $varname
|
||||
end
|
||||
end
|
||||
|
||||
# CHECK: bar
|
||||
foo=bar echo $foo
|
||||
|
||||
# CHECK: nil
|
||||
set -q foo; or echo nil
|
||||
|
||||
# CHECK: lx
|
||||
foo=bar set -qlx foo; and echo lx
|
||||
|
||||
# CHECK: 3
|
||||
a={1, 2, 3} count $a
|
||||
|
||||
# CHECK: 1+2+3
|
||||
a={1, 2, 3} string join + $a
|
||||
|
||||
# CHECK: 1 2 3
|
||||
a=(echo 1 2 3) echo $a
|
||||
|
||||
# CHECK: a a2
|
||||
a=a b={$a}2 echo $a $b
|
||||
|
||||
# CHECK: a
|
||||
a=a builtin echo $a
|
||||
|
||||
# CHECK: 0
|
||||
a=failing-glob-* count $a
|
||||
|
||||
# CHECK: ''
|
||||
a=b true | echo "'$a'"
|
||||
|
||||
if a=b true
|
||||
# CHECK: ''
|
||||
echo "'$a'"
|
||||
end
|
||||
|
||||
# CHECK: b
|
||||
not a=b echo $a
|
||||
|
||||
# CHECK: b
|
||||
a=b not echo $a
|
||||
|
||||
# CHECK: b
|
||||
a=b not builtin echo $a
|
||||
|
||||
# CHECK: /usr/bin:/bin
|
||||
xPATH={/usr,}/bin sh -c 'echo $xPATH'
|
||||
|
||||
# CHECK: 2
|
||||
yPATH=/usr/bin:/bin count $yPATH
|
||||
|
||||
# CHECK: b
|
||||
a=b begin; true | echo $a; end
|
||||
|
||||
# CHECK: b
|
||||
a=b if true; echo $a; end
|
||||
|
||||
# CHECK: b
|
||||
a=b switch x; case x; echo $a; end
|
||||
|
||||
complete -c x --erase
|
||||
complete -c x -xa arg
|
||||
complete -C' a=b x ' # Mind the leading space.
|
||||
# CHECK: arg
|
||||
alias xalias=x
|
||||
complete -C'a=b xalias '
|
||||
# CHECK: arg
|
||||
alias envxalias='a=b x'
|
||||
complete -C'a=b envxalias '
|
||||
# CHECK: arg
|
Loading…
Reference in New Issue
Block a user