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:
Johannes Altmanninger 2019-10-23 03:13:29 +02:00
parent 3b0f642de9
commit 7d5b44e828
21 changed files with 335 additions and 93 deletions

View File

@ -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).

View File

@ -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
-----

View File

@ -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?
---------------------------------------------

View File

@ -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) {

View File

@ -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) {

View File

@ -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},

View File

@ -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.

View File

@ -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'.")

View File

@ -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;
}
}
}

View File

@ -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,

View File

@ -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);
};

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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); }

View File

@ -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();
};

View File

@ -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); }

View File

@ -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;

View File

@ -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,

View 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