Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d135371
{Network} Fix credential scan failure caused by VPN Gateway cert-base…
huiii99 Jan 16, 2026
f347b33
[Storage] `az storage account create/update`, `az storage account net…
calvinhzy Jan 16, 2026
47cfdec
{Network} Fix credential scan failure caused by password (#32650)
huiii99 Jan 16, 2026
9ea3671
[POSTGRESQL] `az postgres flexible-server create/georestore/replica`:…
nasc17 Jan 19, 2026
2816ba0
{POSTGRESSQL} Include `.json` files in package data (#32661)
DanielMicrosoft Jan 19, 2026
b52fb23
feature: add launch.json for tab completion
DanielMicrosoft Dec 18, 2025
ba5cb32
wip: debugging statements
DanielMicrosoft Dec 19, 2025
b318824
wip: add debug statements
DanielMicrosoft Dec 23, 2025
8f4d970
wip:
DanielMicrosoft Dec 23, 2025
0eacf79
wip: more benchmarking comments
DanielMicrosoft Dec 23, 2025
71cf3dd
feature: adding top-level command lookup from commandIndex.json
DanielMicrosoft Dec 23, 2025
d5ded81
wip: add total time print statement
DanielMicrosoft Jan 5, 2026
877045a
test: add argcomplete test for top-level cmds
DanielMicrosoft Jan 7, 2026
c39a8ae
fix: revert perf print statements
DanielMicrosoft Jan 7, 2026
5037d7f
fix: cleanup
DanielMicrosoft Jan 7, 2026
77441a6
refactor: remove blank lines
DanielMicrosoft Jan 8, 2026
d45fec1
refactor: create variable for top_level_marker
DanielMicrosoft Jan 19, 2026
d1a0f27
refactor: put logic into separate methods
DanielMicrosoft Jan 19, 2026
3e7a945
refactor: address copilot comments
DanielMicrosoft Jan 19, 2026
0d707ea
refactor: address copilot docstring comment
DanielMicrosoft Jan 19, 2026
6490a27
refactor: address flake whitespace errors
DanielMicrosoft Jan 19, 2026
7d3ac9a
fix: fix unused args linting error
DanielMicrosoft Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
]
}
48 changes: 47 additions & 1 deletion src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
EXCLUDED_PARAMS = ['self', 'raw', 'polling', 'custom_headers', 'operation_config',
'content_version', 'kwargs', 'client', 'no_wait']
EVENT_FAILED_EXTENSION_LOAD = 'MainLoader.OnFailedExtensionLoad'
# Marker used by CommandIndex.get() to signal top-level tab completion optimization
TOP_LEVEL_COMPLETION_MARKER = '__top_level_completion__'

# [Reserved, in case of future usage]
# Modules that will always be loaded. They don't expose commands but hook into CLI core.
Expand Down Expand Up @@ -208,6 +210,24 @@ def __init__(self, cli_ctx=None):
self.cmd_to_loader_map = {}
self.loaders = []

def _create_stub_commands_for_completion(self, command_names):
"""Create stub commands for top-level tab completion optimization.

Stub commands allow argcomplete to parse command names without loading modules.

:param command_names: List of command names to create stubs for
"""
from azure.cli.core.commands import AzCliCommand

def _stub_handler(*_args, **_kwargs):
"""Stub command handler used only for argument completion."""
return None

for cmd_name in command_names:
if cmd_name not in self.command_table:
# Stub commands only need names for argcomplete parser construction.
self.command_table[cmd_name] = AzCliCommand(self, cmd_name, _stub_handler)

def _update_command_definitions(self):
for cmd_name in self.command_table:
loaders = self.cmd_to_loader_map[cmd_name]
Expand Down Expand Up @@ -434,9 +454,16 @@ def _get_extension_suppressions(mod_loaders):
index_result = command_index.get(args)
if index_result:
index_modules, index_extensions = index_result

if index_modules == TOP_LEVEL_COMPLETION_MARKER:
self._create_stub_commands_for_completion(index_extensions)
_update_command_table_from_extensions([], ALWAYS_LOADED_EXTENSIONS)
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 @@ -484,7 +511,6 @@ def _get_extension_suppressions(mod_loaders):
else:
logger.debug("No module found from index for '%s'", args)

# No module found from the index. Load all command modules and extensions
logger.debug("Loading all modules and extensions")
_update_command_table_from_modules(args)

Expand Down Expand Up @@ -580,6 +606,23 @@ def __init__(self, cli_ctx=None):
self.cloud_profile = cli_ctx.cloud.profile
self.cli_ctx = cli_ctx

def _get_top_level_completion_commands(self):
"""Get top-level command names for tab completion optimization.

Returns marker and list of top-level commands (e.g., 'network', 'vm') for creating
stub commands without module loading. Returns None if index is empty, triggering
fallback to full module loading.

:return: tuple of (TOP_LEVEL_COMPLETION_MARKER, list of top-level command names) or None
"""
index = self.INDEX.get(self._COMMAND_INDEX) or {}
if not index:
logger.debug("Command index is empty, will fall back to loading all modules")
return None
top_level_commands = list(index.keys())
logger.debug("Top-level completion: %d commands available", len(top_level_commands))
return TOP_LEVEL_COMPLETION_MARKER, top_level_commands

def get(self, args):
"""Get the corresponding module and extension list of a command.

Expand All @@ -599,6 +642,9 @@ 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 self._get_top_level_completion_commands()
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')

This file was deleted.

This file was deleted.

Binary file not shown.
Loading