Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
193 changes: 193 additions & 0 deletions src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,19 @@ def _get_extension_suppressions(mod_loaders):
command_index = None
# Set fallback=False to turn off command index in case of regression
use_command_index = self.cli_ctx.config.getboolean('core', 'use_command_index', fallback=True)

# Fast path for top-level help (az --help or az with no args)
# Check if we can use cached help index to skip module loading
if use_command_index and (not args or args[0] in ('--help', '-h', 'help')):
command_index = CommandIndex(self.cli_ctx)
help_index = command_index.get_help_index()
if help_index:
logger.debug("Using cached help index, skipping module loading")
# Display help directly from cached data without loading modules
self._display_cached_help(help_index)
# Raise SystemExit to stop execution (similar to how --help normally works)
sys.exit(0)

if use_command_index:
command_index = CommandIndex(self.cli_ctx)
index_result = command_index.get(args)
Expand Down Expand Up @@ -496,8 +509,163 @@ def _get_extension_suppressions(mod_loaders):

if use_command_index:
command_index.update(self.command_table)

# Also cache help data for fast az --help in future
# This is done after loading all modules when help data is available
self._cache_help_index(command_index)

return self.command_table

def _display_cached_help(self, help_index):
"""Display help from cached help index without loading modules."""
from azure.cli.core._help import WELCOME_MESSAGE, PRIVACY_STATEMENT
import re

def _strip_ansi(text):
"""Remove ANSI color codes from text for length calculation."""
ansi_escape = re.compile(r'\x1b\[[0-9;]*m')
return ansi_escape.sub('', text)

# Show privacy statement if first run
ran_before = self.cli_ctx.config.getboolean('core', 'first_run', fallback=False)
if not ran_before:
print(PRIVACY_STATEMENT)
self.cli_ctx.config.set_value('core', 'first_run', 'yes')

# Show welcome message
print(WELCOME_MESSAGE)

print("\nGroup")
print(" az")

# Import knack's formatting functions
from knack.help import _print_indent, FIRST_LINE_PREFIX, _get_hanging_indent

# Separate groups and commands
groups_data = help_index.get('groups', {})
commands_data = help_index.get('commands', {})

# Helper function matching knack's _get_line_len
def _get_line_len(name, tags):
tags_len = len(_strip_ansi(tags))
return len(name) + tags_len + (2 if tags_len else 1)

# Helper function matching knack's _get_padding_len
def _get_padding_len(max_len, name, tags):
line_len = _get_line_len(name, tags)
if tags:
pad_len = max_len - line_len + 1
else:
pad_len = max_len - line_len
return pad_len

# Build items lists and calculate max_line_len across ALL items (groups + commands)
# This ensures colons align across both sections
max_line_len = 0
groups_items = []
for name in sorted(groups_data.keys()):
item = groups_data[name]
tags = item.get('tags', '')
groups_items.append((name, tags, item.get('summary', '')))
max_line_len = max(max_line_len, _get_line_len(name, tags))

commands_items = []
for name in sorted(commands_data.keys()):
item = commands_data[name]
tags = item.get('tags', '')
commands_items.append((name, tags, item.get('summary', '')))
max_line_len = max(max_line_len, _get_line_len(name, tags))

# Display groups
if groups_items:
print("\nSubgroups:")
indent = 1
LINE_FORMAT = '{name}{padding}{tags}{separator}{summary}'
for name, tags, summary in groups_items:
padding = ' ' * _get_padding_len(max_line_len, name, tags)
line = LINE_FORMAT.format(
name=name,
padding=padding,
tags=tags,
separator=FIRST_LINE_PREFIX if summary else '',
summary=summary
)
_print_indent(line, indent, _get_hanging_indent(max_line_len, indent))

# Display commands
if commands_items:
print("\nCommands:")
indent = 1
LINE_FORMAT = '{name}{padding}{tags}{separator}{summary}'
for name, tags, summary in commands_items:
padding = ' ' * _get_padding_len(max_line_len, name, tags)
line = LINE_FORMAT.format(
name=name,
padding=padding,
tags=tags,
separator=FIRST_LINE_PREFIX if summary else '',
summary=summary
)
_print_indent(line, indent, _get_hanging_indent(max_line_len, indent))

print("\nTo search AI knowledge base for examples, use: az find \"az \"")

# Show update notification
from azure.cli.core.util import show_updates_available
show_updates_available(new_line_after=True)

def _cache_help_index(self, command_index):
"""Cache help summaries for top-level commands to speed up `az --help`."""
try:
# Create a temporary parser to extract help information
from azure.cli.core.parser import AzCliCommandParser
parser = AzCliCommandParser(self.cli_ctx)
parser.load_command_table(self)

# Get the help file for the root level
from azure.cli.core._help import CliGroupHelpFile
subparser = parser.subparsers.get(tuple())
if subparser:
# Use cli_ctx.help which is the AzCliHelp instance
help_file = CliGroupHelpFile(self.cli_ctx.invocation.help, '', subparser)
help_file.load(subparser)

# Helper to build tag string for an item
def _get_tags(item):
tags = []
if hasattr(item, 'deprecate_info') and item.deprecate_info:
tags.append(str(item.deprecate_info.tag))
if hasattr(item, 'preview_info') and item.preview_info:
tags.append(str(item.preview_info.tag))
if hasattr(item, 'experimental_info') and item.experimental_info:
tags.append(str(item.experimental_info.tag))
return ' '.join(tags)

