diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ab96b43..baa41e918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/sphinx_doc_src/cmds/set.rst b/sphinx_doc_src/cmds/set.rst index f215ba0c9..2988b5145 100644 --- a/sphinx_doc_src/cmds/set.rst +++ b/sphinx_doc_src/cmds/set.rst @@ -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 ----- diff --git a/sphinx_doc_src/faq.rst b/sphinx_doc_src/faq.rst index eafb53c44..ea601c131 100644 --- a/sphinx_doc_src/faq.rst +++ b/sphinx_doc_src/faq.rst @@ -8,6 +8,10 @@ Use the :ref:`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 ` and :ref:`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? --------------------------------------------- diff --git a/src/complete.cpp b/src/complete.cpp index bda719620..0d765e2f3 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -1511,6 +1511,11 @@ void completer_t::perform() { } if (cmd_tok.location_in_or_at_end_of_source_range(cursor_pos)) { + maybe_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 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 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) { diff --git a/src/exec.cpp b/src/exec.cpp index a6596b6f8..fcc194341 100644 --- a/src/exec.cpp +++ b/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) { diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index f3728e9b0..d10aef7cb 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -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}, diff --git a/src/highlight.cpp b/src/highlight.cpp index 529745ea9..0972072ad 100644 --- a/src/highlight.cpp +++ b/src/highlight.cpp @@ -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 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. diff --git a/src/parse_constants.h b/src/parse_constants.h index af171a1ba..902ba92a0 100644 --- a/src/parse_constants.h +++ b/src/parse_constants.h @@ -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'.") diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index 0bdd90133..5fa50b16d 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -123,8 +123,8 @@ tnode_t 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 statement = first_job.child<0>(); - tnode_t continuation = first_job.child<1>(); + tnode_t statement = first_job.child<1>(); + tnode_t continuation = first_job.child<2>(); const null_environment_t nullenv{}; while (statement) { tnode_t 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 job_node) const { - tnode_t statement = job_node.child<0>(); + tnode_t statement = job_node.child<1>(); // Must be no pipes. - if (job_node.child<1>().try_get_child()) { + if (job_node.child<2>().try_get_child()) { 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(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 not_statement) { auto &flags = job->mut_flags(); flags.negate = !flags.negate; - return this->populate_job_process(job, proc, - not_statement.require_get_child()); + return this->populate_job_process( + job, proc, not_statement.require_get_child(), + not_statement.require_get_child()); } template @@ -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 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 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 statement) { + job_t *job, process_t *proc, tnode_t statement, + tnode_t 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 statement = job_node.child<0>(); - assert(statement); - - parse_execution_result_t result = parse_execution_success; + tnode_t statement = job_node.child<1>(); + tnode_t 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 job_cont = job_node.child<1>(); + tnode_t job_cont = job_node.child<2>(); assert(job_cont); while (auto pipe = job_cont.try_get_child()) { if (result != parse_execution_success) { break; } - tnode_t statement = job_cont.require_get_child(); + auto variable_assignments = job_cont.require_get_child(); + auto statement = job_cont.require_get_child(); // 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(); + job_cont = job_cont.require_get_child(); assert(job_cont); } @@ -1231,30 +1266,39 @@ parse_execution_result_t parse_execution_context_t::run_1_job(tnode_t 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 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 statement = job_node.child<0>(); + tnode_t 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; + } } } diff --git a/src/parse_execution.h b/src/parse_execution.h index 4d4b8f725..c5abcab85 100644 --- a/src/parse_execution.h +++ b/src/parse_execution.h @@ -87,10 +87,14 @@ class parse_execution_context_t { enum process_type_t process_type_for_command(tnode_t statement, const wcstring &cmd) const; + parse_execution_result_t apply_variable_assignments( + process_t *proc, tnode_t 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 statement); + parse_execution_result_t populate_job_process( + job_t *job, process_t *proc, tnode_t statement, + tnode_t variable_assignments); parse_execution_result_t populate_not_process(job_t *job, process_t *proc, tnode_t not_statement); parse_execution_result_t populate_plain_process(job_t *job, process_t *proc, diff --git a/src/parse_grammar.h b/src/parse_grammar.h index 0a6e5bcc4..1e0a5e5a2 100644 --- a/src/parse_grammar.h +++ b/src/parse_grammar.h @@ -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{BODY(job)}; +DEF(job) +produces_sequence{ + BODY(job)}; DEF_ALT(job_continuation) { - using piped = seq; + using piped = + seq; 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; + ALT_BODY(variable_assignments, empty, var); +}; +// A string token like VAR=value +DEF(variable_assignment) produces_single{BODY(variable_assignment)}; + +// A statement is a normal command, or an if / while / and etc DEF_ALT(statement) { using nots = single; using block = single; @@ -309,8 +321,8 @@ produces_sequence, argument, argument_list, tok_ BODY(function_header)}; DEF_ALT(not_statement) { - using nots = seq, statement>; - using exclams = seq, statement>; + using nots = seq, variable_assignments, statement>; + using exclams = seq, variable_assignments, statement>; ALT_BODY(not_statement, nots, exclams); }; diff --git a/src/parse_grammar_elements.inc b/src/parse_grammar_elements.inc index 79339b6cb..4c88d7f82 100644 --- a/src/parse_grammar_elements.inc +++ b/src/parse_grammar_elements.inc @@ -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) diff --git a/src/parse_productions.cpp b/src/parse_productions.cpp index 05fc80cee..99cf0ffba 100644 --- a/src/parse_productions.cpp +++ b/src/parse_productions.cpp @@ -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(); + } + return production_for(); +} + 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. diff --git a/src/parse_tree.cpp b/src/parse_tree.cpp index 93fc83dab..56c38b707 100644 --- a/src/parse_tree.cpp +++ b/src/parse_tree.cpp @@ -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 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 *out_token, wcstring *storage) { @@ -1028,6 +1048,7 @@ static inline parse_token_t next_parse_token(tokenizer_t *tok, maybe_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 diff --git a/src/parse_tree.h b/src/parse_tree.h index 1eb654b72..208f9ad11 100644 --- a/src/parse_tree.h +++ b/src/parse_tree.h @@ -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; 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 variable_assignment_equals_pos(const wcstring &txt); + #endif diff --git a/src/parser.cpp b/src/parser.cpp index 2357f3794..e8f3d6a13 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -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); } diff --git a/src/parser.h b/src/parser.h index 923851b81..5b826e3ae 100644 --- a/src/parser.h +++ b/src/parser.h @@ -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(); }; diff --git a/src/proc.h b/src/proc.h index 0678a899d..ad498b9f1 100644 --- a/src/proc.h +++ b/src/proc.h @@ -192,6 +192,13 @@ class process_t { parsed_source_ref_t block_node_source{}; tnode_t 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 variable_assignments; + /// Sets argv. void set_argv(const wcstring_list_t &argv) { argv_array.set(argv); } diff --git a/src/tnode.cpp b/src/tnode.cpp index cc9f84743..b29e82d28 100644 --- a/src/tnode.cpp +++ b/src/tnode.cpp @@ -90,6 +90,11 @@ std::vector> parse_node_tree_t::comment_nodes_for_node return result; } +variable_assignment_node_list_t get_variable_assignment_nodes( + tnode_t list, size_t max) { + return list.descendants(max); +} + maybe_t command_for_plain_statement(tnode_t stmt, const wcstring &src) { tnode_t cmd = stmt.child<0>(); @@ -109,7 +114,7 @@ arguments_node_list_t get_argument_nodes(tnode_t job) { - tnode_t bg = job.child<2>(); + tnode_t bg = job.child<3>(); return bg.tag() == parse_background; } @@ -139,8 +144,8 @@ pipeline_position_t get_pipeline_position(tnode_t st) { // Check if we're the beginning of a job, and if so, whether that job // has a non-empty continuation. - tnode_t jc = st.try_get_parent().child<1>(); - if (jc.try_get_child()) { + tnode_t jc = st.try_get_parent().child<2>(); + if (jc.try_get_child()) { return pipeline_position_t::first; } return pipeline_position_t::none; diff --git a/src/tnode.h b/src/tnode.h index 491541336..b13958b2f 100644 --- a/src/tnode.h +++ b/src/tnode.h @@ -218,6 +218,12 @@ tnode_t parse_node_tree_t::find_child(const parse_node_t &parent) const { return tnode_t(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>; +variable_assignment_node_list_t get_variable_assignment_nodes( + tnode_t, 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 command_for_plain_statement(tnode_t stmt, diff --git a/tests/checks/variable-assignment.fish b/tests/checks/variable-assignment.fish new file mode 100644 index 000000000..1d3b60248 --- /dev/null +++ b/tests/checks/variable-assignment.fish @@ -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