Rework variable assignments during tab completion

Prior to this change, tab completing with a variable assignment like
`VAR=val cmd<tab>` would parse out and apply VAR=val, then recursively
invoke completions. This caused some awkwardness around the wrap chain -
if a wrapped command had a variable completion we risked infinite
recursion. A secondary problem is that we would run any command
substitutions inside variable assignment, which the user does not expect
to run until pressing enter.

With this change, we explicitly track variable assignments encountered
during tab completion, including both those explicitly given on the
command line and those found during wrap chain walk. We then apply them
while suppressing command substitutions.
This commit is contained in:
ridiculousfish 2020-09-26 18:39:29 -07:00
parent cc07716dc1
commit e88eb508d0
2 changed files with 104 additions and 45 deletions

View file

@ -411,6 +411,8 @@ class completer_t {
void walk_wrap_chain(const wcstring &cmd, const wcstring &cmdline, source_range_t cmd_range,
custom_arg_data_t *arg_data);
const block_t *apply_var_assignments(const custom_arg_data_t *ad);
bool empty() const { return completions.empty(); }
void escape_opening_brackets(const wcstring &argument);
@ -1350,9 +1352,49 @@ bool completer_t::try_complete_user(const wcstring &str) {
#endif
}
// If we have variable assignments, attempt to apply them in our parser, returning a variable
// assignment block. The caller MUST clean this up by calling ctx.parser->pop_block(). If we do not
// have variable assignments, then return nullptr.
const block_t *completer_t::apply_var_assignments(const custom_arg_data_t *ad) {
if (!ctx.parser || ad->var_assignments.empty()) return nullptr;
env_stack_t &vars = ctx.parser->vars();
assert(&vars == &ctx.vars &&
"Don't know how to tab complete with a parser but a different variable set");
// clone of parse_execution_context_t::apply_variable_assignments.
// Crucially do NOT expand subcommands:
// VAR=(launch_missiles) cmd<tab>
// should not launch missiles.
// Note we also do NOT send --on-variable events.
const expand_flags_t expand_flags{expand_flag::no_descriptions, expand_flag::skip_cmdsubst};
const block_t *block = ctx.parser->push_block(block_t::variable_assignment_block());
for (const wcstring &var_assign : ad->var_assignments) {
maybe_t<size_t> equals_pos = variable_assignment_equals_pos(var_assign);
assert(equals_pos && "All variable assignments should have equals position");
const wcstring variable_name = var_assign.substr(0, *equals_pos);
const wcstring expression = var_assign.substr(*equals_pos + 1);
completion_list_t expression_expanded;
auto expand_ret = expand_string(expression, &expression_expanded, expand_flags, ctx);
// If expansion succeeds, set the value; if it fails (e.g. it has a cmdsub) set an empty
// value anyways.
wcstring_list_t vals;
if (expand_ret == expand_result_t::ok) {
for (auto &completion : expression_expanded) {
vals.emplace_back(std::move(completion.completion));
}
}
ctx.parser->vars().set(variable_name, ENV_LOCAL | ENV_EXPORT, std::move(vals));
if (ctx.check_cancel()) break;
}
return block;
}
// Complete a command by invoking user-specified completions.
void completer_t::complete_custom(const wcstring &cmd, const wcstring &cmdline,
custom_arg_data_t *ad) {
if (ctx.check_cancel()) return;
bool is_autosuggest = this->type() == COMPLETE_AUTOSUGGEST;
// Perhaps set a transient commandline so that custom completions
// buitin_commandline will refer to the wrapped command. But not if
@ -1361,53 +1403,18 @@ void completer_t::complete_custom(const wcstring &cmd, const wcstring &cmdline,
bool wants_transient = ad->wrap_depth > 0 && !is_autosuggest;
if (wants_transient) {
ctx.parser->libdata().transient_commandlines.push_back(cmdline);
remove_transient.emplace([&] { ctx.parser->libdata().transient_commandlines.pop_back(); });
remove_transient.emplace([=] { ctx.parser->libdata().transient_commandlines.pop_back(); });
}
maybe_t<size_t> equals_pos = variable_assignment_equals_pos(cmd);
bool is_variable_assignment = bool(equals_pos);
if (is_variable_assignment && !is_autosuggest) {
assert(ctx.parser);
// clone of parse_execution_context_t::apply_variable_assignments
// but this is not smart enough to report correct error locations, so we ignore
// errors and this create one scope for each assignment instead of just one;
// that should hardly matter
const block_t *block = ctx.parser->push_block(block_t::variable_assignment_block());
const wcstring variable_name = cmd.substr(0, *equals_pos);
const wcstring expression = cmd.substr(*equals_pos + 1);
completion_list_t expression_expanded;
auto expand_ret =
expand_string(expression, &expression_expanded, expand_flag::no_descriptions, ctx);
wcstring_list_t vals;
if (expand_ret == expand_result_t::ok) {
for (auto &completion : expression_expanded)
vals.emplace_back(std::move(completion.completion));
ctx.parser->vars().set(variable_name, ENV_LOCAL | ENV_EXPORT, std::move(vals));
}
cleanup_t scope([&] {
if (block) ctx.parser->pop_block(block);
});
// 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 (ctx.parser->libdata().transient_commandlines.empty()) {
unaliased_cmd = cmdline;
} else {
unaliased_cmd = ctx.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"");
ctx.parser->libdata().transient_commandlines.push_back(unaliased_cmd);
cleanup_t remove_transient(
[&] { ctx.parser->libdata().transient_commandlines.pop_back(); });
perform_for_commandline(std::move(unaliased_cmd));
ad->do_file = false;
} else if (!complete_param(
cmd, ad->previous_argument, ad->current_argument,
!ad->had_ddash)) { // Invoke any custom completions for this command.
// Maybe apply variable assignments.
const block_t *var_assignment_block = apply_var_assignments(ad);
cleanup_t restore_variable_scope([=] {
if (var_assignment_block) ctx.parser->pop_block(var_assignment_block);
});
if (ctx.check_cancel()) return;
if (!complete_param(cmd, ad->previous_argument, ad->current_argument,
!ad->had_ddash)) { // Invoke any custom completions for this command.
ad->do_file = false;
}
}

View file

@ -42,3 +42,55 @@ complete -C 'recvar '
# CHECK: recvar_comp
# We get the same completion twice. TODO: fix this.
# CHECK: recvar_comp
# Test that completions do not perform subcommands.
# That is, `FOO=(launch_missiles) command<tab>` does not launch any missiles.
set -g missile_count 0
function launch_missiles
set -g missile_count (math "$missile_count + 1")
end
# Ensure missile launching work.
launch_missiles
echo $missile_count
# CHECK: 1
set -g GOOD global_good
set -g BAD global_bad
function do_print_good_bad
echo "GOOD:$GOOD"
echo "BAD:$BAD"
end
complete -c print_good_bad -x -a '(do_print_good_bad)'
complete -C 'print_good_bad '
# CHECK: GOOD:global_good
# CHECK: BAD:global_bad
# Key check is completions should expand GOOD but not BAD,
# because GOOD is just a string but BAD contains a cmdsub
# which may do arbitrary things.
complete -C 'GOOD=local_good BAD=(launch_missiles) print_good_bad '
# CHECK: GOOD:local_good
# CHECK: BAD:
# Completion should not have launched any missiles.
echo $missile_count
# CHECK: 1
# Torture test with a bunch of variable assignments.
# Note this tests the existing behavior when there's conflicts
# (innermost wrapped command wins) but it's not clear that this is desirable.
function show_vars
echo "AAA:$AAA"
echo "BBB:$BBB"
echo "CCC:$CCC"
end
complete -c show_vars_cmd0 -x -a '(show_vars)'
complete -c show_vars_cmd1 -x --wraps 'AAA=aaa show_vars_cmd0'
complete -c show_vars_cmd2 -x --wraps 'AAA=nope BBB=bbb show_vars_cmd1'
complete -c show_vars_cmd3 -x --wraps 'BBB=nope show_vars_cmd2'
complete -C 'CCC=ccc show_vars_cmd3 '
# CHECK: AAA:aaa
# CHECK: BBB:bbb
# CHECK: CCC:ccc