# Separate groups and commands
groups = {}
commands = {}

for child in help_file.children:
if hasattr(child, 'name') and hasattr(child, 'short_summary'):
if ' ' not in child.name: # Only top-level items
tags = _get_tags(child)
item_data = {
'summary': child.short_summary,
'tags': tags
}
# Check if it's a group or command
if child.type == 'group':
groups[child.name] = item_data
else:
commands[child.name] = item_data

# Store in the command index
help_index_data = {'groups': groups, 'commands': commands}
if groups or commands:
command_index.INDEX[command_index._HELP_INDEX] = help_index_data
logger.debug("Cached %d groups and %d commands", len(groups), len(commands))
except Exception as ex: # pylint: disable=broad-except
logger.debug("Failed to cache help data: %s", ex)

@staticmethod
def _sort_command_loaders(command_loaders):
Expand Down Expand Up @@ -567,6 +735,7 @@ class CommandIndex:
_COMMAND_INDEX = 'commandIndex'
_COMMAND_INDEX_VERSION = 'version'
_COMMAND_INDEX_CLOUD_PROFILE = 'cloudProfile'
_HELP_INDEX = 'helpIndex'

def __init__(self, cli_ctx=None):
"""Class to manage command index.
Expand Down Expand Up @@ -635,6 +804,25 @@ def get(self, args):

return None

def get_help_index(self):
"""Get the help index for top-level help display.

:return: Dictionary mapping top-level commands to their short summaries, or None if not available
"""
# Check if index is valid
index_version = self.INDEX[self._COMMAND_INDEX_VERSION]
cloud_profile = self.INDEX[self._COMMAND_INDEX_CLOUD_PROFILE]
if not (index_version and index_version == self.version and
cloud_profile and cloud_profile == self.cloud_profile):
return None

help_index = self.INDEX.get(self._HELP_INDEX, {})
if help_index:
logger.debug("Using cached help index with %d entries", len(help_index))
return help_index

return None

def update(self, command_table):
"""Update the command index according to the given command table.

Expand All @@ -645,6 +833,7 @@ def update(self, command_table):
self.INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] = self.cloud_profile
from collections import defaultdict
index = defaultdict(list)
help_index = {} # Maps top-level command to short summary

# self.cli_ctx.invocation.commands_loader.command_table doesn't exist in DummyCli due to the lack of invocation
for command_name, command in command_table.items():
Expand All @@ -654,8 +843,11 @@ def update(self, command_table):
module_name = command.loader.__module__
if module_name not in index[top_command]:
index[top_command].append(module_name)

elapsed_time = timeit.default_timer() - start_time
self.INDEX[self._COMMAND_INDEX] = index
# Note: helpIndex is populated separately when az --help is displayed
# We don't populate it here because the help data isn't available yet
logger.debug("Updated command index in %.3f seconds.", elapsed_time)

def invalidate(self):
Expand All @@ -672,6 +864,7 @@ def invalidate(self):
self.INDEX[self._COMMAND_INDEX_VERSION] = ""
self.INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] = ""
self.INDEX[self._COMMAND_INDEX] = {}
self.INDEX[self._HELP_INDEX] = {}
logger.debug("Command index has been invalidated.")


Expand Down
52 changes: 52 additions & 0 deletions src/azure-cli-core/azure/cli/core/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,58 @@ def execute(self, args):
self.parser.enable_autocomplete()
subparser = self.parser.subparsers[tuple()]
self.help.show_welcome(subparser)

# After showing help, cache the help summaries for future fast access
# This allows subsequent `az --help` calls to skip module loading
use_command_index = self.cli_ctx.config.getboolean('core', 'use_command_index', fallback=True)
logger.debug("About to cache help data, use_command_index=%s", use_command_index)
if use_command_index:
try:
from azure.cli.core import CommandIndex
command_index = CommandIndex(self.cli_ctx)
# Extract help data from the parser that was just used
from azure.cli.core._help import CliGroupHelpFile
help_file = CliGroupHelpFile(self.cli_ctx.help, '', subparser)
help_file.load(subparser)

# Helper to build tag string for an item
def _get_tags(item):
tags = []
if hasattr(item, 'deprecate_info') and item.deprecate_info:
tags.append(str(item.deprecate_info.tag))
if hasattr(item, 'preview_info') and item.preview_info:
tags.append(str(item.preview_info.tag))
if hasattr(item, 'experimental_info') and item.experimental_info:
tags.append(str(item.experimental_info.tag))
return ' '.join(tags)

# Separate groups and commands
groups = {}
commands = {}

for child in help_file.children:
if hasattr(child, 'name') and hasattr(child, 'short_summary'):
if ' ' not in child.name: # Only top-level items
tags = _get_tags(child)
item_data = {
'summary': child.short_summary,
'tags': tags
}
# Check if it's a group or command
if child.type == 'group':
groups[child.name] = item_data
else:
commands[child.name] = item_data

# Store in the command index
help_index_data = {'groups': groups, 'commands': commands}
if groups or commands:
from azure.cli.core._session import INDEX
from azure.cli.core import __version__
INDEX['helpIndex'] = help_index_data
logger.debug("Cached %d groups and %d commands for fast access", len(groups), len(commands))
except Exception as ex: # pylint: disable=broad-except
logger.debug("Failed to cache help data: %s", ex)

# TODO: No event in base with which to target
telemetry.set_command_details('az', command_preserve_casing=command_preserve_casing)
Expand Down
Loading