Skip to content
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
21 changes: 21 additions & 0 deletions src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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`
Expand Down
18 changes: 18 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_argcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')