diff --git a/src/complete.cpp b/src/complete.cpp index b334da2fa..ce95bb211 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -1307,16 +1308,16 @@ bool completer_t::try_complete_user(const wcstring &str) { #endif } -// The callback type for walk_wrap_chain +// The callback type for walk_wrap_chain. using wrap_chain_visitor_t = std::function; -// Helper to complete a parameter for a command and its transitive wrap chain. -// Given a command line \p command_line and the range of the command itself within the command line -// as \p command_range, invoke the \p receiver with the command and the command line. Then, for each -// target wrapped by the given command, update the command line with that target and invoke this -// recursively. -static void walk_wrap_chain(const wcstring &command_line, source_range_t command_range, - const wrap_chain_visitor_t &visitor, size_t depth = 0) { +// A set tracking which (command, wrap) pairs we have seen. +using wrap_chain_visited_set_t = std::set>; + +// Recursive implementation of walk_wrap_chain(). +static void walk_wrap_chain_recursive(const wcstring &command_line, source_range_t command_range, + const wrap_chain_visitor_t &visitor, + wrap_chain_visited_set_t *visited, size_t depth) { // Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing. if (depth > 24) return; if (reader_test_should_cancel()) return; @@ -1339,14 +1340,30 @@ static void walk_wrap_chain(const wcstring &command_line, source_range_t command if (!wrapped_command.empty()) { size_t where = faux_commandline.find(wrapped_command, command_range.start); if (where != wcstring::npos) { - // Recurse with our new command and command line. - source_range_t faux_source_range{uint32_t(where), uint32_t(wrapped_command.size())}; - walk_wrap_chain(faux_commandline, faux_source_range, visitor, depth + 1); + // Do not recurse if we have already seen this. + if (visited->insert({command, wrapped_command}).second) { + // Recurse with our new command and command line. + source_range_t faux_source_range{uint32_t(where), + uint32_t(wrapped_command.size())}; + walk_wrap_chain_recursive(faux_commandline, faux_source_range, visitor, visited, + depth + 1); + } } } } } +// Helper to complete a parameter for a command and its transitive wrap chain. +// Given a command line \p command_line and the range of the command itself within the command line +// as \p command_range, invoke the \p receiver with the command and the command line. Then, for each +// target wrapped by the given command, update the command line with that target and invoke this +// recursively. +static void walk_wrap_chain(const wcstring &command_line, source_range_t command_range, + const wrap_chain_visitor_t &visitor) { + wrap_chain_visited_set_t visited; + walk_wrap_chain_recursive(command_line, command_range, visitor, &visited, 0); +} + /// If the argument contains a '[' typed by the user, completion by appending to the argument might /// produce an invalid token (#5831). /// diff --git a/tests/checks/wraps.fish b/tests/checks/wraps.fish new file mode 100644 index 000000000..5ffc9edb5 --- /dev/null +++ b/tests/checks/wraps.fish @@ -0,0 +1,29 @@ +#RUN: %fish %s +# Validate some things about command wrapping. + +# This tests that we do not trigger a combinatorial explosion - see #5638. +# Ensure it completes successully. +complete -c testcommand --wraps "testcommand x " +complete -c testcommand --wraps "testcommand y " +complete -c testcommand --no-files -a normal +complete -C'testcommand ' +# CHECK: normal + +# We get the same completion twice. TODO: fix this. +# CHECK: normal + + +# This tests that a call to complete from within a completion doesn't trigger +# wrap chain explosion - #5638 again. +function testcommand2_complete + set -l tokens (commandline -opc) (commandline -ct) + set -e tokens[1] + echo $tokens 1>&2 + complete -C"$tokens" +end + +complete -c testcommand2 -x -a "(testcommand2_complete)" +complete -c testcommand2 --wraps "testcommand2 from_wraps " +complete -C'testcommand2 explicit ' +# CHECKERR: explicit +# CHECKERR: from_wraps explicit