From af4e2c8caf511517ae76ba971682ef57bb595458 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Wed, 10 Jul 2024 12:39:14 +0200 Subject: [PATCH 01/44] [issue730] added argcomplete for driver arguments if installed --- driver/arguments.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/driver/arguments.py b/driver/arguments.py index 9800d1e795..0b6af9d1f7 100644 --- a/driver/arguments.py +++ b/driver/arguments.py @@ -392,7 +392,7 @@ def parse_args(): driver_other = parser.add_argument_group( title="other driver options") driver_other.add_argument( - "--alias", + "--alias", choices=aliases.ALIASES, help="run a config with an alias (e.g. seq-sat-lama-2011)") driver_other.add_argument( "--build", @@ -452,6 +452,12 @@ def parse_args(): # can be used as an explicit separator. For example, "./fast-downward.py -- # --help" passes "--help" to the search code. + try: + import argcomplete + argcomplete.autocomplete(parser) + except ImportError: + pass + args = parser.parse_args() if args.sas_file: From 2a34d67208d510c314685f01adacd4253f81ca28 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Fri, 12 Jul 2024 12:44:55 +0200 Subject: [PATCH 02/44] [issue730] started with logic for tab completion --- driver/arguments.py | 14 ++-- driver/tab_completion.py | 141 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 driver/tab_completion.py diff --git a/driver/arguments.py b/driver/arguments.py index 0b6af9d1f7..19b28362e9 100644 --- a/driver/arguments.py +++ b/driver/arguments.py @@ -5,6 +5,7 @@ from . import aliases from . import returncodes +from . import tab_completion from . import util @@ -394,7 +395,7 @@ def parse_args(): driver_other.add_argument( "--alias", choices=aliases.ALIASES, help="run a config with an alias (e.g. seq-sat-lama-2011)") - driver_other.add_argument( + build_arg = driver_other.add_argument( "--build", help="BUILD can be a predefined build name like release " "(default) and debug, a custom build name, or the path to " @@ -403,6 +404,7 @@ def parse_args(): "this path does not exist, it tries the directory " "'/builds/BUILD/bin', where the build script creates " "them by default.") + tab_completion.add_build_arg_completer(build_arg) driver_other.add_argument( "--debug", action="store_true", help="alias for --build=debug --validate") @@ -441,9 +443,10 @@ def parse_args(): "--cleanup", action="store_true", help="clean up temporary files (translator output and plan files) and exit") - parser.add_argument( + planner_args = parser.add_argument( "planner_args", nargs=argparse.REMAINDER, help="file names and options passed on to planner components") + tab_completion.add_planner_args_completer(planner_args) # Using argparse.REMAINDER relies on the fact that the first # argument that doesn't belong to the driver doesn't look like an @@ -452,12 +455,7 @@ def parse_args(): # can be used as an explicit separator. For example, "./fast-downward.py -- # --help" passes "--help" to the search code. - try: - import argcomplete - argcomplete.autocomplete(parser) - except ImportError: - pass - + tab_completion.enable(parser) args = parser.parse_args() if args.sas_file: diff --git a/driver/tab_completion.py b/driver/tab_completion.py new file mode 100644 index 0000000000..4101edc0ae --- /dev/null +++ b/driver/tab_completion.py @@ -0,0 +1,141 @@ +from pathlib import Path + +from . import util + +try: + import argcomplete + HAS_ARGCOMPLETE = True +except ImportError: + HAS_ARGCOMPLETE = False + + +def _rindex(seq, element): + """Like list.index, but gives the index of the *last* occurrence.""" + seq = list(reversed(seq)) + reversed_index = seq.index(element) + return len(seq) - 1 - reversed_index + + +def _split_off_filenames(planner_args): + """Given the list of arguments to be passed on to the planner + components, split it into a prefix of filenames and a suffix of + options. Returns a pair (filenames, options). + + If a "--" separator is present, the last such separator serves as + the border between filenames and options. The separator itself is + not returned. (This implies that "--" can be a filename, but never + an option to a planner component.) + + If no such separator is present, the first argument that begins + with "-" and consists of at least two characters starts the list + of options, and all previous arguments are filenames.""" + + double_dash_in_options = False + if "--" in planner_args: + separator_pos = _rindex(planner_args, "--") + num_filenames = separator_pos + del planner_args[separator_pos] + double_dash_in_options = True + else: + num_filenames = 0 + for arg in planner_args: + # We treat "-" by itself as a filename because by common + # convention it denotes stdin or stdout, and we might want + # to support this later. + if arg.startswith("-") and arg != "-": + break + num_filenames += 1 + return planner_args[:num_filenames], planner_args[num_filenames:], double_dash_in_options + + +def _split_planner_args(parser, args): + """Partition args.planner_args, the list of arguments for the + planner components, into args.filenames, args.translate_options + and args.search_options. Modifies args directly and removes the original + args.planner_args list.""" + + args.filenames, options = _split_off_filenames(args.planner_args) + + args.translate_options = [] + args.search_options = [] + + curr_options = args.search_options + for option in options: + if option == "--translate-options": + curr_options = args.translate_options + elif option == "--search-options": + curr_options = args.search_options + else: + curr_options.append(option) + + +def _build_arg_completion(prefix, parsed_args, **kwargs): + if parsed_args.debug: + argcomplete.warn("The option --debug is an alias for --build=debug " + "--validate. Do no specify both --debug and --build.") + exit(1) + + builds_folder = Path(util.REPO_ROOT_DIR) / "builds" + if not builds_folder.exists(): + argcomplete.warn("No build exists.") + exit(1) + return [p.name for p in builds_folder.iterdir() if p.is_dir()] + + +def add_build_arg_completer(build_arg): + build_arg.completer = _build_arg_completion + + +def _planner_args_completion(prefix, parsed_args, **kwargs): + if HAS_ARGCOMPLETE: + if parsed_args.build and parsed_args.debug: + argcomplete.warn("The option --debug is an alias for --build=debug " + "--validate. Do no specify both --debug and --build.") + exit(1) + + build = parsed_args.build + if not build: + if parsed_args.debug: + build = "debug" + else: + build = "release" + + filenames, options, double_dash_in_options = _split_off_filenames(parsed_args.planner_args) + + completions = [] + + if not options and not (len(filenames) == 2 and double_dash_in_options): + completions.append("--") + + if filenames: + translate_options = [] + search_options = [] + + curr_options = search_options + for option in options: + if option == "--translate-options": + curr_options = translate_options + elif option == "--search-options": + curr_options = search_options + else: + curr_options.append(option) + + if curr_options == search_options: + completions.append("--great-search-options") + else: + completions.append("--great-translate-options") + + if len(filenames) < 2 and not double_dash_in_options and not curr_options: + file_completer = argcomplete.FilesCompleter() + completions += file_completer(prefix, **kwargs) + + return completions + + +def add_planner_args_completer(planner_args): + planner_args.completer = _planner_args_completion + + +def enable(parser): + if HAS_ARGCOMPLETE: + argcomplete.autocomplete(parser) From 5be87799fdf6f673bd682426403a82e3edc3fc36 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Mon, 15 Jul 2024 15:13:48 +0200 Subject: [PATCH 03/44] start with completion in search component --- src/search/command_line.cc | 38 ++++++++++++++++++++++++++++++++++++++ src/search/command_line.h | 3 +++ src/search/planner.cc | 18 ++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index e8934d9240..86bd798968 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -184,6 +184,44 @@ shared_ptr parse_cmd_line( return parse_cmd_line_aux(args); } +vector complete_args(const string ¤t_word, const vector &args) { + assert(!args.empty()); // args[0] is always the program name. + const string &last_arg = args.back(); + vector suggestions; + if (find(args.begin(), args.end(), "--help") != args.end()) { + suggestions.push_back("--txt2tags"); + plugins::Registry registry = plugins::RawRegistry::instance()->construct_registry(); + for (const shared_ptr &feature : registry.get_features()) { + suggestions.push_back(feature->get_key()); + } + } else if (last_arg == "--internal-plan-file") { + // suggest filename starting with current_word + } else if (last_arg == "--internal-previous-portfolio-plans") { + // no suggestions, integer expected + } else if (last_arg == "--search") { + // suggestions in search string based on current_word + } else { + // not completing an argument + suggestions.push_back("--help"); + suggestions.push_back("--search"); + suggestions.push_back("--internal-plan-file"); + suggestions.push_back("--internal-previous-portfolio-plans"); + suggestions.push_back("--if-unit-cost"); + suggestions.push_back("--if-non-unit-cost"); + suggestions.push_back("--always"); + // remove suggestions not starting with current_word + } + + if (!current_word.empty()) { + // Suggest only words that match with current_word + suggestions.erase( + remove_if(suggestions.begin(), suggestions.end(), + [&](const string &value) { + return !value.starts_with(current_word); + }), suggestions.end()); + } + return suggestions; +} string usage(const string &progname) { return "usage: \n" + diff --git a/src/search/command_line.h b/src/search/command_line.h index aeec8bb058..3ba244fd1b 100644 --- a/src/search/command_line.h +++ b/src/search/command_line.h @@ -3,11 +3,14 @@ #include #include +#include class SearchAlgorithm; extern std::shared_ptr parse_cmd_line( int argc, const char **argv, bool is_unit_cost); +extern std::vector complete_args( + const std::string ¤t_word, const std::vector &args); extern std::string usage(const std::string &progname); diff --git a/src/search/planner.cc b/src/search/planner.cc index a524825698..c2843cb7bd 100644 --- a/src/search/planner.cc +++ b/src/search/planner.cc @@ -14,6 +14,24 @@ using utils::ExitCode; int main(int argc, const char **argv) { try { + if (static_cast(argv[1]) == "--bash-complete") { + if (argc < 3) { + utils::g_log << "TODO error" << endl; + utils::exit_with(ExitCode::SEARCH_INPUT_ERROR); + } + string current_word = static_cast(argv[2]); + vector args; + args.reserve(argc - 3); + for (int i = 3; i < argc; ++i) { + args.emplace_back(argv[i]); + } + for (const string &suggestion : complete_args(current_word, args)) { + cout << suggestion << endl; + } + // Do not use exit_with here because it will generate additional output. + exit(0); + } + utils::register_event_handlers(); if (argc < 2) { From c7841266f088bbb1ea86f158fc8ea4404170ffab Mon Sep 17 00:00:00 2001 From: speckdavid Date: Mon, 15 Jul 2024 17:29:41 +0200 Subject: [PATCH 04/44] started communiaction between driver and search component --- driver/tab_completion.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 4101edc0ae..8a09ad028d 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,4 +1,6 @@ from pathlib import Path +import subprocess +import sys from . import util @@ -121,7 +123,18 @@ def _planner_args_completion(prefix, parsed_args, **kwargs): curr_options.append(option) if curr_options == search_options: - completions.append("--great-search-options") + downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" + if not downward.exists(): + argcomplete.warn("Search binary does not exists.") + exit(1) + + unpacked_search_options = " ".join([f"\"{o}\"" for o in search_options]) + cmd = f"{downward} --bash-complete \"{prefix}\" \"downward\" {unpacked_search_options}" + #argcomplete.warn(cmd) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True) + stdout, _ = p.communicate() + + completions += stdout.split() else: completions.append("--great-translate-options") From 64713a9dabd97a1954bab16dee8f58d7c782af9c Mon Sep 17 00:00:00 2001 From: speckdavid Date: Tue, 16 Jul 2024 10:23:04 +0200 Subject: [PATCH 05/44] some prelim fixes and style --- driver/tab_completion.py | 9 ++++++--- src/search/command_line.cc | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 8a09ad028d..430376b361 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,6 +1,5 @@ from pathlib import Path import subprocess -import sys from . import util @@ -110,19 +109,23 @@ def _planner_args_completion(prefix, parsed_args, **kwargs): completions.append("--") if filenames: + completions += ["--translate-options", "--search-options"] translate_options = [] search_options = [] + mode = "SEARCH" curr_options = search_options for option in options: if option == "--translate-options": curr_options = translate_options + mode = "TRANSLATE" elif option == "--search-options": curr_options = search_options + mode = "SEARCH" else: curr_options.append(option) - if curr_options == search_options: + if mode == "SEARCH": downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" if not downward.exists(): argcomplete.warn("Search binary does not exists.") @@ -130,7 +133,7 @@ def _planner_args_completion(prefix, parsed_args, **kwargs): unpacked_search_options = " ".join([f"\"{o}\"" for o in search_options]) cmd = f"{downward} --bash-complete \"{prefix}\" \"downward\" {unpacked_search_options}" - #argcomplete.warn(cmd) + # argcomplete.warn(cmd) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True) stdout, _ = p.communicate() diff --git a/src/search/command_line.cc b/src/search/command_line.cc index 86bd798968..c67e2249b6 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -216,9 +216,9 @@ vector complete_args(const string ¤t_word, const vector &a // Suggest only words that match with current_word suggestions.erase( remove_if(suggestions.begin(), suggestions.end(), - [&](const string &value) { - return !value.starts_with(current_word); - }), suggestions.end()); + [&](const string &value) { + return !value.starts_with(current_word); + }), suggestions.end()); } return suggestions; } From e3aaf187252310a08a96a428a15983d81186fe9c Mon Sep 17 00:00:00 2001 From: speckdavid Date: Tue, 16 Jul 2024 11:42:16 +0200 Subject: [PATCH 06/44] logic of argcomplete --- driver/tab_completion.py | 47 +++++++++++++++++----------------------- fast-downward.py | 1 + 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 430376b361..2f4a0456c4 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -108,37 +108,30 @@ def _planner_args_completion(prefix, parsed_args, **kwargs): if not options and not (len(filenames) == 2 and double_dash_in_options): completions.append("--") + translate_options = [] + search_options = [] + + curr_options = search_options + for option in options: + if option == "--translate-options": + curr_options = translate_options + elif option == "--search-options": + curr_options = search_options + else: + curr_options.append(option) + if filenames: - completions += ["--translate-options", "--search-options"] - translate_options = [] - search_options = [] - mode = "SEARCH" - - curr_options = search_options - for option in options: - if option == "--translate-options": - curr_options = translate_options - mode = "TRANSLATE" - elif option == "--search-options": - curr_options = search_options - mode = "SEARCH" - else: - curr_options.append(option) - - if mode == "SEARCH": - downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" - if not downward.exists(): - argcomplete.warn("Search binary does not exists.") - exit(1) + if curr_options is search_options: + completions.append("--translate-options") - unpacked_search_options = " ".join([f"\"{o}\"" for o in search_options]) - cmd = f"{downward} --bash-complete \"{prefix}\" \"downward\" {unpacked_search_options}" - # argcomplete.warn(cmd) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True) - stdout, _ = p.communicate() + downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" + if downward.exists(): + cmd = [str(downward), "--bash-complete", prefix, str(downward)] + search_options + output = subprocess.check_output(cmd, text=True) + completions += output.split() - completions += stdout.split() else: + completions.append("--search-options") completions.append("--great-translate-options") if len(filenames) < 2 and not double_dash_in_options and not curr_options: diff --git a/fast-downward.py b/fast-downward.py index dc7c843a42..3fe78f90e9 100755 --- a/fast-downward.py +++ b/fast-downward.py @@ -1,4 +1,5 @@ #! /usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK if __name__ == "__main__": from driver.main import main From c0e0b8a074cb5deb0016e3ba026318e94fbfb9c4 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Tue, 16 Jul 2024 13:50:07 +0200 Subject: [PATCH 07/44] tab completion for translator option when called as standalone and via the driver --- driver/tab_completion.py | 21 ++++++++++-- src/translate/arguments.py | 55 ++++++++++++++++++++++++++++++ src/translate/options.py | 70 +++++++------------------------------- src/translate/translate.py | 1 + 4 files changed, 87 insertions(+), 60 deletions(-) create mode 100644 src/translate/arguments.py diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 2f4a0456c4..332367e080 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,3 +1,4 @@ +import argparse from pathlib import Path import subprocess @@ -87,7 +88,7 @@ def add_build_arg_completer(build_arg): build_arg.completer = _build_arg_completion -def _planner_args_completion(prefix, parsed_args, **kwargs): +def _planner_args_completion(prefix, parser, parsed_args, **kwargs): if HAS_ARGCOMPLETE: if parsed_args.build and parsed_args.debug: argcomplete.warn("The option --debug is an alias for --build=debug " @@ -132,9 +133,23 @@ def _planner_args_completion(prefix, parsed_args, **kwargs): else: completions.append("--search-options") - completions.append("--great-translate-options") - if len(filenames) < 2 and not double_dash_in_options and not curr_options: + tranlator_arguments = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" + import importlib.util + import sys + spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments) + arguments = importlib.util.module_from_spec(spec) + sys.modules["arguments"] = arguments + spec.loader.exec_module(arguments) + + # We create a new parser that will handle the autocompletion + # for the translator + new_parser = argparse.ArgumentParser() + arguments.add_args(new_parser, False) + new_parser.add_argument("--search-options", action="store_false") + argcomplete.autocomplete(new_parser) + + if len(filenames) < 2 and not double_dash_in_options and not curr_options and "--translate-options" not in options and not "--search-options" not in options: file_completer = argcomplete.FilesCompleter() completions += file_completer(prefix, **kwargs) diff --git a/src/translate/arguments.py b/src/translate/arguments.py new file mode 100644 index 0000000000..12ee56a965 --- /dev/null +++ b/src/translate/arguments.py @@ -0,0 +1,55 @@ +def add_args(argparser, add_file_args=True): + if add_file_args: + argparser.add_argument( + "domain", help="path to domain pddl file") + argparser.add_argument( + "task", help="path to task pddl file") + argparser.add_argument( + "--relaxed", dest="generate_relaxed_task", action="store_true", + help="output relaxed task (no delete effects)") + argparser.add_argument( + "--full-encoding", + dest="use_partial_encoding", action="store_false", + help="By default we represent facts that occur in multiple " + "mutex groups only in one variable. Using this parameter adds " + "these facts to multiple variables. This can make the meaning " + "of the variables clearer, but increases the number of facts.") + argparser.add_argument( + "--invariant-generation-max-candidates", default=100000, type=int, + help="max number of candidates for invariant generation " + "(default: %(default)d). Set to 0 to disable invariant " + "generation and obtain only binary variables. The limit is " + "needed for grounded input files that would otherwise produce " + "too many candidates.") + argparser.add_argument( + "--sas-file", default="output.sas", + help="path to the SAS output file (default: %(default)s)") + argparser.add_argument( + "--invariant-generation-max-time", default=300, type=int, + help="max time for invariant generation (default: %(default)ds)") + argparser.add_argument( + "--add-implied-preconditions", action="store_true", + help="infer additional preconditions. This setting can cause a " + "severe performance penalty due to weaker relevance analysis " + "(see issue7).") + argparser.add_argument( + "--keep-unreachable-facts", + dest="filter_unreachable_facts", action="store_false", + help="keep facts that can't be reached from the initial state") + argparser.add_argument( + "--skip-variable-reordering", + dest="reorder_variables", action="store_false", + help="do not reorder variables based on the causal graph. Do not use " + "this option with the causal graph heuristic!") + argparser.add_argument( + "--keep-unimportant-variables", + dest="filter_unimportant_vars", action="store_false", + help="keep variables that do not influence the goal in the causal graph") + argparser.add_argument( + "--dump-task", action="store_true", + help="dump human-readable SAS+ representation of the task") + argparser.add_argument( + "--layer-strategy", default="min", choices=["min", "max"], + help="How to assign layers to derived variables. 'min' attempts to put as " + "many variables into the same layer as possible, while 'max' puts each variable " + "into its own layer unless it is part of a cycle.") diff --git a/src/translate/options.py b/src/translate/options.py index 091e8e1c83..139a51794d 100644 --- a/src/translate/options.py +++ b/src/translate/options.py @@ -1,63 +1,13 @@ import argparse import sys +import arguments -def parse_args(): - argparser = argparse.ArgumentParser() - argparser.add_argument( - "domain", help="path to domain pddl file") - argparser.add_argument( - "task", help="path to task pddl file") - argparser.add_argument( - "--relaxed", dest="generate_relaxed_task", action="store_true", - help="output relaxed task (no delete effects)") - argparser.add_argument( - "--full-encoding", - dest="use_partial_encoding", action="store_false", - help="By default we represent facts that occur in multiple " - "mutex groups only in one variable. Using this parameter adds " - "these facts to multiple variables. This can make the meaning " - "of the variables clearer, but increases the number of facts.") - argparser.add_argument( - "--invariant-generation-max-candidates", default=100000, type=int, - help="max number of candidates for invariant generation " - "(default: %(default)d). Set to 0 to disable invariant " - "generation and obtain only binary variables. The limit is " - "needed for grounded input files that would otherwise produce " - "too many candidates.") - argparser.add_argument( - "--sas-file", default="output.sas", - help="path to the SAS output file (default: %(default)s)") - argparser.add_argument( - "--invariant-generation-max-time", default=300, type=int, - help="max time for invariant generation (default: %(default)ds)") - argparser.add_argument( - "--add-implied-preconditions", action="store_true", - help="infer additional preconditions. This setting can cause a " - "severe performance penalty due to weaker relevance analysis " - "(see issue7).") - argparser.add_argument( - "--keep-unreachable-facts", - dest="filter_unreachable_facts", action="store_false", - help="keep facts that can't be reached from the initial state") - argparser.add_argument( - "--skip-variable-reordering", - dest="reorder_variables", action="store_false", - help="do not reorder variables based on the causal graph. Do not use " - "this option with the causal graph heuristic!") - argparser.add_argument( - "--keep-unimportant-variables", - dest="filter_unimportant_vars", action="store_false", - help="keep variables that do not influence the goal in the causal graph") - argparser.add_argument( - "--dump-task", action="store_true", - help="dump human-readable SAS+ representation of the task") - argparser.add_argument( - "--layer-strategy", default="min", choices=["min", "max"], - help="How to assign layers to derived variables. 'min' attempts to put as " - "many variables into the same layer as possible, while 'max' puts each variable " - "into its own layer unless it is part of a cycle.") - return argparser.parse_args() +try: + import argcomplete + HAS_ARGCOMPLETE = True +except ImportError: + HAS_ARGCOMPLETE = False def copy_args_to_module(args): @@ -67,7 +17,13 @@ def copy_args_to_module(args): def setup(): - args = parse_args() + argparser = argparse.ArgumentParser() + arguments.add_args(argparser) + + if HAS_ARGCOMPLETE: + argcomplete.autocomplete(argparser) + + args = argparser.parse_args() copy_args_to_module(args) diff --git a/src/translate/translate.py b/src/translate/translate.py index d2df8cafb8..d7e946ca2f 100755 --- a/src/translate/translate.py +++ b/src/translate/translate.py @@ -1,4 +1,5 @@ #! /usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK import os From d5de54f3261af61abc7c83979fd630d5c4b1daa7 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Tue, 16 Jul 2024 13:54:03 +0200 Subject: [PATCH 08/44] minor --- driver/tab_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 332367e080..06eefbb72c 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -146,7 +146,7 @@ def _planner_args_completion(prefix, parser, parsed_args, **kwargs): # for the translator new_parser = argparse.ArgumentParser() arguments.add_args(new_parser, False) - new_parser.add_argument("--search-options", action="store_false") + new_parser.add_argument("--search-options", action="store_true") argcomplete.autocomplete(new_parser) if len(filenames) < 2 and not double_dash_in_options and not curr_options and "--translate-options" not in options and not "--search-options" not in options: From ddd7f84f7202a0f8d73a31a9681d3a4402f2c4da Mon Sep 17 00:00:00 2001 From: speckdavid Date: Wed, 17 Jul 2024 10:49:56 +0200 Subject: [PATCH 09/44] some intermediate fixes --- driver/tab_completion.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 06eefbb72c..4fa13bad93 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -123,7 +123,8 @@ def _planner_args_completion(prefix, parser, parsed_args, **kwargs): if filenames: if curr_options is search_options: - completions.append("--translate-options") + if search_options: + completions.append("--translate-options") downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" if downward.exists(): @@ -132,8 +133,6 @@ def _planner_args_completion(prefix, parser, parsed_args, **kwargs): completions += output.split() else: - completions.append("--search-options") - tranlator_arguments = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" import importlib.util import sys @@ -146,10 +145,11 @@ def _planner_args_completion(prefix, parser, parsed_args, **kwargs): # for the translator new_parser = argparse.ArgumentParser() arguments.add_args(new_parser, False) - new_parser.add_argument("--search-options", action="store_true") + if curr_options: + new_parser.add_argument("--search-options", action="store_true") argcomplete.autocomplete(new_parser) - if len(filenames) < 2 and not double_dash_in_options and not curr_options and "--translate-options" not in options and not "--search-options" not in options: + if len(filenames) < 2 and not double_dash_in_options and not translate_options and not search_options: file_completer = argcomplete.FilesCompleter() completions += file_completer(prefix, **kwargs) From 37416632914fb40e0b12af34b145e6be768133b5 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Wed, 17 Jul 2024 11:44:58 +0200 Subject: [PATCH 10/44] some logic fixes --- driver/tab_completion.py | 70 ++++++++++++++------------------------ src/translate/arguments.py | 13 +++---- src/translate/options.py | 3 +- 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 4fa13bad93..86159e0d41 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,6 +1,8 @@ import argparse +import importlib.util from pathlib import Path import subprocess +import sys from . import util @@ -50,31 +52,9 @@ def _split_off_filenames(planner_args): return planner_args[:num_filenames], planner_args[num_filenames:], double_dash_in_options -def _split_planner_args(parser, args): - """Partition args.planner_args, the list of arguments for the - planner components, into args.filenames, args.translate_options - and args.search_options. Modifies args directly and removes the original - args.planner_args list.""" - - args.filenames, options = _split_off_filenames(args.planner_args) - - args.translate_options = [] - args.search_options = [] - - curr_options = args.search_options - for option in options: - if option == "--translate-options": - curr_options = args.translate_options - elif option == "--search-options": - curr_options = args.search_options - else: - curr_options.append(option) - - def _build_arg_completion(prefix, parsed_args, **kwargs): if parsed_args.debug: - argcomplete.warn("The option --debug is an alias for --build=debug " - "--validate. Do no specify both --debug and --build.") + argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") exit(1) builds_folder = Path(util.REPO_ROOT_DIR) / "builds" @@ -88,11 +68,10 @@ def add_build_arg_completer(build_arg): build_arg.completer = _build_arg_completion -def _planner_args_completion(prefix, parser, parsed_args, **kwargs): +def _planner_args_completion(prefix, parsed_args, **kwargs): if HAS_ARGCOMPLETE: if parsed_args.build and parsed_args.debug: - argcomplete.warn("The option --debug is an alias for --build=debug " - "--validate. Do no specify both --debug and --build.") + argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") exit(1) build = parsed_args.build @@ -111,19 +90,23 @@ def _planner_args_completion(prefix, parser, parsed_args, **kwargs): translate_options = [] search_options = [] + last_option_was_mode_switch = False curr_options = search_options for option in options: if option == "--translate-options": curr_options = translate_options + last_option_was_mode_switch = True elif option == "--search-options": curr_options = search_options + last_option_was_mode_switch = True else: curr_options.append(option) + last_option_was_mode_switch = False if filenames: if curr_options is search_options: - if search_options: + if not last_option_was_mode_switch: completions.append("--translate-options") downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" @@ -133,23 +116,22 @@ def _planner_args_completion(prefix, parser, parsed_args, **kwargs): completions += output.split() else: - tranlator_arguments = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" - import importlib.util - import sys - spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments) - arguments = importlib.util.module_from_spec(spec) - sys.modules["arguments"] = arguments - spec.loader.exec_module(arguments) - - # We create a new parser that will handle the autocompletion - # for the translator - new_parser = argparse.ArgumentParser() - arguments.add_args(new_parser, False) - if curr_options: - new_parser.add_argument("--search-options", action="store_true") - argcomplete.autocomplete(new_parser) - - if len(filenames) < 2 and not double_dash_in_options and not translate_options and not search_options: + tranlator_arguments_path = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" + if tranlator_arguments_path.exists(): + spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments_path) + tranlator_arguments = importlib.util.module_from_spec(spec) + sys.modules["arguments"] = tranlator_arguments + spec.loader.exec_module(tranlator_arguments) + + # We create a new parser that will handle the autocompletion + # for the translator + translator_parser = argparse.ArgumentParser() + tranlator_arguments.add_options(translator_parser) + if not last_option_was_mode_switch: + translator_parser.add_argument("--search-options", action="store_true") + argcomplete.autocomplete(translator_parser) + + if len(filenames) < 2 and not double_dash_in_options and not translate_options and not search_options and not last_option_was_mode_switch: file_completer = argcomplete.FilesCompleter() completions += file_completer(prefix, **kwargs) diff --git a/src/translate/arguments.py b/src/translate/arguments.py index 12ee56a965..d015b55ef3 100644 --- a/src/translate/arguments.py +++ b/src/translate/arguments.py @@ -1,9 +1,10 @@ -def add_args(argparser, add_file_args=True): - if add_file_args: - argparser.add_argument( - "domain", help="path to domain pddl file") - argparser.add_argument( - "task", help="path to task pddl file") +def add_file_arguments(argparser): + argparser.add_argument( + "domain", help="path to domain pddl file") + argparser.add_argument( + "task", help="path to task pddl file") + +def add_options(argparser): argparser.add_argument( "--relaxed", dest="generate_relaxed_task", action="store_true", help="output relaxed task (no delete effects)") diff --git a/src/translate/options.py b/src/translate/options.py index 139a51794d..ed56d08481 100644 --- a/src/translate/options.py +++ b/src/translate/options.py @@ -18,7 +18,8 @@ def copy_args_to_module(args): def setup(): argparser = argparse.ArgumentParser() - arguments.add_args(argparser) + arguments.add_file_arguments(argparser) + arguments.add_options(argparser) if HAS_ARGCOMPLETE: argcomplete.autocomplete(argparser) From 816f9839bdd6e0b8f4edf5608df49f5ff6c7b518 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Wed, 17 Jul 2024 14:49:28 +0200 Subject: [PATCH 11/44] forward cursor point to c++ --- driver/tab_completion.py | 8 +++- src/search/command_line.cc | 78 ++++++++++++++++++++++++++++++++++++-- src/search/command_line.h | 3 +- src/search/planner.cc | 24 +++--------- 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 86159e0d41..416fe97860 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -111,10 +111,14 @@ def _planner_args_completion(prefix, parsed_args, **kwargs): downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" if downward.exists(): - cmd = [str(downward), "--bash-complete", prefix, str(downward)] + search_options + simulated_commandline = [str(downward)] + search_options + [prefix] + comp_line = " ".join(simulated_commandline) + comp_point = str(len(comp_line)) + comp_cword = str(len(simulated_commandline) - 1) + cmd = [str(downward), "--bash-complete", + comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) completions += output.split() - else: tranlator_arguments_path = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" if tranlator_arguments_path.exists(): diff --git a/src/search/command_line.cc b/src/search/command_line.cc index c67e2249b6..a7f9777e54 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -184,11 +184,13 @@ shared_ptr parse_cmd_line( return parse_cmd_line_aux(args); } -vector complete_args(const string ¤t_word, const vector &args) { - assert(!args.empty()); // args[0] is always the program name. - const string &last_arg = args.back(); +static vector complete_args( + const vector &parsed_args, const string ¤t_word, + int /*cursor_pos*/) { + assert(!parsed_args.empty()); // args[0] is always the program name. + const string &last_arg = parsed_args.back(); vector suggestions; - if (find(args.begin(), args.end(), "--help") != args.end()) { + if (find(parsed_args.begin(), parsed_args.end(), "--help") != parsed_args.end()) { suggestions.push_back("--txt2tags"); plugins::Registry registry = plugins::RawRegistry::instance()->construct_registry(); for (const shared_ptr &feature : registry.get_features()) { @@ -200,6 +202,10 @@ vector complete_args(const string ¤t_word, const vector &a // no suggestions, integer expected } else if (last_arg == "--search") { // suggestions in search string based on current_word + plugins::Registry registry = plugins::RawRegistry::instance()->construct_registry(); + for (const shared_ptr &feature : registry.get_features()) { + suggestions.push_back(feature->get_key() + "("); + } } else { // not completing an argument suggestions.push_back("--help"); @@ -223,6 +229,70 @@ vector complete_args(const string ¤t_word, const vector &a return suggestions; } +void handle_tab_completion(int argc, const char **argv) { + if (argc < 2 || static_cast(argv[1]) != "--bash-complete") { + return; + } + if (argc < 5) { + input_error( + "The option --bash-complete is only meant to be called " + "internally to generate suggestions for tab completion.\n" + "Usage:\n ./downward --bash-complete " + "$COMP_POINT \"$COMP_LINE\" $COMP_CWORD ${COMP_WORDS[@]}\n" + "where the environment variables have their usual meaning for bash completion:\n" + "$COMP_POINT is the position of the cursor in the command line.\n" + "$COMP_LINE is the current command line.\n" + "$COMP_CWORD is an index into ${COMP_WORDS} of the word under the cursor.\n" + "$COMP_WORDS is the current command line split into words.\n" + ); + } + int cursor_pos = parse_int_arg("COMP_POINT", static_cast(argv[2])); + const string command_line = static_cast(argv[3]); + int cursor_word_index = parse_int_arg("COMP_CWORD", static_cast(argv[4])); + vector words; + words.reserve(argc - 5); + for (int i = 5; i < argc; ++i) { + words.emplace_back(argv[i]); + } + + vector parsed_args; + string current_word; + int pos = 0; + int end = static_cast(command_line.size()); + int num_words = static_cast(words.size()); + for (int i = 0; i < num_words; ++i) { + current_word = words[i]; + int word_len = static_cast(current_word.size()); + if (command_line.substr(pos, word_len) != current_word) { + input_error("Expected '" + current_word + "' in $COMP_LINE at position " + + to_string(pos)); + } + if (i == cursor_word_index) { + if (cursor_pos < 0 || cursor_pos > word_len) { + input_error("Inconsistent information in COMP_LINE and COMP_WORDS"); + } + break; + } + + // Skip word + pos += word_len; + cursor_pos -= word_len; + parsed_args.push_back(current_word); + + // Skip whitespace between words. + while (pos < end && isspace(command_line[pos])) { + ++pos; + --cursor_pos; + } + } + + for (const string &suggestion : complete_args(parsed_args, current_word, cursor_pos)) { + cout << suggestion << endl; + } + // Do not use exit_with here because it would generate additional output. + exit(0); +} + string usage(const string &progname) { return "usage: \n" + progname + " [OPTIONS] --search SEARCH < OUTPUT\n\n" diff --git a/src/search/command_line.h b/src/search/command_line.h index 3ba244fd1b..0a84f70d46 100644 --- a/src/search/command_line.h +++ b/src/search/command_line.h @@ -9,8 +9,7 @@ class SearchAlgorithm; extern std::shared_ptr parse_cmd_line( int argc, const char **argv, bool is_unit_cost); -extern std::vector complete_args( - const std::string ¤t_word, const std::vector &args); +extern void handle_tab_completion(int argc, const char **argv); extern std::string usage(const std::string &progname); diff --git a/src/search/planner.cc b/src/search/planner.cc index c2843cb7bd..00dd78fbaf 100644 --- a/src/search/planner.cc +++ b/src/search/planner.cc @@ -14,24 +14,12 @@ using utils::ExitCode; int main(int argc, const char **argv) { try { - if (static_cast(argv[1]) == "--bash-complete") { - if (argc < 3) { - utils::g_log << "TODO error" << endl; - utils::exit_with(ExitCode::SEARCH_INPUT_ERROR); - } - string current_word = static_cast(argv[2]); - vector args; - args.reserve(argc - 3); - for (int i = 3; i < argc; ++i) { - args.emplace_back(argv[i]); - } - for (const string &suggestion : complete_args(current_word, args)) { - cout << suggestion << endl; - } - // Do not use exit_with here because it will generate additional output. - exit(0); - } - + /* + We have to handle tab completion before registering event handlers + because event handlers will print to stdout when the program exits + and everything on stdout counts as a suggestion for tab completion. + */ + handle_tab_completion(argc, argv); utils::register_event_handlers(); if (argc < 2) { From ed83082aff77001cf826ab04e10a7a08de79975e Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Wed, 17 Jul 2024 15:28:40 +0200 Subject: [PATCH 12/44] always set completer --- driver/arguments.py | 4 +- driver/tab_completion.py | 149 ++++++++++++++++++--------------------- 2 files changed, 72 insertions(+), 81 deletions(-) diff --git a/driver/arguments.py b/driver/arguments.py index 19b28362e9..bcff0621b0 100644 --- a/driver/arguments.py +++ b/driver/arguments.py @@ -404,7 +404,7 @@ def parse_args(): "this path does not exist, it tries the directory " "'/builds/BUILD/bin', where the build script creates " "them by default.") - tab_completion.add_build_arg_completer(build_arg) + build_arg.completer = tab_completion.complete_build_arg driver_other.add_argument( "--debug", action="store_true", help="alias for --build=debug --validate") @@ -446,7 +446,7 @@ def parse_args(): planner_args = parser.add_argument( "planner_args", nargs=argparse.REMAINDER, help="file names and options passed on to planner components") - tab_completion.add_planner_args_completer(planner_args) + planner_args.completer = tab_completion.complete_planner_args # Using argparse.REMAINDER relies on the fact that the first # argument that doesn't belong to the driver doesn't look like an diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 416fe97860..9b2b10d612 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -52,7 +52,7 @@ def _split_off_filenames(planner_args): return planner_args[:num_filenames], planner_args[num_filenames:], double_dash_in_options -def _build_arg_completion(prefix, parsed_args, **kwargs): +def complete_build_arg(prefix, parsed_args, **kwargs): if parsed_args.debug: argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") exit(1) @@ -64,86 +64,77 @@ def _build_arg_completion(prefix, parsed_args, **kwargs): return [p.name for p in builds_folder.iterdir() if p.is_dir()] -def add_build_arg_completer(build_arg): - build_arg.completer = _build_arg_completion - +def complete_planner_args(prefix, parsed_args, **kwargs): + if parsed_args.build and parsed_args.debug: + argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") + exit(1) -def _planner_args_completion(prefix, parsed_args, **kwargs): - if HAS_ARGCOMPLETE: - if parsed_args.build and parsed_args.debug: - argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") - exit(1) - - build = parsed_args.build - if not build: - if parsed_args.debug: - build = "debug" - else: - build = "release" - - filenames, options, double_dash_in_options = _split_off_filenames(parsed_args.planner_args) - - completions = [] - - if not options and not (len(filenames) == 2 and double_dash_in_options): - completions.append("--") - - translate_options = [] - search_options = [] - last_option_was_mode_switch = False - - curr_options = search_options - for option in options: - if option == "--translate-options": - curr_options = translate_options - last_option_was_mode_switch = True - elif option == "--search-options": - curr_options = search_options - last_option_was_mode_switch = True - else: - curr_options.append(option) - last_option_was_mode_switch = False - - if filenames: - if curr_options is search_options: + build = parsed_args.build + if not build: + if parsed_args.debug: + build = "debug" + else: + build = "release" + + filenames, options, double_dash_in_options = _split_off_filenames(parsed_args.planner_args) + + completions = [] + + if not options and not (len(filenames) == 2 and double_dash_in_options): + completions.append("--") + + translate_options = [] + search_options = [] + last_option_was_mode_switch = False + + curr_options = search_options + for option in options: + if option == "--translate-options": + curr_options = translate_options + last_option_was_mode_switch = True + elif option == "--search-options": + curr_options = search_options + last_option_was_mode_switch = True + else: + curr_options.append(option) + last_option_was_mode_switch = False + + if filenames: + if curr_options is search_options: + if not last_option_was_mode_switch: + completions.append("--translate-options") + + downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" + if downward.exists(): + simulated_commandline = [str(downward)] + search_options + [prefix] + comp_line = " ".join(simulated_commandline) + comp_point = str(len(comp_line)) + comp_cword = str(len(simulated_commandline) - 1) + cmd = [str(downward), "--bash-complete", + comp_point, comp_line, comp_cword] + simulated_commandline + output = subprocess.check_output(cmd, text=True) + completions += output.split() + else: + tranlator_arguments_path = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" + if tranlator_arguments_path.exists(): + spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments_path) + tranlator_arguments = importlib.util.module_from_spec(spec) + sys.modules["arguments"] = tranlator_arguments + spec.loader.exec_module(tranlator_arguments) + + # We create a new parser that will handle the autocompletion + # for the translator + translator_parser = argparse.ArgumentParser() + tranlator_arguments.add_options(translator_parser) if not last_option_was_mode_switch: - completions.append("--translate-options") - - downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" - if downward.exists(): - simulated_commandline = [str(downward)] + search_options + [prefix] - comp_line = " ".join(simulated_commandline) - comp_point = str(len(comp_line)) - comp_cword = str(len(simulated_commandline) - 1) - cmd = [str(downward), "--bash-complete", - comp_point, comp_line, comp_cword] + simulated_commandline - output = subprocess.check_output(cmd, text=True) - completions += output.split() - else: - tranlator_arguments_path = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" - if tranlator_arguments_path.exists(): - spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments_path) - tranlator_arguments = importlib.util.module_from_spec(spec) - sys.modules["arguments"] = tranlator_arguments - spec.loader.exec_module(tranlator_arguments) - - # We create a new parser that will handle the autocompletion - # for the translator - translator_parser = argparse.ArgumentParser() - tranlator_arguments.add_options(translator_parser) - if not last_option_was_mode_switch: - translator_parser.add_argument("--search-options", action="store_true") - argcomplete.autocomplete(translator_parser) - - if len(filenames) < 2 and not double_dash_in_options and not translate_options and not search_options and not last_option_was_mode_switch: - file_completer = argcomplete.FilesCompleter() - completions += file_completer(prefix, **kwargs) - - return completions - - -def add_planner_args_completer(planner_args): - planner_args.completer = _planner_args_completion + translator_parser.add_argument("--search-options", action="store_true") + argcomplete.autocomplete(translator_parser) + + if len(filenames) < 2 and not double_dash_in_options and not translate_options and not search_options and not last_option_was_mode_switch: + file_completer = argcomplete.FilesCompleter() + completions += file_completer(prefix, **kwargs) + + return completions def enable(parser): From 80839920db40cc690a2f7364e9c997bd2a260eca Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Wed, 17 Jul 2024 17:23:51 +0200 Subject: [PATCH 13/44] reduce code duplication and simplify logic --- driver/arguments.py | 60 +------------------------------ driver/tab_completion.py | 78 +++++++++------------------------------- driver/util.py | 61 +++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 120 deletions(-) diff --git a/driver/arguments.py b/driver/arguments.py index bcff0621b0..6dbee5a8bc 100644 --- a/driver/arguments.py +++ b/driver/arguments.py @@ -128,64 +128,6 @@ def _format_args(self, action, default_metavar): return argparse.HelpFormatter._format_args(self, action, default_metavar) -def _rindex(seq, element): - """Like list.index, but gives the index of the *last* occurrence.""" - seq = list(reversed(seq)) - reversed_index = seq.index(element) - return len(seq) - 1 - reversed_index - - -def _split_off_filenames(planner_args): - """Given the list of arguments to be passed on to the planner - components, split it into a prefix of filenames and a suffix of - options. Returns a pair (filenames, options). - - If a "--" separator is present, the last such separator serves as - the border between filenames and options. The separator itself is - not returned. (This implies that "--" can be a filename, but never - an option to a planner component.) - - If no such separator is present, the first argument that begins - with "-" and consists of at least two characters starts the list - of options, and all previous arguments are filenames.""" - - if "--" in planner_args: - separator_pos = _rindex(planner_args, "--") - num_filenames = separator_pos - del planner_args[separator_pos] - else: - num_filenames = 0 - for arg in planner_args: - # We treat "-" by itself as a filename because by common - # convention it denotes stdin or stdout, and we might want - # to support this later. - if arg.startswith("-") and arg != "-": - break - num_filenames += 1 - return planner_args[:num_filenames], planner_args[num_filenames:] - - -def _split_planner_args(parser, args): - """Partition args.planner_args, the list of arguments for the - planner components, into args.filenames, args.translate_options - and args.search_options. Modifies args directly and removes the original - args.planner_args list.""" - - args.filenames, options = _split_off_filenames(args.planner_args) - - args.translate_options = [] - args.search_options = [] - - curr_options = args.search_options - for option in options: - if option == "--translate-options": - curr_options = args.translate_options - elif option == "--search-options": - curr_options = args.search_options - else: - curr_options.append(option) - - def _check_mutex_args(parser, args, required=False): for pos, (name1, is_specified1) in enumerate(args): for name2, is_specified2 in args[pos + 1:]: @@ -473,7 +415,7 @@ def parse_args(): else: args.build = "release" - _split_planner_args(parser, args) + util.split_planner_args(args) _check_mutex_args(parser, [ ("--alias", args.alias is not None), diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 9b2b10d612..56a102a64c 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -13,45 +13,6 @@ HAS_ARGCOMPLETE = False -def _rindex(seq, element): - """Like list.index, but gives the index of the *last* occurrence.""" - seq = list(reversed(seq)) - reversed_index = seq.index(element) - return len(seq) - 1 - reversed_index - - -def _split_off_filenames(planner_args): - """Given the list of arguments to be passed on to the planner - components, split it into a prefix of filenames and a suffix of - options. Returns a pair (filenames, options). - - If a "--" separator is present, the last such separator serves as - the border between filenames and options. The separator itself is - not returned. (This implies that "--" can be a filename, but never - an option to a planner component.) - - If no such separator is present, the first argument that begins - with "-" and consists of at least two characters starts the list - of options, and all previous arguments are filenames.""" - - double_dash_in_options = False - if "--" in planner_args: - separator_pos = _rindex(planner_args, "--") - num_filenames = separator_pos - del planner_args[separator_pos] - double_dash_in_options = True - else: - num_filenames = 0 - for arg in planner_args: - # We treat "-" by itself as a filename because by common - # convention it denotes stdin or stdout, and we might want - # to support this later. - if arg.startswith("-") and arg != "-": - break - num_filenames += 1 - return planner_args[:num_filenames], planner_args[num_filenames:], double_dash_in_options - - def complete_build_arg(prefix, parsed_args, **kwargs): if parsed_args.debug: argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") @@ -76,37 +37,32 @@ def complete_planner_args(prefix, parsed_args, **kwargs): else: build = "release" - filenames, options, double_dash_in_options = _split_off_filenames(parsed_args.planner_args) + # Get some information from planner_args before it is deleted in split_planner_args(). + planner_args = parsed_args.planner_args + num_planner_args = len(planner_args) + mode_switches = ["--translate-options", "--search-options"] + last_option_was_mode_switch = planner_args and (planner_args[-1] in mode_switches) + double_dash_in_options = "--" in planner_args + + current_mode = util.split_planner_args(parsed_args) + num_filenames = len(parsed_args.filenames) + has_only_filename_options = (num_filenames == num_planner_args) + has_only_filename_or_double_dash_options = (num_filenames + int(double_dash_in_options) == num_planner_args) + can_use_double_dash = (1 <= num_planner_args <= 2) and has_only_filename_or_double_dash_options completions = [] - if not options and not (len(filenames) == 2 and double_dash_in_options): + if can_use_double_dash: completions.append("--") - translate_options = [] - search_options = [] - last_option_was_mode_switch = False - - curr_options = search_options - for option in options: - if option == "--translate-options": - curr_options = translate_options - last_option_was_mode_switch = True - elif option == "--search-options": - curr_options = search_options - last_option_was_mode_switch = True - else: - curr_options.append(option) - last_option_was_mode_switch = False - - if filenames: - if curr_options is search_options: + if parsed_args.filenames: + if current_mode == "search": if not last_option_was_mode_switch: completions.append("--translate-options") downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" if downward.exists(): - simulated_commandline = [str(downward)] + search_options + [prefix] + simulated_commandline = [str(downward)] + parsed_args.search_options + [prefix] comp_line = " ".join(simulated_commandline) comp_point = str(len(comp_line)) comp_cword = str(len(simulated_commandline) - 1) @@ -130,7 +86,7 @@ def complete_planner_args(prefix, parsed_args, **kwargs): translator_parser.add_argument("--search-options", action="store_true") argcomplete.autocomplete(translator_parser) - if len(filenames) < 2 and not double_dash_in_options and not translate_options and not search_options and not last_option_was_mode_switch: + if has_only_filename_options and len(parsed_args.filenames) < 2: file_completer = argcomplete.FilesCompleter() completions += file_completer(prefix, **kwargs) diff --git a/driver/util.py b/driver/util.py index 7016a2b806..5e551a4d53 100644 --- a/driver/util.py +++ b/driver/util.py @@ -39,3 +39,64 @@ def find_domain_path(task_path: Path): returncodes.exit_with_driver_input_error( "Error: Could not find domain file using automatic naming rules.") + +def _rindex(seq, element): + """Like list.index, but gives the index of the *last* occurrence.""" + seq = list(reversed(seq)) + reversed_index = seq.index(element) + return len(seq) - 1 - reversed_index + + +def _split_off_filenames(planner_args): + """Given the list of arguments to be passed on to the planner + components, split it into a prefix of filenames and a suffix of + options. Returns a pair (filenames, options). + + If a "--" separator is present, the last such separator serves as + the border between filenames and options. The separator itself is + not returned. (This implies that "--" can be a filename, but never + an option to a planner component.) + + If no such separator is present, the first argument that begins + with "-" and consists of at least two characters starts the list + of options, and all previous arguments are filenames.""" + + if "--" in planner_args: + separator_pos = _rindex(planner_args, "--") + num_filenames = separator_pos + del planner_args[separator_pos] + else: + num_filenames = 0 + for arg in planner_args: + # We treat "-" by itself as a filename because by common + # convention it denotes stdin or stdout, and we might want + # to support this later. + if arg.startswith("-") and arg != "-": + break + num_filenames += 1 + return planner_args[:num_filenames], planner_args[num_filenames:] + + +def split_planner_args(args): + """Partition args.planner_args, the list of arguments for the + planner components, into args.filenames, args.translate_options + and args.search_options. Modifies args directly. + Returns the name of the last active component for tab completion.""" + + args.filenames, options = _split_off_filenames(args.planner_args) + + args.translate_options = [] + args.search_options = [] + + curr_options = args.search_options + curr_option_name = "search" + for option in options: + if option == "--translate-options": + curr_options = args.translate_options + curr_option_name = "translate" + elif option == "--search-options": + curr_options = args.search_options + curr_option_name = "search" + else: + curr_options.append(option) + return curr_option_name From 75b64400cb2ab8d10b4353efea26a21c497c3eac Mon Sep 17 00:00:00 2001 From: speckdavid Date: Wed, 17 Jul 2024 18:06:32 +0200 Subject: [PATCH 14/44] work on broken translate completion via driver --- driver/tab_completion.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 56a102a64c..7cdbf389f9 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -4,6 +4,8 @@ import subprocess import sys +from argcomplete.completers import FilesCompleter + from . import util try: @@ -55,7 +57,7 @@ def complete_planner_args(prefix, parsed_args, **kwargs): if can_use_double_dash: completions.append("--") - if parsed_args.filenames: + if parsed_args.filenames or double_dash_in_options: if current_mode == "search": if not last_option_was_mode_switch: completions.append("--translate-options") @@ -67,24 +69,36 @@ def complete_planner_args(prefix, parsed_args, **kwargs): comp_point = str(len(comp_line)) comp_cword = str(len(simulated_commandline) - 1) cmd = [str(downward), "--bash-complete", - comp_point, comp_line, comp_cword] + simulated_commandline + comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) completions += output.split() else: + if not last_option_was_mode_switch: + completions.append("--search-options") + tranlator_arguments_path = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" if tranlator_arguments_path.exists(): + simulated_commandline = [str(tranlator_arguments_path)] + parsed_args.translate_options + [prefix] + comp_line = " ".join(simulated_commandline) + comp_point = str(len(comp_line)) + spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments_path) - tranlator_arguments = importlib.util.module_from_spec(spec) - sys.modules["arguments"] = tranlator_arguments - spec.loader.exec_module(tranlator_arguments) + translator_arguments = importlib.util.module_from_spec(spec) + sys.modules["arguments"] = translator_arguments + spec.loader.exec_module(translator_arguments) # We create a new parser that will handle the autocompletion # for the translator translator_parser = argparse.ArgumentParser() - tranlator_arguments.add_options(translator_parser) - if not last_option_was_mode_switch: - translator_parser.add_argument("--search-options", action="store_true") - argcomplete.autocomplete(translator_parser) + translator_arguments.add_options(translator_parser) + + class CustomInputFinder(argcomplete.finders.CompletionFinder): + def get_completions(self, comp_line, comp_point): + cword_prequote, cword_prefix, _, comp_words, last_wordbreak_pos = argcomplete.lexers.split_line(comp_line, comp_point) + return self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) + + translator_finder = CustomInputFinder(translator_parser) + completions += translator_finder.get_completions(comp_line, comp_point) if has_only_filename_options and len(parsed_args.filenames) < 2: file_completer = argcomplete.FilesCompleter() From 9bda42c9d9dca6e741a78d8e58c8418fdd6374c6 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Wed, 17 Jul 2024 22:43:23 +0200 Subject: [PATCH 15/44] extract functions for forwarding calls and fix bugs --- driver/tab_completion.py | 77 ++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 7cdbf389f9..94e648d0ed 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -4,8 +4,6 @@ import subprocess import sys -from argcomplete.completers import FilesCompleter - from . import util try: @@ -58,47 +56,23 @@ def complete_planner_args(prefix, parsed_args, **kwargs): completions.append("--") if parsed_args.filenames or double_dash_in_options: + bin_dir = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" if current_mode == "search": if not last_option_was_mode_switch: completions.append("--translate-options") - downward = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "downward" + downward = bin_dir / "downward" if downward.exists(): - simulated_commandline = [str(downward)] + parsed_args.search_options + [prefix] - comp_line = " ".join(simulated_commandline) - comp_point = str(len(comp_line)) - comp_cword = str(len(simulated_commandline) - 1) - cmd = [str(downward), "--bash-complete", - comp_point, comp_line, comp_cword] + simulated_commandline - output = subprocess.check_output(cmd, text=True) - completions += output.split() + completions += get_completions_from_downward( + downward, parsed_args.search_options, prefix) else: if not last_option_was_mode_switch: completions.append("--search-options") - tranlator_arguments_path = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" / "translate" / "arguments.py" - if tranlator_arguments_path.exists(): - simulated_commandline = [str(tranlator_arguments_path)] + parsed_args.translate_options + [prefix] - comp_line = " ".join(simulated_commandline) - comp_point = str(len(comp_line)) - - spec = importlib.util.spec_from_file_location("arguments", tranlator_arguments_path) - translator_arguments = importlib.util.module_from_spec(spec) - sys.modules["arguments"] = translator_arguments - spec.loader.exec_module(translator_arguments) - - # We create a new parser that will handle the autocompletion - # for the translator - translator_parser = argparse.ArgumentParser() - translator_arguments.add_options(translator_parser) - - class CustomInputFinder(argcomplete.finders.CompletionFinder): - def get_completions(self, comp_line, comp_point): - cword_prequote, cword_prefix, _, comp_words, last_wordbreak_pos = argcomplete.lexers.split_line(comp_line, comp_point) - return self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) - - translator_finder = CustomInputFinder(translator_parser) - completions += translator_finder.get_completions(comp_line, comp_point) + translator = bin_dir / "translate" / "translate.py" + if translator.exists(): + completions += get_completions_from_translator( + translator, parsed_args.translate_options, prefix) if has_only_filename_options and len(parsed_args.filenames) < 2: file_completer = argcomplete.FilesCompleter() @@ -107,6 +81,41 @@ def get_completions(self, comp_line, comp_point): return completions +def get_completions_from_downward(downward, options, prefix): + simulated_commandline = [str(downward)] + options + [prefix] + comp_line = " ".join(simulated_commandline) + comp_point = str(len(comp_line)) + comp_cword = str(len(simulated_commandline) - 1) + cmd = [str(downward), "--bash-complete", + comp_point, comp_line, comp_cword] + simulated_commandline + output = subprocess.check_output(cmd, text=True) + return output.split() + +def get_completions_from_translator(translator, options, prefix): + simulated_commandline = [str(translator)] + options + [prefix] + comp_line = " ".join(simulated_commandline) + comp_point = len(comp_line) + + translator_argument_path = translator.parent / "arguments.py" + spec = importlib.util.spec_from_file_location("arguments", translator_argument_path) + translator_arguments = importlib.util.module_from_spec(spec) + sys.modules["arguments"] = translator_arguments + spec.loader.exec_module(translator_arguments) + + # We create a new parser that will handle the autocompletion + # for the translator + translator_parser = argparse.ArgumentParser() + translator_arguments.add_options(translator_parser) + + class CustomInputFinder(argcomplete.finders.CompletionFinder): + def get_completions(self, comp_line, comp_point): + cword_prequote, cword_prefix, _, comp_words, last_wordbreak_pos = argcomplete.lexers.split_line(comp_line, comp_point) + return self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) + + translator_finder = CustomInputFinder(translator_parser) + return translator_finder.get_completions(comp_line, comp_point) + + def enable(parser): if HAS_ARGCOMPLETE: argcomplete.autocomplete(parser) From 91048dae3e178d16a17cac21cdf98e555b3cbec1 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 09:24:11 +0200 Subject: [PATCH 16/44] added file completer to search component for --internal-plan-file option --- driver/tab_completion.py | 5 +++-- src/search/command_line.cc | 30 ++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 94e648d0ed..bc73c5972e 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -61,7 +61,7 @@ def complete_planner_args(prefix, parsed_args, **kwargs): if not last_option_was_mode_switch: completions.append("--translate-options") - downward = bin_dir / "downward" + downward = bin_dir / "downward" if downward.exists(): completions += get_completions_from_downward( downward, parsed_args.search_options, prefix) @@ -87,10 +87,11 @@ def get_completions_from_downward(downward, options, prefix): comp_point = str(len(comp_line)) comp_cword = str(len(simulated_commandline) - 1) cmd = [str(downward), "--bash-complete", - comp_point, comp_line, comp_cword] + simulated_commandline + comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) return output.split() + def get_completions_from_translator(translator, options, prefix): simulated_commandline = [str(translator)] + options + [prefix] comp_line = " ".join(simulated_commandline) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index a7f9777e54..e462f12998 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -12,6 +12,7 @@ #include "utils/strings.h" #include +#include #include #include @@ -197,7 +198,32 @@ static vector complete_args( suggestions.push_back(feature->get_key()); } } else if (last_arg == "--internal-plan-file") { - // suggest filename starting with current_word + // Suggest filename starting with current_word + + // Split into directory and file_prefix + string directory = "."; + string file_prefix = current_word; + auto last_slash_pos = current_word.find_last_of("/\\"); + if (last_slash_pos != string::npos) { + directory = current_word.substr(0, last_slash_pos); + file_prefix = current_word.substr(last_slash_pos + 1); + } + + // Add file and directory names to suggestions + for (const auto& entry : filesystem::directory_iterator(directory)) { + string path = entry.path().string(); + + // Append slash to directories + if (entry.is_directory()) { + path += "/"; + } + + // Remove "./" prefix when not present in prefix + if (last_slash_pos == string::npos && directory == "." && path.starts_with("./")) { + path = path.substr(2); + } + suggestions.push_back(path); + } } else if (last_arg == "--internal-previous-portfolio-plans") { // no suggestions, integer expected } else if (last_arg == "--search") { @@ -244,7 +270,7 @@ void handle_tab_completion(int argc, const char **argv) { "$COMP_LINE is the current command line.\n" "$COMP_CWORD is an index into ${COMP_WORDS} of the word under the cursor.\n" "$COMP_WORDS is the current command line split into words.\n" - ); + ); } int cursor_pos = parse_int_arg("COMP_POINT", static_cast(argv[2])); const string command_line = static_cast(argv[3]); From 05aa478dff64a356ac7387010c689cf2dddf67f5 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 09:39:16 +0200 Subject: [PATCH 17/44] Make path handling OS-agnostic by using std::filesystem::path::preferred_separator --- src/search/command_line.cc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index e462f12998..a985ce4604 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -210,17 +210,18 @@ static vector complete_args( } // Add file and directory names to suggestions - for (const auto& entry : filesystem::directory_iterator(directory)) { + for (const auto &entry : filesystem::directory_iterator(directory)) { string path = entry.path().string(); - // Append slash to directories + // Append preferred separator ("/" or "\") to directories base on + // operating system if (entry.is_directory()) { - path += "/"; + path += filesystem::path::preferred_separator; } - + // Remove "./" prefix when not present in prefix - if (last_slash_pos == string::npos && directory == "." && path.starts_with("./")) { - path = path.substr(2); + if (last_slash_pos == string::npos && directory == "." && path.starts_with("/\\")) { + path = path.substr(2); } suggestions.push_back(path); } From b37280b2b2704bc55e51154be301e3b6adbb538a Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 10:10:44 +0200 Subject: [PATCH 18/44] file completion for search component --- src/search/command_line.cc | 63 ++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index a985ce4604..65f49506fe 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -185,9 +185,43 @@ shared_ptr parse_cmd_line( return parse_cmd_line_aux(args); } +static void complete_filename(const string &prefix, vector &suggestions) { + // Split into directory and file_prefix + string partial_filename = ""; + filesystem::path prefix_path(prefix); + if (!filesystem::is_directory(prefix_path)) { + partial_filename = prefix_path.filename(); + prefix_path = prefix_path.parent_path(); + } + + vector path_suggestions; + if (prefix_path.empty()) { + for (const auto &entry : filesystem::directory_iterator(filesystem::current_path())) { + path_suggestions.push_back(entry.path().filename()); + } + } else if (filesystem::is_directory(prefix_path)) { + for (const auto &entry : filesystem::directory_iterator(prefix_path)) { + path_suggestions.push_back(entry.path()); + } + } + + for (const filesystem::path &path : path_suggestions) { + string suggestion = path.string(); + if (filesystem::is_directory(path)) { + /* + Append preferred separator ("/" or "\") to directories based on + operating system. + */ + suggestion += filesystem::path::preferred_separator; + } + suggestions.push_back(suggestion); + } +} + static vector complete_args( const vector &parsed_args, const string ¤t_word, - int /*cursor_pos*/) { + int cursor_pos) { + string prefix = current_word.substr(0, cursor_pos); assert(!parsed_args.empty()); // args[0] is always the program name. const string &last_arg = parsed_args.back(); vector suggestions; @@ -199,32 +233,7 @@ static vector complete_args( } } else if (last_arg == "--internal-plan-file") { // Suggest filename starting with current_word - - // Split into directory and file_prefix - string directory = "."; - string file_prefix = current_word; - auto last_slash_pos = current_word.find_last_of("/\\"); - if (last_slash_pos != string::npos) { - directory = current_word.substr(0, last_slash_pos); - file_prefix = current_word.substr(last_slash_pos + 1); - } - - // Add file and directory names to suggestions - for (const auto &entry : filesystem::directory_iterator(directory)) { - string path = entry.path().string(); - - // Append preferred separator ("/" or "\") to directories base on - // operating system - if (entry.is_directory()) { - path += filesystem::path::preferred_separator; - } - - // Remove "./" prefix when not present in prefix - if (last_slash_pos == string::npos && directory == "." && path.starts_with("/\\")) { - path = path.substr(2); - } - suggestions.push_back(path); - } + complete_filename(prefix, suggestions); } else if (last_arg == "--internal-previous-portfolio-plans") { // no suggestions, integer expected } else if (last_arg == "--search") { From 43cf4d794076b69f2dbf1c2d023935b63c042045 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 10:52:41 +0200 Subject: [PATCH 19/44] tab completion docu --- README.md | 3 +++ TABCOMPLETION.md | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 TABCOMPLETION.md diff --git a/README.md b/README.md index eacf770968..70aa412d30 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ and on macOS we do not test LP solvers (yet). See [BUILD.md](BUILD.md). +## Tab completion + +See [TABCOMPLETION.md](TABCOMPLETION.md). ## Contributors diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md new file mode 100644 index 0000000000..9aeb249ab7 --- /dev/null +++ b/TABCOMPLETION.md @@ -0,0 +1,26 @@ +## Completion for Fast Downward + +We support tab completion for bash and zsh based on the python package [argcomplete](https://pypi.org/project/argcomplete/) which can be installed via apt + +```bash +sudo apt install python3-argcomplete +``` + +or pip + +```bash +pip install argcomplete +``` + +We recommand to globally enable tab completion for argcomplete as follows. This will enable argcomplete for all python programs which use argcomplete. You can find local alternatives in the [argcomplete documentation](https://pypi.org/project/argcomplete/#activating-global-completion). + +```bash +activate-global-python-argcomplete +``` + +Restart your shell afterwards. + +## Completion for Search Component + + + From a1f8b80301bd00d6901c1578620f720f69186f9f Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 11:00:16 +0200 Subject: [PATCH 20/44] removed unncessary variable --- src/search/command_line.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index 65f49506fe..c8feec9ed8 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -187,10 +187,8 @@ shared_ptr parse_cmd_line( static void complete_filename(const string &prefix, vector &suggestions) { // Split into directory and file_prefix - string partial_filename = ""; filesystem::path prefix_path(prefix); if (!filesystem::is_directory(prefix_path)) { - partial_filename = prefix_path.filename(); prefix_path = prefix_path.parent_path(); } From b5826a8db50e13fac87800ceb13237b712e89531 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 11:55:00 +0200 Subject: [PATCH 21/44] missing instructions for tab completion --- TABCOMPLETION.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index 9aeb249ab7..8984d468c3 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -20,7 +20,15 @@ activate-global-python-argcomplete Restart your shell afterwards. -## Completion for Search Component - +## Completion for search component +You can also enable tab completion for the search binary by adding the following to your `.bashrc` or `.zshrc`. +```bash +function _downward_complete() { + local IFS=$'\n' + COMPREPLY=( $( "$1" --bash-complete \ + "$COMP_POINT" "$COMP_LINE" "$COMP_CWORD" ${COMP_WORDS[@]})) +} +complete -o nosort -F _downward_complete downward +``` From 57e0f4c4406f2613461f9e740c41921f817a00cd Mon Sep 17 00:00:00 2001 From: speckdavid Date: Thu, 18 Jul 2024 12:33:55 +0200 Subject: [PATCH 22/44] removed file completer in search component and rely on a default bash completion for it. fixed issue with cursor on last word --- TABCOMPLETION.md | 2 +- src/search/command_line.cc | 43 +++++++------------------------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index 8984d468c3..2d52d00be5 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -30,5 +30,5 @@ function _downward_complete() { COMPREPLY=( $( "$1" --bash-complete \ "$COMP_POINT" "$COMP_LINE" "$COMP_CWORD" ${COMP_WORDS[@]})) } -complete -o nosort -F _downward_complete downward +complete -o nosort -o default -o bashdefault -F _downward_complete downward ``` diff --git a/src/search/command_line.cc b/src/search/command_line.cc index c8feec9ed8..8a6188b780 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -185,37 +185,6 @@ shared_ptr parse_cmd_line( return parse_cmd_line_aux(args); } -static void complete_filename(const string &prefix, vector &suggestions) { - // Split into directory and file_prefix - filesystem::path prefix_path(prefix); - if (!filesystem::is_directory(prefix_path)) { - prefix_path = prefix_path.parent_path(); - } - - vector path_suggestions; - if (prefix_path.empty()) { - for (const auto &entry : filesystem::directory_iterator(filesystem::current_path())) { - path_suggestions.push_back(entry.path().filename()); - } - } else if (filesystem::is_directory(prefix_path)) { - for (const auto &entry : filesystem::directory_iterator(prefix_path)) { - path_suggestions.push_back(entry.path()); - } - } - - for (const filesystem::path &path : path_suggestions) { - string suggestion = path.string(); - if (filesystem::is_directory(path)) { - /* - Append preferred separator ("/" or "\") to directories based on - operating system. - */ - suggestion += filesystem::path::preferred_separator; - } - suggestions.push_back(suggestion); - } -} - static vector complete_args( const vector &parsed_args, const string ¤t_word, int cursor_pos) { @@ -230,10 +199,12 @@ static vector complete_args( suggestions.push_back(feature->get_key()); } } else if (last_arg == "--internal-plan-file") { - // Suggest filename starting with current_word - complete_filename(prefix, suggestions); + /* Suggest filename starting with current_word. + Handeled by default bash completion. */ + exit(1); } else if (last_arg == "--internal-previous-portfolio-plans") { - // no suggestions, integer expected + /* We want no suggestions and expect an integer + but we cannot avoid the default bash completion. */ } else if (last_arg == "--search") { // suggestions in search string based on current_word plugins::Registry registry = plugins::RawRegistry::instance()->construct_registry(); @@ -284,10 +255,12 @@ void handle_tab_completion(int argc, const char **argv) { const string command_line = static_cast(argv[3]); int cursor_word_index = parse_int_arg("COMP_CWORD", static_cast(argv[4])); vector words; - words.reserve(argc - 5); + words.reserve(argc - 4); for (int i = 5; i < argc; ++i) { words.emplace_back(argv[i]); } + // Sentinal for cases where the cursor is after the last word. + words.push_back(""); vector parsed_args; string current_word; From 60111339e1ee53fa149031fcd2a6889a7511ddfb Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Thu, 18 Jul 2024 18:01:06 +0200 Subject: [PATCH 23/44] enable tab completion in build.py --- build.py | 127 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/build.py b/build.py index c85f776cfe..5f266ed013 100755 --- a/build.py +++ b/build.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK +import argparse import errno import os import subprocess @@ -7,6 +9,12 @@ import build_configs +try: + import argcomplete + HAS_ARGCOMPLETE = True +except ImportError: + HAS_ARGCOMPLETE = False + CONFIGS = {config: params for config, params in build_configs.__dict__.items() if not config.startswith("_")} DEFAULT_CONFIG_NAME = CONFIGS.pop("DEFAULT") @@ -24,7 +32,27 @@ # Number of available CPUs as a fall-back (may be None) NUM_CPUS = os.cpu_count() -def print_usage(): + +class RawHelpFormatter(argparse.HelpFormatter): + """Preserve newlines and spacing.""" + def _fill_text(self, text, width, indent): + return ''.join([indent + line for line in text.splitlines(True)]) + + def _format_args(self, action, default_metavar): + """Show explicit help for remaining args instead of "...".""" + if action.nargs == argparse.REMAINDER: + return "[BUILD ...] [CMAKE_OPTION ...]" + else: + return argparse.HelpFormatter._format_args(self, action, default_metavar) + + +def parse_args(): + description = f"""Build one or more predefined build configurations of Fast Downward. Each build +uses CMake to compile the code. Build configurations differ in the parameters +they pass to CMake. By default, the build uses all available cores if this +number can be determined. Use the "-j" option for CMake to override this +default behaviour. +""" script_name = os.path.basename(__file__) configs = [] for name, args in sorted(CONFIGS.items()): @@ -34,37 +62,37 @@ def print_usage(): name += " (default with --debug)" configs.append(name + "\n " + " ".join(args)) configs_string = "\n ".join(configs) - cmake_name = os.path.basename(CMAKE) - generator_name = CMAKE_GENERATOR.lower() - default_config_name = DEFAULT_CONFIG_NAME - debug_config_name = DEBUG_CONFIG_NAME - print(f"""Usage: {script_name} [BUILD [BUILD ...]] [--all] [--debug] [MAKE_OPTIONS] - -Build one or more predefined build configurations of Fast Downward. Each build -uses {cmake_name} to compile the code using {generator_name} . Build configurations -differ in the parameters they pass to {cmake_name}. By default, the build uses all -available cores if this number can be determined. Use the "-j" option for -{cmake_name} to override this default behaviour. - -Build configurations + epilog = f"""build configurations: {configs_string} ---all Alias to build all build configurations. ---debug Alias to build the default debug build configuration. ---help Print this message and exit. - -Make options - All other parameters are forwarded to the build step. - -Example usage: - ./{script_name} # build {default_config_name} in #cores threads - ./{script_name} -j4 # build {default_config_name} in 4 threads - ./{script_name} debug # build debug - ./{script_name} --debug # build {debug_config_name} - ./{script_name} release debug # build release and debug configs - ./{script_name} --all VERBOSE=true # build all build configs with detailed logs -""") - +example usage: + {script_name} # build {DEFAULT_CONFIG_NAME} with #cores parallel processes + {script_name} -j4 # build {DEFAULT_CONFIG_NAME} with 4 parallel processes + {script_name} debug # build debug + {script_name} --debug # build {DEBUG_CONFIG_NAME} + {script_name} release debug # build release and debug configs + {script_name} --all VERBOSE=true # build all build configs with detailed logs +""" + + parser = argparse.ArgumentParser( + description=description, epilog=epilog, formatter_class=RawHelpFormatter) + parser.add_argument("--debug", action="store_true", + help="alias to build the default debug build configuration") + parser.add_argument("--all", action="store_true", + help="alias to build all build configurations") + remaining_args = parser.add_argument("arguments", nargs=argparse.REMAINDER, + help="build configurations (see below) or build options") + remaining_args.completer = complete_arguments + + if HAS_ARGCOMPLETE: + argcomplete.autocomplete(parser) + args = parser.parse_args() + return args + +def complete_arguments(prefix, parsed_args, **kwargs): + split_args(parsed_args) + unused_configs = set(CONFIGS) - set(parsed_args.config_names) + return sorted([c for c in unused_configs if c.startswith(prefix)]) def get_project_root_path(): import __main__ @@ -114,25 +142,28 @@ def build(config_name, configure_parameters, build_parameters): print(f"Built configuration {config_name} successfully.") -def main(): - config_names = [] - build_parameters = [] - for arg in sys.argv[1:]: - if arg == "--help" or arg == "-h": - print_usage() - sys.exit(0) - elif arg == "--debug": - config_names.append(DEBUG_CONFIG_NAME) - elif arg == "--all": - config_names.extend(sorted(CONFIGS.keys())) - elif arg in CONFIGS: - config_names.append(arg) +def split_args(args): + args.config_names = [] + args.build_parameters = [] + for arg in args.arguments: + if arg in CONFIGS: + args.config_names.append(arg) else: - build_parameters.append(arg) - if not config_names: - config_names.append(DEFAULT_CONFIG_NAME) - for config_name in config_names: - build(config_name, CONFIGS[config_name], build_parameters) + args.build_parameters.append(arg) + + if args.debug: + args.config_names.append(DEBUG_CONFIG_NAME) + if args.all: + args.config_names.extend(sorted(CONFIGS.keys())) + if not args.config_names: + args.config_names.append(DEFAULT_CONFIG_NAME) + + +def main(): + args = parse_args() + split_args(args) + for config_name in args.config_names: + build(config_name, CONFIGS[config_name], args.build_parameters) if __name__ == "__main__": From 228a1f2e156c3e0620884d95509eb320fa562a23 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Fri, 19 Jul 2024 10:13:01 +0200 Subject: [PATCH 24/44] remove redundant import --- src/search/command_line.cc | 1 - src/search/command_line.h | 1 - 2 files changed, 2 deletions(-) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index 8a6188b780..2c9d0fd031 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -12,7 +12,6 @@ #include "utils/strings.h" #include -#include #include #include diff --git a/src/search/command_line.h b/src/search/command_line.h index 0a84f70d46..56c60285c7 100644 --- a/src/search/command_line.h +++ b/src/search/command_line.h @@ -3,7 +3,6 @@ #include #include -#include class SearchAlgorithm; From 0aec46441abc9c431ef5d2918d2ae203547bb773 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Fri, 19 Jul 2024 10:13:28 +0200 Subject: [PATCH 25/44] handle default differently from aliases --- build.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 5f266ed013..1e6d20fee6 100755 --- a/build.py +++ b/build.py @@ -155,15 +155,14 @@ def split_args(args): args.config_names.append(DEBUG_CONFIG_NAME) if args.all: args.config_names.extend(sorted(CONFIGS.keys())) - if not args.config_names: - args.config_names.append(DEFAULT_CONFIG_NAME) def main(): args = parse_args() split_args(args) for config_name in args.config_names: - build(config_name, CONFIGS[config_name], args.build_parameters) + build(config_name, CONFIGS[config_name], + args.build_parameters or [DEFAULT_CONFIG_NAME]) if __name__ == "__main__": From e7151b0c3b83e049d7a29097c0309ab2dd297566 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Fri, 19 Jul 2024 10:58:02 +0200 Subject: [PATCH 26/44] improved tab completion setup instructions --- TABCOMPLETION.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index 2d52d00be5..3e7d09ba6e 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -1,4 +1,4 @@ -## Completion for Fast Downward +## Tab completion for Fast Downward We support tab completion for bash and zsh based on the python package [argcomplete](https://pypi.org/project/argcomplete/) which can be installed via apt @@ -12,19 +12,13 @@ or pip pip install argcomplete ``` -We recommand to globally enable tab completion for argcomplete as follows. This will enable argcomplete for all python programs which use argcomplete. You can find local alternatives in the [argcomplete documentation](https://pypi.org/project/argcomplete/#activating-global-completion). +After the installation, add the following commands to your `.bashrc` or `.zshrc`. Depending on your installation replace `register-python-argcomplete` with `register-python-argcomplete3`. -```bash -activate-global-python-argcomplete ``` +eval "$(register-python-argcomplete fast-downward.py)" +eval "$(register-python-argcomplete build.py)" +eval "$(register-python-argcomplete translate.py)" -Restart your shell afterwards. - -## Completion for search component - -You can also enable tab completion for the search binary by adding the following to your `.bashrc` or `.zshrc`. - -```bash function _downward_complete() { local IFS=$'\n' COMPREPLY=( $( "$1" --bash-complete \ @@ -32,3 +26,5 @@ function _downward_complete() { } complete -o nosort -o default -o bashdefault -F _downward_complete downward ``` + +Restart your shell afterwards. From bed863b544a9727df6b438a073f822133f63abef Mon Sep 17 00:00:00 2001 From: speckdavid Date: Fri, 19 Jul 2024 11:10:36 +0200 Subject: [PATCH 27/44] fixed build script. no search configuration suggestions --- build.py | 10 +++++++--- src/search/command_line.cc | 8 +++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/build.py b/build.py index 1e6d20fee6..16feea558b 100755 --- a/build.py +++ b/build.py @@ -47,7 +47,7 @@ def _format_args(self, action, default_metavar): def parse_args(): - description = f"""Build one or more predefined build configurations of Fast Downward. Each build + description = """Build one or more predefined build configurations of Fast Downward. Each build uses CMake to compile the code. Build configurations differ in the parameters they pass to CMake. By default, the build uses all available cores if this number can be determined. Use the "-j" option for CMake to override this @@ -89,11 +89,13 @@ def parse_args(): args = parser.parse_args() return args + def complete_arguments(prefix, parsed_args, **kwargs): split_args(parsed_args) unused_configs = set(CONFIGS) - set(parsed_args.config_names) return sorted([c for c in unused_configs if c.startswith(prefix)]) + def get_project_root_path(): import __main__ return os.path.dirname(__main__.__file__) @@ -110,6 +112,7 @@ def get_src_path(): def get_build_path(config_name): return os.path.join(get_builds_path(), config_name) + def try_run(cmd): print(f'Executing command "{" ".join(cmd)}"') try: @@ -122,6 +125,7 @@ def try_run(cmd): else: raise + def build(config_name, configure_parameters, build_parameters): print(f"Building configuration {config_name}.") @@ -160,9 +164,9 @@ def split_args(args): def main(): args = parse_args() split_args(args) - for config_name in args.config_names: + for config_name in args.config_names or [DEFAULT_CONFIG_NAME]: build(config_name, CONFIGS[config_name], - args.build_parameters or [DEFAULT_CONFIG_NAME]) + args.build_parameters) if __name__ == "__main__": diff --git a/src/search/command_line.cc b/src/search/command_line.cc index 2c9d0fd031..a714f78081 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -205,11 +205,9 @@ static vector complete_args( /* We want no suggestions and expect an integer but we cannot avoid the default bash completion. */ } else if (last_arg == "--search") { - // suggestions in search string based on current_word - plugins::Registry registry = plugins::RawRegistry::instance()->construct_registry(); - for (const shared_ptr &feature : registry.get_features()) { - suggestions.push_back(feature->get_key() + "("); - } + /* Return suggestions for the search string based on current_word. + Not implemented at the moment. */ + exit(1); } else { // not completing an argument suggestions.push_back("--help"); From 39a5d35cc6b172ea3d79b8423fff8eb59fa1876e Mon Sep 17 00:00:00 2001 From: speckdavid Date: Fri, 19 Jul 2024 11:20:44 +0200 Subject: [PATCH 28/44] fixed issue with remaining args related to '-j' options --- build.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 16feea558b..a68cc99434 100755 --- a/build.py +++ b/build.py @@ -86,7 +86,12 @@ def parse_args(): if HAS_ARGCOMPLETE: argcomplete.autocomplete(parser) - args = parser.parse_args() + args, unparsed_args = parser.parse_known_args() + + if hasattr(args, "remaining_args"): + args.remaining_args += unparsed_args + else: + args.remaining_args = unparsed_args return args @@ -152,7 +157,7 @@ def split_args(args): for arg in args.arguments: if arg in CONFIGS: args.config_names.append(arg) - else: + elif arg != "--": args.build_parameters.append(arg) if args.debug: From 962d2d0338ba497275cac67f66058727d85c2f60 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Fri, 19 Jul 2024 12:02:45 +0200 Subject: [PATCH 29/44] imporved tab completion --- TABCOMPLETION.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index 3e7d09ba6e..185e1ca8dc 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -1,20 +1,14 @@ ## Tab completion for Fast Downward -We support tab completion for bash and zsh based on the python package [argcomplete](https://pypi.org/project/argcomplete/) which can be installed via apt +We support tab completion for bash and zsh based on the python package [argcomplete](https://pypi.org/project/argcomplete/). For full support, use at least version 3.3 which can be installed via `pip`. ```bash -sudo apt install python3-argcomplete -``` - -or pip - -```bash -pip install argcomplete +pip install argcomplete>=3.3 ``` After the installation, add the following commands to your `.bashrc` or `.zshrc`. Depending on your installation replace `register-python-argcomplete` with `register-python-argcomplete3`. -``` +```bash eval "$(register-python-argcomplete fast-downward.py)" eval "$(register-python-argcomplete build.py)" eval "$(register-python-argcomplete translate.py)" From 3ba6f8432985e147ccd5faf19caf759d80097a8a Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Sat, 20 Jul 2024 10:37:32 +0200 Subject: [PATCH 30/44] code review of build.py --- build.py | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/build.py b/build.py index a68cc99434..b7852e6a3e 100755 --- a/build.py +++ b/build.py @@ -4,6 +4,7 @@ import argparse import errno import os +from pathlib import Path import subprocess import sys @@ -15,6 +16,9 @@ except ImportError: HAS_ARGCOMPLETE = False +PROJECT_ROOT_PATH = Path(__file__).parent +SRC_PATH = PROJECT_ROOT_PATH / "src" + CONFIGS = {config: params for config, params in build_configs.__dict__.items() if not config.startswith("_")} DEFAULT_CONFIG_NAME = CONFIGS.pop("DEFAULT") @@ -36,7 +40,7 @@ class RawHelpFormatter(argparse.HelpFormatter): """Preserve newlines and spacing.""" def _fill_text(self, text, width, indent): - return ''.join([indent + line for line in text.splitlines(True)]) + return "".join([indent + line for line in text.splitlines(True)]) def _format_args(self, action, default_metavar): """Show explicit help for remaining args instead of "...".""" @@ -86,40 +90,30 @@ def parse_args(): if HAS_ARGCOMPLETE: argcomplete.autocomplete(parser) + + # With calls like 'build.py -j4' the parser would try to interpret '-j4' as + # an option and fail to parse. We use parse_known_args to parse everything + # we recognize and add everything else to the list of arguments. args, unparsed_args = parser.parse_known_args() + args.arguments = unparsed_args + args.arguments - if hasattr(args, "remaining_args"): - args.remaining_args += unparsed_args - else: - args.remaining_args = unparsed_args return args def complete_arguments(prefix, parsed_args, **kwargs): + # This will modify parsed_args in place. This is not a problem because the + # process will stop after generating suggestions for the tab completion. split_args(parsed_args) unused_configs = set(CONFIGS) - set(parsed_args.config_names) return sorted([c for c in unused_configs if c.startswith(prefix)]) -def get_project_root_path(): - import __main__ - return os.path.dirname(__main__.__file__) - - -def get_builds_path(): - return os.path.join(get_project_root_path(), "builds") - - -def get_src_path(): - return os.path.join(get_project_root_path(), "src") - - def get_build_path(config_name): - return os.path.join(get_builds_path(), config_name) + return PROJECT_ROOT_PATH / "builds" / config_name def try_run(cmd): - print(f'Executing command "{" ".join(cmd)}"') + print(f"Executing command '{" ".join(cmd)}'") try: subprocess.check_call(cmd) except OSError as exc: @@ -135,7 +129,7 @@ def build(config_name, configure_parameters, build_parameters): print(f"Building configuration {config_name}.") build_path = get_build_path(config_name) - generator_cmd = [CMAKE, "-S", get_src_path(), "-B", build_path] + generator_cmd = [CMAKE, "-S", str(SRC_PATH), "-B", str(build_path)] if CMAKE_GENERATOR: generator_cmd += ["-G", CMAKE_GENERATOR] generator_cmd += configure_parameters From 2276c0b9c169b5f5a1884e0e8e8fa6f86378d1fd Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Mon, 22 Jul 2024 10:58:52 +0200 Subject: [PATCH 31/44] extract warning function and use BUILDS_DIR --- driver/tab_completion.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index bc73c5972e..828420f2e6 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -4,6 +4,7 @@ import subprocess import sys +from . import returncodes from . import util try: @@ -13,22 +14,27 @@ HAS_ARGCOMPLETE = False +def abort_tab_completion(warning): + argcomplete.warn(warning) + exit(returncodes.DRIVER_INPUT_ERROR) + + def complete_build_arg(prefix, parsed_args, **kwargs): if parsed_args.debug: - argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") - exit(1) + abort_tab_completion( + "The option --debug is an alias for --build=debug. Do no specify " + "both --debug and --build.") - builds_folder = Path(util.REPO_ROOT_DIR) / "builds" - if not builds_folder.exists(): - argcomplete.warn("No build exists.") - exit(1) - return [p.name for p in builds_folder.iterdir() if p.is_dir()] + if not Path(util.BUILDS_DIR).exists(): + abort_tab_completion("No build exists.") + return [p.name for p in Path(util.BUILDS_DIR).iterdir() if p.is_dir()] def complete_planner_args(prefix, parsed_args, **kwargs): if parsed_args.build and parsed_args.debug: - argcomplete.warn("The option --debug is an alias for --build=debug. Do no specify both --debug and --build.") - exit(1) + abort_tab_completion( + "The option --debug is an alias for --build=debug. Do no specify " + "both --debug and --build.") build = parsed_args.build if not build: @@ -56,7 +62,7 @@ def complete_planner_args(prefix, parsed_args, **kwargs): completions.append("--") if parsed_args.filenames or double_dash_in_options: - bin_dir = Path(util.REPO_ROOT_DIR) / "builds" / build / "bin" + bin_dir = Path(util.BUILDS_DIR) / build / "bin" if current_mode == "search": if not last_option_was_mode_switch: completions.append("--translate-options") From def3f5a65befd17eee29187aaa4656b2a2b4f084 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Mon, 22 Jul 2024 16:44:13 +0200 Subject: [PATCH 32/44] worked on code clarity and fixed issues --- build.py | 6 +-- driver/tab_completion.py | 2 +- src/search/command_line.cc | 98 ++++++++++++++++++++++---------------- src/search/planner.cc | 2 +- 4 files changed, 63 insertions(+), 45 deletions(-) diff --git a/build.py b/build.py index b7852e6a3e..c43a9891f1 100755 --- a/build.py +++ b/build.py @@ -93,7 +93,7 @@ def parse_args(): # With calls like 'build.py -j4' the parser would try to interpret '-j4' as # an option and fail to parse. We use parse_known_args to parse everything - # we recognize and add everything else to the list of arguments. + # we recognize and add everything else to the list of arguments. args, unparsed_args = parser.parse_known_args() args.arguments = unparsed_args + args.arguments @@ -113,7 +113,7 @@ def get_build_path(config_name): def try_run(cmd): - print(f"Executing command '{" ".join(cmd)}'") + print(f"""Executing command '{" ".join(cmd)}'""") try: subprocess.check_call(cmd) except OSError as exc: @@ -135,7 +135,7 @@ def build(config_name, configure_parameters, build_parameters): generator_cmd += configure_parameters try_run(generator_cmd) - build_cmd = [CMAKE, "--build", build_path] + build_cmd = [CMAKE, "--build", str(build_path)] if NUM_CPUS: build_cmd += ["-j", f"{NUM_CPUS}"] if build_parameters: diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 828420f2e6..f73ad8b4f1 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -95,7 +95,7 @@ def get_completions_from_downward(downward, options, prefix): cmd = [str(downward), "--bash-complete", comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) - return output.split() + return output.splitlines() def get_completions_from_translator(translator, options, prefix): diff --git a/src/search/command_line.cc b/src/search/command_line.cc index a714f78081..8cd9c234e5 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -231,8 +231,52 @@ static vector complete_args( return suggestions; } +static vector get_word_starts(const string &command_line, const vector &words) { + vector word_starts; + word_starts.reserve(words.size()); + int pos = 0; + int end = static_cast(command_line.size()); + for (const string &word : words) { + int word_len = static_cast(word.size()); + if (command_line.substr(pos, word_len) != word) { + input_error("Expected '" + word + "' in command line at position " + + to_string(pos)); + } + word_starts.push_back(pos); + + // Skip word + pos += word_len; + + // Skip whitespace between words. + while (pos < end && isspace(command_line[pos])) { + ++pos; + } + } + return word_starts; +} + +static int get_position_in_current_word( + int cursor_word_index, const string &command_line, + int cursor_pos, const vector &words) { + vector word_starts = get_word_starts(command_line, words); + + assert(utils::in_bounds(cursor_word_index, words)); + const string ¤t_word = words[cursor_word_index]; + int len_current_word = static_cast(current_word.size()); + assert(utils::in_bounds(cursor_word_index, word_starts)); + int current_word_start = word_starts[cursor_word_index]; + int position_in_current_word = cursor_pos - current_word_start; + + if (position_in_current_word < 0 || position_in_current_word > len_current_word) { + input_error("Cursor position out-of-bounds: " + + to_string(current_word_start) + " " + to_string(position_in_current_word) + "."); + } + + return position_in_current_word; +} + void handle_tab_completion(int argc, const char **argv) { - if (argc < 2 || static_cast(argv[1]) != "--bash-complete") { + if (argc < 2 || string(argv[1]) != "--bash-complete") { return; } if (argc < 5) { @@ -248,49 +292,23 @@ void handle_tab_completion(int argc, const char **argv) { "$COMP_WORDS is the current command line split into words.\n" ); } - int cursor_pos = parse_int_arg("COMP_POINT", static_cast(argv[2])); - const string command_line = static_cast(argv[3]); - int cursor_word_index = parse_int_arg("COMP_CWORD", static_cast(argv[4])); - vector words; - words.reserve(argc - 4); - for (int i = 5; i < argc; ++i) { - words.emplace_back(argv[i]); - } - // Sentinal for cases where the cursor is after the last word. + int cursor_pos = parse_int_arg("COMP_POINT", argv[2]); + string command_line(argv[3]); + int cursor_word_index = parse_int_arg("COMP_CWORD", argv[4]); + vector words(&argv[5], &argv[argc]); + // Sentinel for cases where the cursor is after the last word. words.push_back(""); - vector parsed_args; - string current_word; - int pos = 0; - int end = static_cast(command_line.size()); - int num_words = static_cast(words.size()); - for (int i = 0; i < num_words; ++i) { - current_word = words[i]; - int word_len = static_cast(current_word.size()); - if (command_line.substr(pos, word_len) != current_word) { - input_error("Expected '" + current_word + "' in $COMP_LINE at position " - + to_string(pos)); - } - if (i == cursor_word_index) { - if (cursor_pos < 0 || cursor_pos > word_len) { - input_error("Inconsistent information in COMP_LINE and COMP_WORDS"); - } - break; - } - - // Skip word - pos += word_len; - cursor_pos -= word_len; - parsed_args.push_back(current_word); - - // Skip whitespace between words. - while (pos < end && isspace(command_line[pos])) { - ++pos; - --cursor_pos; - } + if (!utils::in_bounds(cursor_word_index, words)) { + input_error("Cursor word index out-of-bounds."); } - for (const string &suggestion : complete_args(parsed_args, current_word, cursor_pos)) { + vector preceding_words(words.begin(), words.begin() + cursor_word_index); + const string ¤t_word = words[cursor_word_index]; + int pos_in_word = get_position_in_current_word( + cursor_word_index, command_line, cursor_pos, words); + + for (const string &suggestion : complete_args(preceding_words, current_word, pos_in_word)) { cout << suggestion << endl; } // Do not use exit_with here because it would generate additional output. diff --git a/src/search/planner.cc b/src/search/planner.cc index 00dd78fbaf..8c2acf0e46 100644 --- a/src/search/planner.cc +++ b/src/search/planner.cc @@ -28,7 +28,7 @@ int main(int argc, const char **argv) { } bool unit_cost = false; - if (static_cast(argv[1]) != "--help") { + if (string(argv[1]) != "--help") { utils::g_log << "reading input..." << endl; tasks::read_root_task(cin); utils::g_log << "done reading input!" << endl; From 5ed1356a1a64a452192189c125724c4868c8017f Mon Sep 17 00:00:00 2001 From: speckdavid Date: Tue, 23 Jul 2024 10:58:56 +0200 Subject: [PATCH 33/44] minor --- src/search/command_line.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index 8cd9c234e5..da626dbbc4 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -268,8 +268,8 @@ static int get_position_in_current_word( int position_in_current_word = cursor_pos - current_word_start; if (position_in_current_word < 0 || position_in_current_word > len_current_word) { - input_error("Cursor position out-of-bounds: " - + to_string(current_word_start) + " " + to_string(position_in_current_word) + "."); + input_error("Cursor position out-of-bounds: " + + to_string(position_in_current_word) + "."); } return position_in_current_word; @@ -300,7 +300,8 @@ void handle_tab_completion(int argc, const char **argv) { words.push_back(""); if (!utils::in_bounds(cursor_word_index, words)) { - input_error("Cursor word index out-of-bounds."); + input_error("Cursor word index out-of-bounds: " + + to_string(cursor_word_index) + "."); } vector preceding_words(words.begin(), words.begin() + cursor_word_index); From 7c167dcb6384820007ac7547a1548d4033da9a69 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Thu, 25 Jul 2024 12:32:49 +0200 Subject: [PATCH 34/44] call translator standalone for tab completion --- driver/tab_completion.py | 83 ++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index f73ad8b4f1..96f2823e2a 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,8 +1,10 @@ import argparse import importlib.util +import os from pathlib import Path import subprocess import sys +import tempfile from . import returncodes from . import util @@ -56,71 +58,86 @@ def complete_planner_args(prefix, parsed_args, **kwargs): has_only_filename_or_double_dash_options = (num_filenames + int(double_dash_in_options) == num_planner_args) can_use_double_dash = (1 <= num_planner_args <= 2) and has_only_filename_or_double_dash_options - completions = [] + completions = {} if can_use_double_dash: - completions.append("--") + completions["--"] = "" if parsed_args.filenames or double_dash_in_options: bin_dir = Path(util.BUILDS_DIR) / build / "bin" if current_mode == "search": if not last_option_was_mode_switch: - completions.append("--translate-options") + completions["--translate-options"] = "" downward = bin_dir / "downward" if downward.exists(): - completions += get_completions_from_downward( - downward, parsed_args.search_options, prefix) + completions.update(_get_completions_from_downward( + downward, parsed_args.search_options, prefix)) else: if not last_option_was_mode_switch: - completions.append("--search-options") + completions["--search-options"] = "" translator = bin_dir / "translate" / "translate.py" if translator.exists(): - completions += get_completions_from_translator( - translator, parsed_args.translate_options, prefix) + completions.update(_get_completions_from_translator( + translator, parsed_args.translate_options, prefix)) if has_only_filename_options and len(parsed_args.filenames) < 2: file_completer = argcomplete.FilesCompleter() - completions += file_completer(prefix, **kwargs) + completions.update({f : "" for f in file_completer(prefix, **kwargs)}) return completions -def get_completions_from_downward(downward, options, prefix): +def _get_field_separators(env): + entry_separator = env.get("IFS", "\n") + help_separator = env.get("_ARGCOMPLETE_DFS") + if env.get("_ARGCOMPLETE_SHELL") == "zsh": + # Argcomplete always uses ":" on zsh, even if another value is set in + # _ARGCOMPLETE_DFS. + help_separator = ":" + return entry_separator, help_separator + + +def _split_argcomplete_ouput(content, entry_separator, help_separator): + suggestions = {} + for line in content.split(entry_separator): + if help_separator and help_separator in line: + suggestion, help = line.split(help_separator, 1) + suggestions[suggestion] = help + else: + suggestions[line] = "" + return suggestions + + +def _call_argcomplete(python_file, comp_line, comp_point): + with tempfile.NamedTemporaryFile(mode="r") as f: + env = os.environ.copy() + env["COMP_LINE"] = comp_line + env["COMP_POINT"] = str(comp_point) + env["_ARGCOMPLETE"] = "1" + env["_ARGCOMPLETE_STDOUT_FILENAME"] = f.name + subprocess.check_call([sys.executable, python_file], env=env) + entry_separator, help_separator = _get_field_separators(env) + return _split_argcomplete_ouput(f.read(), entry_separator, help_separator) + +def _get_completions_from_downward(downward, options, prefix): + # TODO: entry_separator, help_separator = _get_field_separators(os.environ) simulated_commandline = [str(downward)] + options + [prefix] comp_line = " ".join(simulated_commandline) comp_point = str(len(comp_line)) comp_cword = str(len(simulated_commandline) - 1) - cmd = [str(downward), "--bash-complete", + cmd = [str(downward), "--bash-complete", # TODO: "--ifs", entry_separator, "--dfs", help_separator, comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) - return output.splitlines() + return _split_argcomplete_ouput(output, "\n", ":") #TODO: entry_separator, help_separator) -def get_completions_from_translator(translator, options, prefix): - simulated_commandline = [str(translator)] + options + [prefix] +def _get_completions_from_translator(translator, options, prefix): + simulated_commandline = [str(translator), "dummy_domain", "dummy_problem"] + options + [prefix] comp_line = " ".join(simulated_commandline) comp_point = len(comp_line) - - translator_argument_path = translator.parent / "arguments.py" - spec = importlib.util.spec_from_file_location("arguments", translator_argument_path) - translator_arguments = importlib.util.module_from_spec(spec) - sys.modules["arguments"] = translator_arguments - spec.loader.exec_module(translator_arguments) - - # We create a new parser that will handle the autocompletion - # for the translator - translator_parser = argparse.ArgumentParser() - translator_arguments.add_options(translator_parser) - - class CustomInputFinder(argcomplete.finders.CompletionFinder): - def get_completions(self, comp_line, comp_point): - cword_prequote, cword_prefix, _, comp_words, last_wordbreak_pos = argcomplete.lexers.split_line(comp_line, comp_point) - return self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) - - translator_finder = CustomInputFinder(translator_parser) - return translator_finder.get_completions(comp_line, comp_point) + return _call_argcomplete(translator, comp_line, comp_point) def enable(parser): From 5a23b09f484f99d480fc3f541c13b1a19f9b8853 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Thu, 25 Jul 2024 12:46:59 +0200 Subject: [PATCH 35/44] reset translator --- src/translate/arguments.py | 56 ------------------------------ src/translate/options.py | 71 +++++++++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 65 deletions(-) delete mode 100644 src/translate/arguments.py diff --git a/src/translate/arguments.py b/src/translate/arguments.py deleted file mode 100644 index d015b55ef3..0000000000 --- a/src/translate/arguments.py +++ /dev/null @@ -1,56 +0,0 @@ -def add_file_arguments(argparser): - argparser.add_argument( - "domain", help="path to domain pddl file") - argparser.add_argument( - "task", help="path to task pddl file") - -def add_options(argparser): - argparser.add_argument( - "--relaxed", dest="generate_relaxed_task", action="store_true", - help="output relaxed task (no delete effects)") - argparser.add_argument( - "--full-encoding", - dest="use_partial_encoding", action="store_false", - help="By default we represent facts that occur in multiple " - "mutex groups only in one variable. Using this parameter adds " - "these facts to multiple variables. This can make the meaning " - "of the variables clearer, but increases the number of facts.") - argparser.add_argument( - "--invariant-generation-max-candidates", default=100000, type=int, - help="max number of candidates for invariant generation " - "(default: %(default)d). Set to 0 to disable invariant " - "generation and obtain only binary variables. The limit is " - "needed for grounded input files that would otherwise produce " - "too many candidates.") - argparser.add_argument( - "--sas-file", default="output.sas", - help="path to the SAS output file (default: %(default)s)") - argparser.add_argument( - "--invariant-generation-max-time", default=300, type=int, - help="max time for invariant generation (default: %(default)ds)") - argparser.add_argument( - "--add-implied-preconditions", action="store_true", - help="infer additional preconditions. This setting can cause a " - "severe performance penalty due to weaker relevance analysis " - "(see issue7).") - argparser.add_argument( - "--keep-unreachable-facts", - dest="filter_unreachable_facts", action="store_false", - help="keep facts that can't be reached from the initial state") - argparser.add_argument( - "--skip-variable-reordering", - dest="reorder_variables", action="store_false", - help="do not reorder variables based on the causal graph. Do not use " - "this option with the causal graph heuristic!") - argparser.add_argument( - "--keep-unimportant-variables", - dest="filter_unimportant_vars", action="store_false", - help="keep variables that do not influence the goal in the causal graph") - argparser.add_argument( - "--dump-task", action="store_true", - help="dump human-readable SAS+ representation of the task") - argparser.add_argument( - "--layer-strategy", default="min", choices=["min", "max"], - help="How to assign layers to derived variables. 'min' attempts to put as " - "many variables into the same layer as possible, while 'max' puts each variable " - "into its own layer unless it is part of a cycle.") diff --git a/src/translate/options.py b/src/translate/options.py index ed56d08481..3abebdb04a 100644 --- a/src/translate/options.py +++ b/src/translate/options.py @@ -1,7 +1,6 @@ import argparse import sys -import arguments try: import argcomplete @@ -10,6 +9,67 @@ HAS_ARGCOMPLETE = False +def parse_args(): + argparser = argparse.ArgumentParser() + argparser.add_argument( + "domain", help="path to domain pddl file") + argparser.add_argument( + "task", help="path to task pddl file") + argparser.add_argument( + "--relaxed", dest="generate_relaxed_task", action="store_true", + help="output relaxed task (no delete effects)") + argparser.add_argument( + "--full-encoding", + dest="use_partial_encoding", action="store_false", + help="By default we represent facts that occur in multiple " + "mutex groups only in one variable. Using this parameter adds " + "these facts to multiple variables. This can make the meaning " + "of the variables clearer, but increases the number of facts.") + argparser.add_argument( + "--invariant-generation-max-candidates", default=100000, type=int, + help="max number of candidates for invariant generation " + "(default: %(default)d). Set to 0 to disable invariant " + "generation and obtain only binary variables. The limit is " + "needed for grounded input files that would otherwise produce " + "too many candidates.") + argparser.add_argument( + "--sas-file", default="output.sas", + help="path to the SAS output file (default: %(default)s)") + argparser.add_argument( + "--invariant-generation-max-time", default=300, type=int, + help="max time for invariant generation (default: %(default)ds)") + argparser.add_argument( + "--add-implied-preconditions", action="store_true", + help="infer additional preconditions. This setting can cause a " + "severe performance penalty due to weaker relevance analysis " + "(see issue7).") + argparser.add_argument( + "--keep-unreachable-facts", + dest="filter_unreachable_facts", action="store_false", + help="keep facts that can't be reached from the initial state") + argparser.add_argument( + "--skip-variable-reordering", + dest="reorder_variables", action="store_false", + help="do not reorder variables based on the causal graph. Do not use " + "this option with the causal graph heuristic!") + argparser.add_argument( + "--keep-unimportant-variables", + dest="filter_unimportant_vars", action="store_false", + help="keep variables that do not influence the goal in the causal graph") + argparser.add_argument( + "--dump-task", action="store_true", + help="dump human-readable SAS+ representation of the task") + argparser.add_argument( + "--layer-strategy", default="min", choices=["min", "max"], + help="How to assign layers to derived variables. 'min' attempts to put as " + "many variables into the same layer as possible, while 'max' puts each variable " + "into its own layer unless it is part of a cycle.") + + if HAS_ARGCOMPLETE: + argcomplete.autocomplete(argparser) + return argparser.parse_args() + + def copy_args_to_module(args): module_dict = sys.modules[__name__].__dict__ for key, value in vars(args).items(): @@ -17,14 +77,7 @@ def copy_args_to_module(args): def setup(): - argparser = argparse.ArgumentParser() - arguments.add_file_arguments(argparser) - arguments.add_options(argparser) - - if HAS_ARGCOMPLETE: - argcomplete.autocomplete(argparser) - - args = argparser.parse_args() + args = parse_args() copy_args_to_module(args) From 23ae10a6ed8983c5dc50ee491b871d3ca0385bcc Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Thu, 25 Jul 2024 12:50:33 +0200 Subject: [PATCH 36/44] style --- driver/tab_completion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 96f2823e2a..bce36b256e 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -84,7 +84,7 @@ def complete_planner_args(prefix, parsed_args, **kwargs): if has_only_filename_options and len(parsed_args.filenames) < 2: file_completer = argcomplete.FilesCompleter() - completions.update({f : "" for f in file_completer(prefix, **kwargs)}) + completions.update({f: "" for f in file_completer(prefix, **kwargs)}) return completions @@ -130,7 +130,7 @@ def _get_completions_from_downward(downward, options, prefix): cmd = [str(downward), "--bash-complete", # TODO: "--ifs", entry_separator, "--dfs", help_separator, comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) - return _split_argcomplete_ouput(output, "\n", ":") #TODO: entry_separator, help_separator) + return _split_argcomplete_ouput(output, "\n", ":") # TODO: entry_separator, help_separator) def _get_completions_from_translator(translator, options, prefix): From 4f5b8479c28c9495e1587a0affd70afea340c5c0 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Fri, 26 Jul 2024 17:46:01 +0200 Subject: [PATCH 37/44] zsh completion with help --- TABCOMPLETION.md | 28 ++++++++++++++++++--- driver/tab_completion.py | 7 +++--- src/search/command_line.cc | 51 ++++++++++++++++++++++---------------- 3 files changed, 58 insertions(+), 28 deletions(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index 185e1ca8dc..42e4406a73 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -14,11 +14,31 @@ eval "$(register-python-argcomplete build.py)" eval "$(register-python-argcomplete translate.py)" function _downward_complete() { - local IFS=$'\n' - COMPREPLY=( $( "$1" --bash-complete \ - "$COMP_POINT" "$COMP_LINE" "$COMP_CWORD" ${COMP_WORDS[@]})) + local IFS=$'\013' + if [[ -n "${ZSH_VERSION-}" ]]; then + local DFS=":" + local completions + local COMP_CWORD=$((CURRENT - 1)) + completions=( $( "${words[1]}" --bash-complete \ + "$IFS" "$DFS" "$CURSOR" "$BUFFER" "$COMP_CWORD" ${words[@]})) + if [[ $? == 0 ]]; then + _describe "${words[1]}" completions -o nosort + fi + else + local DFS="" + COMPREPLY=( $( "$1" --bash-complete \ + "$IFS" "$DFS" "$COMP_POINT" "$COMP_LINE" "$COMP_CWORD" ${COMP_WORDS[@]})) + if [[ $? != 0 ]]; then + unset COMPREPLY + fi + fi } -complete -o nosort -o default -o bashdefault -F _downward_complete downward + +if [[ -n "${ZSH_VERSION-}" ]]; then + compdef _downward_complete downward +else + complete -o nosort -F _downward_complete downward +fi ``` Restart your shell afterwards. diff --git a/driver/tab_completion.py b/driver/tab_completion.py index bce36b256e..904145afbc 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -122,15 +122,16 @@ def _call_argcomplete(python_file, comp_line, comp_point): return _split_argcomplete_ouput(f.read(), entry_separator, help_separator) def _get_completions_from_downward(downward, options, prefix): - # TODO: entry_separator, help_separator = _get_field_separators(os.environ) + entry_separator, help_separator = _get_field_separators(os.environ) + help_separator = help_separator or ":" simulated_commandline = [str(downward)] + options + [prefix] comp_line = " ".join(simulated_commandline) comp_point = str(len(comp_line)) comp_cword = str(len(simulated_commandline) - 1) - cmd = [str(downward), "--bash-complete", # TODO: "--ifs", entry_separator, "--dfs", help_separator, + cmd = [str(downward), "--bash-complete", entry_separator, help_separator, comp_point, comp_line, comp_cword] + simulated_commandline output = subprocess.check_output(cmd, text=True) - return _split_argcomplete_ouput(output, "\n", ":") # TODO: entry_separator, help_separator) + return _split_argcomplete_ouput(output, entry_separator, help_separator) def _get_completions_from_translator(translator, options, prefix): diff --git a/src/search/command_line.cc b/src/search/command_line.cc index da626dbbc4..b6e3ae641f 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -184,18 +184,18 @@ shared_ptr parse_cmd_line( return parse_cmd_line_aux(args); } -static vector complete_args( +static vector> complete_args( const vector &parsed_args, const string ¤t_word, int cursor_pos) { string prefix = current_word.substr(0, cursor_pos); assert(!parsed_args.empty()); // args[0] is always the program name. const string &last_arg = parsed_args.back(); - vector suggestions; + vector> suggestions; if (find(parsed_args.begin(), parsed_args.end(), "--help") != parsed_args.end()) { - suggestions.push_back("--txt2tags"); + suggestions.emplace_back("--txt2tags", ""); plugins::Registry registry = plugins::RawRegistry::instance()->construct_registry(); for (const shared_ptr &feature : registry.get_features()) { - suggestions.push_back(feature->get_key()); + suggestions.emplace_back(feature->get_key(), feature->get_title()); } } else if (last_arg == "--internal-plan-file") { /* Suggest filename starting with current_word. @@ -210,13 +210,13 @@ static vector complete_args( exit(1); } else { // not completing an argument - suggestions.push_back("--help"); - suggestions.push_back("--search"); - suggestions.push_back("--internal-plan-file"); - suggestions.push_back("--internal-previous-portfolio-plans"); - suggestions.push_back("--if-unit-cost"); - suggestions.push_back("--if-non-unit-cost"); - suggestions.push_back("--always"); + suggestions.emplace_back("--help", ""); + suggestions.emplace_back("--search", ""); + suggestions.emplace_back("--internal-plan-file", ""); + suggestions.emplace_back("--internal-previous-portfolio-plans", ""); + suggestions.emplace_back("--if-unit-cost", ""); + suggestions.emplace_back("--if-non-unit-cost", ""); + suggestions.emplace_back("--always", ""); // remove suggestions not starting with current_word } @@ -224,8 +224,8 @@ static vector complete_args( // Suggest only words that match with current_word suggestions.erase( remove_if(suggestions.begin(), suggestions.end(), - [&](const string &value) { - return !value.starts_with(current_word); + [&](const pair &value) { + return !value.first.starts_with(current_word); }), suggestions.end()); } return suggestions; @@ -279,23 +279,27 @@ void handle_tab_completion(int argc, const char **argv) { if (argc < 2 || string(argv[1]) != "--bash-complete") { return; } - if (argc < 5) { + if (argc < 7) { input_error( "The option --bash-complete is only meant to be called " "internally to generate suggestions for tab completion.\n" - "Usage:\n ./downward --bash-complete " + "Usage:\n ./downward --bash-complete $IFS $DFS\n" "$COMP_POINT \"$COMP_LINE\" $COMP_CWORD ${COMP_WORDS[@]}\n" "where the environment variables have their usual meaning for bash completion:\n" + "$IFS is a character used to separate different suggestions.\n" + "$DFS is a character used within a suggestion to separate the value from its description.\n" "$COMP_POINT is the position of the cursor in the command line.\n" "$COMP_LINE is the current command line.\n" "$COMP_CWORD is an index into ${COMP_WORDS} of the word under the cursor.\n" "$COMP_WORDS is the current command line split into words.\n" ); } - int cursor_pos = parse_int_arg("COMP_POINT", argv[2]); - string command_line(argv[3]); - int cursor_word_index = parse_int_arg("COMP_CWORD", argv[4]); - vector words(&argv[5], &argv[argc]); + string entry_separator(argv[2]); + string help_separator(argv[3]); + int cursor_pos = parse_int_arg("COMP_POINT", argv[4]); + string command_line(argv[5]); + int cursor_word_index = parse_int_arg("COMP_CWORD", argv[6]); + vector words(&argv[7], &argv[argc]); // Sentinel for cases where the cursor is after the last word. words.push_back(""); @@ -309,8 +313,13 @@ void handle_tab_completion(int argc, const char **argv) { int pos_in_word = get_position_in_current_word( cursor_word_index, command_line, cursor_pos, words); - for (const string &suggestion : complete_args(preceding_words, current_word, pos_in_word)) { - cout << suggestion << endl; + for (const auto &[suggestion, description] : complete_args( + preceding_words, current_word, pos_in_word)) { + cout << suggestion; + if (!description.empty() && !help_separator.empty()) { + cout << help_separator << description; + } + cout << entry_separator; } // Do not use exit_with here because it would generate additional output. exit(0); From fc46d8333c41e3d6bfd43a8fec7d911bcd2b53fa Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Fri, 26 Jul 2024 17:55:33 +0200 Subject: [PATCH 38/44] style --- src/search/command_line.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search/command_line.cc b/src/search/command_line.cc index b6e3ae641f..1681975474 100644 --- a/src/search/command_line.cc +++ b/src/search/command_line.cc @@ -314,7 +314,7 @@ void handle_tab_completion(int argc, const char **argv) { cursor_word_index, command_line, cursor_pos, words); for (const auto &[suggestion, description] : complete_args( - preceding_words, current_word, pos_in_word)) { + preceding_words, current_word, pos_in_word)) { cout << suggestion; if (!description.empty() && !help_separator.empty()) { cout << help_separator << description; From 7a270a08e136d401759281471aaff7bccb9d468d Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Mon, 29 Jul 2024 16:00:31 +0200 Subject: [PATCH 39/44] changes from code review --- driver/run_components.py | 43 +++++++++++++++++++++++++++------------- driver/tab_completion.py | 43 ++++++++++------------------------------ driver/tests.py | 4 ++-- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/driver/run_components.py b/driver/run_components.py index bdb5df127d..470f4991f4 100644 --- a/driver/run_components.py +++ b/driver/run_components.py @@ -1,3 +1,4 @@ +import errno import logging import os from pathlib import Path @@ -20,16 +21,24 @@ returncodes.exit_with_driver_unsupported_error("Unsupported OS: " + os.name) # TODO: We might want to turn translate into a module and call it with "python3 -m translate". -REL_TRANSLATE_PATH = Path("translate") / "translate.py" -REL_SEARCH_PATH = Path(f"downward{BINARY_EXT}") +_REL_TRANSLATE_PATH = Path("translate") / "translate.py" +_REL_SEARCH_PATH = Path(f"downward{BINARY_EXT}") # Older versions of VAL use lower case, newer versions upper case. We prefer the # older version because this is what our build instructions recommend. _VALIDATE_NAME = (shutil.which(f"validate{BINARY_EXT}") or shutil.which(f"Validate{BINARY_EXT}")) -VALIDATE = Path(_VALIDATE_NAME) if _VALIDATE_NAME else None +_VALIDATE_PATH = Path(_VALIDATE_NAME) if _VALIDATE_NAME else None -def get_executable(build: str, rel_path: Path): +def get_search_executable(build: str, exit_on_failure=True): + return _get_executable(build, _REL_SEARCH_PATH, exit_on_failure) + + +def get_translate_executable(build: str, exit_on_failure=True): + return _get_executable(build, _REL_TRANSLATE_PATH, exit_on_failure) + + +def _get_executable(build: str, rel_path: Path, exit_on_failure=True): # First, consider 'build' to be a path directly to the binaries. # The path can be absolute or relative to the current working # directory. @@ -41,15 +50,21 @@ def get_executable(build: str, rel_path: Path): # '/builds//bin'. build_dir = util.BUILDS_DIR / build / "bin" if not build_dir.exists(): - returncodes.exit_with_driver_input_error( - f"Could not find build '{build}' at {build_dir}. " - f"Please run './build.py {build}'.") + if exit_on_failure: + returncodes.exit_with_driver_input_error( + f"Could not find build '{build}' at {build_dir}. " + f"Please run './build.py {build}'.") + else: + return None abs_path = build_dir / rel_path if not abs_path.exists(): - returncodes.exit_with_driver_input_error( - f"Could not find '{rel_path}' in build '{build}'. " - f"Please run './build.py {build}'.") + if exit_on_failure: + returncodes.exit_with_driver_input_error( + f"Could not find '{rel_path}' in build '{build}'. " + f"Please run './build.py {build}'.") + else: + return None return abs_path @@ -60,7 +75,7 @@ def run_translate(args): args.translate_time_limit, args.overall_time_limit) memory_limit = limits.get_memory_limit( args.translate_memory_limit, args.overall_memory_limit) - translate = get_executable(args.build, REL_TRANSLATE_PATH) + translate = get_translate_executable(args.build) assert sys.executable, "Path to interpreter could not be found" cmd = [sys.executable] + [translate] + args.translate_inputs + args.translate_options @@ -106,7 +121,7 @@ def run_search(args): args.search_time_limit, args.overall_time_limit) memory_limit = limits.get_memory_limit( args.search_memory_limit, args.overall_memory_limit) - executable = get_executable(args.build, REL_SEARCH_PATH) + executable = get_search_executable(args.build) plan_manager = PlanManager( args.plan_file, @@ -146,7 +161,7 @@ def run_search(args): def run_validate(args): - if not VALIDATE: + if not _VALIDATE_PATH: returncodes.exit_with_driver_input_error( "Error: Trying to run validate but it was not found on the PATH.") @@ -159,7 +174,7 @@ def run_validate(args): try: call.check_call( "validate", - [VALIDATE] + args.validate_inputs + plan_files, + [_VALIDATE_PATH] + args.validate_inputs + plan_files, time_limit=args.validate_time_limit, memory_limit=args.validate_memory_limit) except OSError as err: diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 904145afbc..da45e69745 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,5 +1,3 @@ -import argparse -import importlib.util import os from pathlib import Path import subprocess @@ -8,6 +6,7 @@ from . import returncodes from . import util +from .run_components import get_search_executable, get_translate_executable try: import argcomplete @@ -16,28 +15,13 @@ HAS_ARGCOMPLETE = False -def abort_tab_completion(warning): - argcomplete.warn(warning) - exit(returncodes.DRIVER_INPUT_ERROR) - - def complete_build_arg(prefix, parsed_args, **kwargs): - if parsed_args.debug: - abort_tab_completion( - "The option --debug is an alias for --build=debug. Do no specify " - "both --debug and --build.") - - if not Path(util.BUILDS_DIR).exists(): - abort_tab_completion("No build exists.") - return [p.name for p in Path(util.BUILDS_DIR).iterdir() if p.is_dir()] + if not util.BUILDS_DIR.exists(): + return [] + return [p.name for p in util.BUILDS_DIR.iterdir() if p.is_dir()] def complete_planner_args(prefix, parsed_args, **kwargs): - if parsed_args.build and parsed_args.debug: - abort_tab_completion( - "The option --debug is an alias for --build=debug. Do no specify " - "both --debug and --build.") - build = parsed_args.build if not build: if parsed_args.debug: @@ -48,8 +32,6 @@ def complete_planner_args(prefix, parsed_args, **kwargs): # Get some information from planner_args before it is deleted in split_planner_args(). planner_args = parsed_args.planner_args num_planner_args = len(planner_args) - mode_switches = ["--translate-options", "--search-options"] - last_option_was_mode_switch = planner_args and (planner_args[-1] in mode_switches) double_dash_in_options = "--" in planner_args current_mode = util.split_planner_args(parsed_args) @@ -64,21 +46,18 @@ def complete_planner_args(prefix, parsed_args, **kwargs): completions["--"] = "" if parsed_args.filenames or double_dash_in_options: - bin_dir = Path(util.BUILDS_DIR) / build / "bin" if current_mode == "search": - if not last_option_was_mode_switch: - completions["--translate-options"] = "" + completions["--translate-options"] = "" - downward = bin_dir / "downward" - if downward.exists(): + downward = get_search_executable(build, exit_on_failure=False) + if downward and downward.exists(): completions.update(_get_completions_from_downward( downward, parsed_args.search_options, prefix)) else: - if not last_option_was_mode_switch: - completions["--search-options"] = "" + completions["--search-options"] = "" - translator = bin_dir / "translate" / "translate.py" - if translator.exists(): + translator = get_translate_executable(build, exit_on_failure=False) + if translator and translator.exists(): completions.update(_get_completions_from_translator( translator, parsed_args.translate_options, prefix)) @@ -135,7 +114,7 @@ def _get_completions_from_downward(downward, options, prefix): def _get_completions_from_translator(translator, options, prefix): - simulated_commandline = [str(translator), "dummy_domain", "dummy_problem"] + options + [prefix] + simulated_commandline = [str(translator)] + options + [prefix] comp_line = " ".join(simulated_commandline) comp_point = len(comp_line) return _call_argcomplete(translator, comp_line, comp_point) diff --git a/driver/tests.py b/driver/tests.py index 33f6c61d93..8204bc3f5e 100644 --- a/driver/tests.py +++ b/driver/tests.py @@ -17,7 +17,7 @@ from .call import check_call, _replace_paths_with_strings from . import limits from . import returncodes -from .run_components import get_executable, REL_SEARCH_PATH +from .run_components import get_search_executable from .util import REPO_ROOT_DIR, find_domain_path @@ -89,7 +89,7 @@ def _convert_to_standalone_config(config): def _run_search(config): check_call( "search", - [get_executable("release", REL_SEARCH_PATH)] + list(config), + [get_search_executable("release")] + list(config), stdin="output.sas") From 8564b9110bc2210dc7e05427bc7e803bab1a7487 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Mon, 29 Jul 2024 17:52:48 +0200 Subject: [PATCH 40/44] avoid computing default build in two places. --- driver/arguments.py | 6 +----- driver/tab_completion.py | 11 +++-------- driver/util.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/driver/arguments.py b/driver/arguments.py index 6dbee5a8bc..f5216d608c 100644 --- a/driver/arguments.py +++ b/driver/arguments.py @@ -409,12 +409,8 @@ def parse_args(): print_usage_and_exit_with_driver_input_error( parser, "The option --debug is an alias for --build=debug " "--validate. Do no specify both --debug and --build.") - if not args.build: - if args.debug: - args.build = "debug" - else: - args.build = "release" + util.set_default_build(args) util.split_planner_args(args) _check_mutex_args(parser, [ diff --git a/driver/tab_completion.py b/driver/tab_completion.py index da45e69745..1ba7f7c698 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -22,12 +22,7 @@ def complete_build_arg(prefix, parsed_args, **kwargs): def complete_planner_args(prefix, parsed_args, **kwargs): - build = parsed_args.build - if not build: - if parsed_args.debug: - build = "debug" - else: - build = "release" + util.set_default_build(parsed_args) # Get some information from planner_args before it is deleted in split_planner_args(). planner_args = parsed_args.planner_args @@ -49,14 +44,14 @@ def complete_planner_args(prefix, parsed_args, **kwargs): if current_mode == "search": completions["--translate-options"] = "" - downward = get_search_executable(build, exit_on_failure=False) + downward = get_search_executable(parsed_args.build, exit_on_failure=False) if downward and downward.exists(): completions.update(_get_completions_from_downward( downward, parsed_args.search_options, prefix)) else: completions["--search-options"] = "" - translator = get_translate_executable(build, exit_on_failure=False) + translator = get_translate_executable(parsed_args.build, exit_on_failure=False) if translator and translator.exists(): completions.update(_get_completions_from_translator( translator, parsed_args.translate_options, prefix)) diff --git a/driver/util.py b/driver/util.py index 5e551a4d53..a9527f228a 100644 --- a/driver/util.py +++ b/driver/util.py @@ -77,6 +77,17 @@ def _split_off_filenames(planner_args): return planner_args[:num_filenames], planner_args[num_filenames:] +def set_default_build(args): + """If no build is specified, set args.build to the default build. This is + typically 'release' but can be changed to 'debug' with the option + '--debug'. This function modifies args directly.""" + if not args.build: + if args.debug: + args.build = "debug" + else: + args.build = "release" + + def split_planner_args(args): """Partition args.planner_args, the list of arguments for the planner components, into args.filenames, args.translate_options From a239859f653d779ca6b9b1b534c4217c5dd094c2 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Wed, 31 Jul 2024 17:08:48 +0200 Subject: [PATCH 41/44] code review --- driver/portfolio_runner.py | 26 ++++++------ driver/run_components.py | 52 +++++++++++++----------- driver/tab_completion.py | 83 ++++++++++++++++++++++++-------------- 3 files changed, 94 insertions(+), 67 deletions(-) diff --git a/driver/portfolio_runner.py b/driver/portfolio_runner.py index 746020596c..f70aa1087a 100644 --- a/driver/portfolio_runner.py +++ b/driver/portfolio_runner.py @@ -63,8 +63,8 @@ def adapt_args(args, search_cost_type, heuristic_cost_type, plan_manager): break -def run_search(executable, args, sas_file, plan_manager, time, memory): - complete_args = [executable] + args + [ +def run_search(command, args, sas_file, plan_manager, time, memory): + complete_args = command + args + [ "--internal-plan-file", plan_manager.get_plan_prefix()] print("args: %s" % complete_args) @@ -91,7 +91,7 @@ def compute_run_time(timeout, configs, pos): def run_sat_config(configs, pos, search_cost_type, heuristic_cost_type, - executable, sas_file, plan_manager, timeout, memory): + command, sas_file, plan_manager, timeout, memory): run_time = compute_run_time(timeout, configs, pos) if run_time <= 0: return None @@ -102,12 +102,12 @@ def run_sat_config(configs, pos, search_cost_type, heuristic_cost_type, args.extend([ "--internal-previous-portfolio-plans", str(plan_manager.get_plan_counter())]) - result = run_search(executable, args, sas_file, plan_manager, run_time, memory) + result = run_search(command, args, sas_file, plan_manager, run_time, memory) plan_manager.process_new_plans() return result -def run_sat(configs, executable, sas_file, plan_manager, final_config, +def run_sat(configs, command, sas_file, plan_manager, final_config, final_config_builder, timeout, memory): # If the configuration contains S_COST_TYPE or H_COST_TRANSFORM and the task # has non-unit costs, we start by treating all costs as one. When we find @@ -120,7 +120,7 @@ def run_sat(configs, executable, sas_file, plan_manager, final_config, for pos, (relative_time, args) in enumerate(configs): exitcode = run_sat_config( configs, pos, search_cost_type, heuristic_cost_type, - executable, sas_file, plan_manager, timeout, memory) + command, sas_file, plan_manager, timeout, memory) if exitcode is None: continue @@ -140,7 +140,7 @@ def run_sat(configs, executable, sas_file, plan_manager, final_config, heuristic_cost_type = "plusone" exitcode = run_sat_config( configs, pos, search_cost_type, heuristic_cost_type, - executable, sas_file, plan_manager, timeout, memory) + command, sas_file, plan_manager, timeout, memory) if exitcode is None: return @@ -162,18 +162,18 @@ def run_sat(configs, executable, sas_file, plan_manager, final_config, print("Abort portfolio and run final config.") exitcode = run_sat_config( [(1, final_config)], 0, search_cost_type, - heuristic_cost_type, executable, sas_file, plan_manager, + heuristic_cost_type, command, sas_file, plan_manager, timeout, memory) if exitcode is not None: yield exitcode -def run_opt(configs, executable, sas_file, plan_manager, timeout, memory): +def run_opt(configs, command, sas_file, plan_manager, timeout, memory): for pos, (relative_time, args) in enumerate(configs): run_time = compute_run_time(timeout, configs, pos) if run_time <= 0: return - exitcode = run_search(executable, args, sas_file, plan_manager, + exitcode = run_search(command, args, sas_file, plan_manager, run_time, memory) yield exitcode @@ -202,7 +202,7 @@ def get_portfolio_attributes(portfolio: Path): return attributes -def run(portfolio: Path, executable, sas_file, plan_manager, time, memory): +def run(portfolio: Path, command, sas_file, plan_manager, time, memory): """ Run the configs in the given portfolio file. @@ -231,9 +231,9 @@ def run(portfolio: Path, executable, sas_file, plan_manager, time, memory): if optimal: exitcodes = run_opt( - configs, executable, sas_file, plan_manager, timeout, memory) + configs, command, sas_file, plan_manager, timeout, memory) else: exitcodes = run_sat( - configs, executable, sas_file, plan_manager, final_config, + configs, command, sas_file, plan_manager, final_config, final_config_builder, timeout, memory) return returncodes.generate_portfolio_exitcode(list(exitcodes)) diff --git a/driver/run_components.py b/driver/run_components.py index 470f4991f4..ba6241c07f 100644 --- a/driver/run_components.py +++ b/driver/run_components.py @@ -30,15 +30,21 @@ _VALIDATE_PATH = Path(_VALIDATE_NAME) if _VALIDATE_NAME else None -def get_search_executable(build: str, exit_on_failure=True): - return _get_executable(build, _REL_SEARCH_PATH, exit_on_failure) +class MissingBuildError(Exception): + pass -def get_translate_executable(build: str, exit_on_failure=True): - return _get_executable(build, _REL_TRANSLATE_PATH, exit_on_failure) +def get_search_command(build: str): + return [_get_executable(build, _REL_SEARCH_PATH)] -def _get_executable(build: str, rel_path: Path, exit_on_failure=True): +def get_translate_command(build: str): + assert sys.executable, "Path to interpreter could not be found" + abs_path = _get_executable(build, _REL_TRANSLATE_PATH) + return [sys.executable, abs_path] + + +def _get_executable(build: str, rel_path: Path): # First, consider 'build' to be a path directly to the binaries. # The path can be absolute or relative to the current working # directory. @@ -50,21 +56,15 @@ def _get_executable(build: str, rel_path: Path, exit_on_failure=True): # '/builds//bin'. build_dir = util.BUILDS_DIR / build / "bin" if not build_dir.exists(): - if exit_on_failure: - returncodes.exit_with_driver_input_error( - f"Could not find build '{build}' at {build_dir}. " - f"Please run './build.py {build}'.") - else: - return None + raise MissingBuildError( + f"Could not find build '{build}' at {build_dir}. " + f"Please run './build.py {build}'.") abs_path = build_dir / rel_path if not abs_path.exists(): - if exit_on_failure: - returncodes.exit_with_driver_input_error( - f"Could not find '{rel_path}' in build '{build}'. " - f"Please run './build.py {build}'.") - else: - return None + raise MissingBuildError( + f"Could not find '{rel_path}' in build '{build}'. " + f"Please run './build.py {build}'.") return abs_path @@ -75,13 +75,14 @@ def run_translate(args): args.translate_time_limit, args.overall_time_limit) memory_limit = limits.get_memory_limit( args.translate_memory_limit, args.overall_memory_limit) - translate = get_translate_executable(args.build) - assert sys.executable, "Path to interpreter could not be found" - cmd = [sys.executable] + [translate] + args.translate_inputs + args.translate_options + try: + translate_command = get_translate_command(args.build) + except MissingBuildError as e: + returncodes.exit_with_driver_input_error(e) stderr, returncode = call.get_error_output_and_returncode( "translator", - cmd, + translate_command + args.translate_inputs + args.translate_options, time_limit=time_limit, memory_limit=memory_limit) @@ -121,7 +122,10 @@ def run_search(args): args.search_time_limit, args.overall_time_limit) memory_limit = limits.get_memory_limit( args.search_memory_limit, args.overall_memory_limit) - executable = get_search_executable(args.build) + try: + search_command = get_search_command(args.build) + except MissingBuildError as e: + returncodes.exit_with_driver_input_error(e) plan_manager = PlanManager( args.plan_file, @@ -133,7 +137,7 @@ def run_search(args): assert not args.search_options logging.info(f"search portfolio: {args.portfolio}") return portfolio_runner.run( - args.portfolio, executable, args.search_input, plan_manager, + args.portfolio, search_command, args.search_input, plan_manager, time_limit, memory_limit) else: if not args.search_options: @@ -144,7 +148,7 @@ def run_search(args): try: call.check_call( "search", - [executable] + args.search_options, + search_command + args.search_options, stdin=args.search_input, time_limit=time_limit, memory_limit=memory_limit) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 1ba7f7c698..462a43e400 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -1,12 +1,10 @@ import os -from pathlib import Path import subprocess import sys import tempfile -from . import returncodes from . import util -from .run_components import get_search_executable, get_translate_executable +from .run_components import get_search_command, get_translate_command, MissingBuildError try: import argcomplete @@ -16,9 +14,10 @@ def complete_build_arg(prefix, parsed_args, **kwargs): - if not util.BUILDS_DIR.exists(): + try: + return [p.name for p in util.BUILDS_DIR.iterdir() if p.is_dir()] + except OSError: return [] - return [p.name for p in util.BUILDS_DIR.iterdir() if p.is_dir()] def complete_planner_args(prefix, parsed_args, **kwargs): @@ -43,18 +42,12 @@ def complete_planner_args(prefix, parsed_args, **kwargs): if parsed_args.filenames or double_dash_in_options: if current_mode == "search": completions["--translate-options"] = "" - - downward = get_search_executable(parsed_args.build, exit_on_failure=False) - if downward and downward.exists(): - completions.update(_get_completions_from_downward( - downward, parsed_args.search_options, prefix)) + completions.update(_get_completions_from_downward( + parsed_args.build, parsed_args.search_options, prefix)) else: completions["--search-options"] = "" - - translator = get_translate_executable(parsed_args.build, exit_on_failure=False) - if translator and translator.exists(): - completions.update(_get_completions_from_translator( - translator, parsed_args.translate_options, prefix)) + completions.update(_get_completions_from_translator( + parsed_args.build, parsed_args.translate_options, prefix)) if has_only_filename_options and len(parsed_args.filenames) < 2: file_completer = argcomplete.FilesCompleter() @@ -84,35 +77,65 @@ def _split_argcomplete_ouput(content, entry_separator, help_separator): return suggestions -def _call_argcomplete(python_file, comp_line, comp_point): +def _get_bash_completion_args(cmd, options, prefix): + """ + Return values for four environment variables, bash uses as part of tab + completion when cmd is called with parsed arguments args, and the unparsed + prefix of a word to be completed prefix. + COMP_POINT: the cursor position within COMP_LINE + COMP_LINE: the full command line as a string + COMP_CWORD: an index into COMP_WORDS to the word under the cursor. + COMP_WORDS: the command line as list of words + """ + comp_words = [str(x) for x in cmd] + options + [prefix] + comp_line = " ".join(comp_words) + comp_point = str(len(comp_line)) + comp_cword = str(len(comp_words) - 1) + return comp_point, comp_line, comp_cword, comp_words + + +def _call_argcomplete(cmd, comp_line, comp_point): with tempfile.NamedTemporaryFile(mode="r") as f: env = os.environ.copy() env["COMP_LINE"] = comp_line - env["COMP_POINT"] = str(comp_point) + env["COMP_POINT"] = comp_point env["_ARGCOMPLETE"] = "1" env["_ARGCOMPLETE_STDOUT_FILENAME"] = f.name - subprocess.check_call([sys.executable, python_file], env=env) + subprocess.check_call(cmd, env=env) entry_separator, help_separator = _get_field_separators(env) return _split_argcomplete_ouput(f.read(), entry_separator, help_separator) -def _get_completions_from_downward(downward, options, prefix): +def _get_completions_from_downward(build, options, prefix): + try: + search_command = get_search_command(build) + except MissingBuildError: + return {} + entry_separator, help_separator = _get_field_separators(os.environ) help_separator = help_separator or ":" - simulated_commandline = [str(downward)] + options + [prefix] - comp_line = " ".join(simulated_commandline) - comp_point = str(len(comp_line)) - comp_cword = str(len(simulated_commandline) - 1) - cmd = [str(downward), "--bash-complete", entry_separator, help_separator, - comp_point, comp_line, comp_cword] + simulated_commandline + comp_point, comp_line, comp_cword, comp_words = _get_bash_completion_args( + search_command, options, prefix) + cmd = [str(x) for x in search_command] + ["--bash-complete", entry_separator, help_separator, + comp_point, comp_line, comp_cword] + comp_words output = subprocess.check_output(cmd, text=True) return _split_argcomplete_ouput(output, entry_separator, help_separator) -def _get_completions_from_translator(translator, options, prefix): - simulated_commandline = [str(translator)] + options + [prefix] - comp_line = " ".join(simulated_commandline) - comp_point = len(comp_line) - return _call_argcomplete(translator, comp_line, comp_point) +def _get_completions_from_translator(build, options, prefix): + try: + translate_command = get_translate_command(build) + except MissingBuildError: + return {} + + # We add domain and problem as dummy file names because otherwise, the + # translators tab completion will suggest filenames which we don't want at + # this point. Technically, file names should come after any options we + # complete but to consider them in the completion, they have to be to the + # left of the cursor position and to the left of any options that take + # parameters. + comp_point, comp_line, _, _ = _get_bash_completion_args( + translate_command, ["domain", "problem"] + options, prefix) + return _call_argcomplete(translate_command, comp_line, comp_point) def enable(parser): From 8f6c57bd258f70a847954ca3e02c6cce766ba235 Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Wed, 31 Jul 2024 17:26:40 +0200 Subject: [PATCH 42/44] fix tests and typo --- driver/tab_completion.py | 6 +++--- driver/tests.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/driver/tab_completion.py b/driver/tab_completion.py index 462a43e400..4ffbb43648 100644 --- a/driver/tab_completion.py +++ b/driver/tab_completion.py @@ -66,7 +66,7 @@ def _get_field_separators(env): return entry_separator, help_separator -def _split_argcomplete_ouput(content, entry_separator, help_separator): +def _split_argcomplete_output(content, entry_separator, help_separator): suggestions = {} for line in content.split(entry_separator): if help_separator and help_separator in line: @@ -103,7 +103,7 @@ def _call_argcomplete(cmd, comp_line, comp_point): env["_ARGCOMPLETE_STDOUT_FILENAME"] = f.name subprocess.check_call(cmd, env=env) entry_separator, help_separator = _get_field_separators(env) - return _split_argcomplete_ouput(f.read(), entry_separator, help_separator) + return _split_argcomplete_output(f.read(), entry_separator, help_separator) def _get_completions_from_downward(build, options, prefix): try: @@ -118,7 +118,7 @@ def _get_completions_from_downward(build, options, prefix): cmd = [str(x) for x in search_command] + ["--bash-complete", entry_separator, help_separator, comp_point, comp_line, comp_cword] + comp_words output = subprocess.check_output(cmd, text=True) - return _split_argcomplete_ouput(output, entry_separator, help_separator) + return _split_argcomplete_output(output, entry_separator, help_separator) def _get_completions_from_translator(build, options, prefix): diff --git a/driver/tests.py b/driver/tests.py index 8204bc3f5e..14a0a97150 100644 --- a/driver/tests.py +++ b/driver/tests.py @@ -17,7 +17,7 @@ from .call import check_call, _replace_paths_with_strings from . import limits from . import returncodes -from .run_components import get_search_executable +from .run_components import get_search_command from .util import REPO_ROOT_DIR, find_domain_path @@ -89,7 +89,7 @@ def _convert_to_standalone_config(config): def _run_search(config): check_call( "search", - [get_search_executable("release")] + list(config), + get_search_command("release") + list(config), stdin="output.sas") From 12b81aa83f95e9900fb1c343b1428c2e318c4c3d Mon Sep 17 00:00:00 2001 From: Florian Pommerening Date: Fri, 6 Sep 2024 15:54:33 +0200 Subject: [PATCH 43/44] discuss local and global activation of argcomplete --- TABCOMPLETION.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index 42e4406a73..ccaa5d46d4 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -6,13 +6,35 @@ We support tab completion for bash and zsh based on the python package [argcompl pip install argcomplete>=3.3 ``` -After the installation, add the following commands to your `.bashrc` or `.zshrc`. Depending on your installation replace `register-python-argcomplete` with `register-python-argcomplete3`. +After the installation, tab completion has to be enabled in one of two ways. + + +### Activating tab completion globally + +The global activation will enable tab completion for *all* Python files that support argcomplete (not only files related to Fast Downward). To activate the global tab completion execute the following command. Depending on your installation replace `activate-global-python-argcomplete` with `activate-global-python-argcomplete3`. + +```bash +activate-global-python-argcomplete +``` + + +### Activating tab completion locally + +In contrast to global activation, local activation only enables tab completion for files called `fast-downward.py`, `build.py`, or `translate.py`. However, activation is not limited to files that support argcomplete. This means that pressing tab on older version of Fast Downward files or unrelated files with the same name may have unintended side effects. For example, with older version of Fast Downward `build.py ` will start a build without printing the output. + +To activate the local tab completion, add the following commands to your `.bashrc` or `.zshrc`. Depending on your installation replace `register-python-argcomplete` with `register-python-argcomplete3`. ```bash eval "$(register-python-argcomplete fast-downward.py)" eval "$(register-python-argcomplete build.py)" eval "$(register-python-argcomplete translate.py)" +``` + +### Activating tab completion for the search binary + +If you are working with the search binary `downward` directly, adding the following commands to your `.bashrc` or `.zshrc` will enable tab completion. This is only necessary if you are not using the driver script `fast-downward.py`. +```bash function _downward_complete() { local IFS=$'\013' if [[ -n "${ZSH_VERSION-}" ]]; then @@ -42,3 +64,8 @@ fi ``` Restart your shell afterwards. + + +### Limitations + +The search configuration following the `--search` option is not yet covered by tab completion. For example, `fast-downward.py problem.pddl --search "ast"` will not suggest `astar`. \ No newline at end of file From 0762dc25724b54ca2c4cb1602dc0d4f28bffe221 Mon Sep 17 00:00:00 2001 From: speckdavid Date: Wed, 11 Sep 2024 10:56:38 +0200 Subject: [PATCH 44/44] fixed pip install comment --- TABCOMPLETION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TABCOMPLETION.md b/TABCOMPLETION.md index ccaa5d46d4..21ac99bac1 100644 --- a/TABCOMPLETION.md +++ b/TABCOMPLETION.md @@ -3,7 +3,7 @@ We support tab completion for bash and zsh based on the python package [argcomplete](https://pypi.org/project/argcomplete/). For full support, use at least version 3.3 which can be installed via `pip`. ```bash -pip install argcomplete>=3.3 +pip install "argcomplete>=3.3" ``` After the installation, tab completion has to be enabled in one of two ways.