diff --git a/.vscode/launch.json b/.vscode/launch.json index e7ed7a11353..5d00ab3b09d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,6 +38,26 @@ "--help" ], "console": "integratedTerminal", + }, + { + "name": "Azure CLI Debug Tab Completion (External Console)", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/azure-cli/azure/cli/__main__.py", + "args": [], + "console": "externalTerminal", + "cwd": "${workspaceFolder}", + "env": { + "_ARGCOMPLETE": "1", + "COMP_LINE": "az vm create --", + "COMP_POINT": "18", + "_ARGCOMPLETE_SUPPRESS_SPACE": "0", + "_ARGCOMPLETE_IFS": "\n", + "_ARGCOMPLETE_SHELL": "powershell", + "ARGCOMPLETE_USE_TEMPFILES": "1", + "_ARGCOMPLETE_STDOUT_FILENAME": "C:\\temp\\az_debug_completion.txt" + }, + "justMyCode": false } ] } \ No newline at end of file diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index ae0f253caee..0c333c24a66 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -432,9 +432,23 @@ def _get_extension_suppressions(mod_loaders): index_result = command_index.get(args) if index_result: index_modules, index_extensions = index_result + # Special case for top-level completion - create minimal command groups + if index_modules == '__top_level_completion__': + from azure.cli.core.commands import AzCliCommand + # index_extensions contains the command names, not extensions + for cmd_name in index_extensions: + # Create a minimal command entry for tab completion + # This allows argparse to see the command without loading the module + if cmd_name not in self.command_table: + self.command_table[cmd_name] = AzCliCommand( + self, cmd_name, lambda: None + ) + return self.command_table + # Always load modules and extensions, because some of them (like those in # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core _update_command_table_from_modules(args, index_modules) + # The index won't contain suppressed extensions _update_command_table_from_extensions([], index_extensions) @@ -597,6 +611,13 @@ def get(self, args): # Make sure the top-level command is provided, like `az version`. # Skip command index for `az` or `az --help`. if not args or args[0].startswith('-'): + # For top-level completion (az [tab]) + if not args and self.cli_ctx.data.get('completer_active'): + # Return a special marker so we know to skip module loading for top-level completion + index = self.INDEX[self._COMMAND_INDEX] + all_commands = list(index.keys()) + logger.debug("Top-level completion: %d commands available", len(all_commands)) + return '__top_level_completion__', all_commands # special marker, command list return None # Get the top-level command, like `network` in `network vnet create -h` diff --git a/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py b/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py index 52a24af9b3c..1b0047f102b 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py @@ -64,3 +64,21 @@ def dummy_completor(*args, **kwargs): with open('argcomplete.out') as f: self.assertEqual(f.read(), 'dummystorage ') os.remove('argcomplete.out') + + def test_top_level_completion(self): + """Test that top-level completion (az [tab]) returns command names from index""" + import os + import sys + + if sys.platform == 'win32': + self.skipTest('Skip argcomplete test on Windows') + + run_cmd(['az'], env=self.argcomplete_env('az ', '3')) + with open('argcomplete.out') as f: + completions = f.read().split() + # Verify common top-level commands are present + self.assertIn('account', completions) + self.assertIn('vm', completions) + self.assertIn('network', completions) + self.assertIn('storage', completions) + os.remove('argcomplete.out')