@@ -475,15 +475,22 @@ def __init__(
475475 # The multiline command currently being typed which is used to tab complete multiline commands.
476476 self ._multiline_in_progress = ''
477477
478- # Set the header used for the help function's listing of documented functions
479- self .doc_header = "Documented commands (use 'help -v' for verbose/'help <topic>' for details)"
478+ # Set text which prints right before all of the help topics are listed.
479+ self .doc_leader = ""
480+
481+ # Set header for table listing documented commands.
482+ self .doc_header = "Documented Commands"
480483
481484 # Set header for table listing help topics not related to a command.
482485 self .misc_header = "Miscellaneous Help Topics"
483486
484487 # Set header for table listing commands that have no help info.
485488 self .undoc_header = "Undocumented Commands"
486489
490+ # If any command has been categorized, then all other commands that haven't been categorized
491+ # will display under this section in the help output.
492+ self .default_category = "Uncategorized Commands"
493+
487494 # The error that prints when no help information can be found
488495 self .help_error = "No help on {}"
489496
@@ -551,10 +558,6 @@ def __init__(
551558 # values are DisabledCommand objects.
552559 self .disabled_commands : dict [str , DisabledCommand ] = {}
553560
554- # If any command has been categorized, then all other commands that haven't been categorized
555- # will display under this section in the help output.
556- self .default_category = 'Uncategorized'
557-
558561 # The default key for sorting string results. Its default value performs a case-insensitive alphabetical sort.
559562 # If natural sorting is preferred, then set this to NATURAL_SORT_KEY.
560563 # cmd2 uses this key for sorting:
@@ -4039,6 +4042,45 @@ def complete_help_subcommands(
40394042 completer = argparse_completer .DEFAULT_AP_COMPLETER (argparser , self )
40404043 return completer .complete_subcommand_help (text , line , begidx , endidx , arg_tokens ['subcommands' ])
40414044
4045+ def _build_command_info (self ) -> tuple [dict [str , list [str ]], list [str ], list [str ], list [str ]]:
4046+ """Categorizes and sorts visible commands and help topics for display.
4047+
4048+ :return: tuple containing:
4049+ - dictionary mapping category names to lists of command names
4050+ - list of documented command names
4051+ - list of undocumented command names
4052+ - list of help topic names that are not also commands
4053+ """
4054+ # Get a sorted list of help topics
4055+ help_topics = sorted (self .get_help_topics (), key = self .default_sort_key )
4056+
4057+ # Get a sorted list of visible command names
4058+ visible_commands = sorted (self .get_visible_commands (), key = self .default_sort_key )
4059+ cmds_doc : list [str ] = []
4060+ cmds_undoc : list [str ] = []
4061+ cmds_cats : dict [str , list [str ]] = {}
4062+ for command in visible_commands :
4063+ func = cast (CommandFunc , self .cmd_func (command ))
4064+ has_help_func = False
4065+ has_parser = func in self ._command_parsers
4066+
4067+ if command in help_topics :
4068+ # Prevent the command from showing as both a command and help topic in the output
4069+ help_topics .remove (command )
4070+
4071+ # Non-argparse commands can have help_functions for their documentation
4072+ has_help_func = not has_parser
4073+
4074+ if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
4075+ category : str = getattr (func , constants .CMD_ATTR_HELP_CATEGORY )
4076+ cmds_cats .setdefault (category , [])
4077+ cmds_cats [category ].append (command )
4078+ elif func .__doc__ or has_help_func or has_parser :
4079+ cmds_doc .append (command )
4080+ else :
4081+ cmds_undoc .append (command )
4082+ return cmds_cats , cmds_doc , cmds_undoc , help_topics
4083+
40424084 @classmethod
40434085 def _build_help_parser (cls ) -> Cmd2ArgumentParser :
40444086 help_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (
@@ -4074,7 +4116,24 @@ def do_help(self, args: argparse.Namespace) -> None:
40744116 self .last_result = True
40754117
40764118 if not args .command or args .verbose :
4077- self ._help_menu (args .verbose )
4119+ cmds_cats , cmds_doc , cmds_undoc , help_topics = self ._build_command_info ()
4120+
4121+ if self .doc_leader :
4122+ self .poutput ()
4123+ self .poutput (self .doc_leader , style = Cmd2Style .HELP_LEADER , soft_wrap = False )
4124+ self .poutput ()
4125+
4126+ if not cmds_cats :
4127+ # No categories found, fall back to standard behavior
4128+ self ._print_documented_command_topics (self .doc_header , cmds_doc , args .verbose )
4129+ else :
4130+ # Categories found, Organize all commands by category
4131+ for category in sorted (cmds_cats .keys (), key = self .default_sort_key ):
4132+ self ._print_documented_command_topics (category , cmds_cats [category ], args .verbose )
4133+ self ._print_documented_command_topics (self .default_category , cmds_doc , args .verbose )
4134+
4135+ self .print_topics (self .misc_header , help_topics , 15 , 80 )
4136+ self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
40784137
40794138 else :
40804139 # Getting help for a specific command
@@ -4111,14 +4170,77 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol:
41114170 :param cmdlen: unused, even by cmd's version
41124171 :param maxcol: max number of display columns to fit into
41134172 """
4114- if cmds :
4115- header_grid = Table .grid ()
4116- header_grid .add_row (header , style = Cmd2Style .HELP_TITLE )
4117- if self .ruler :
4118- header_grid .add_row (Rule (characters = self .ruler ))
4119- self .poutput (header_grid )
4120- self .columnize (cmds , maxcol - 1 )
4121- self .poutput ()
4173+ if not cmds :
4174+ return
4175+
4176+ header_grid = Table .grid ()
4177+ header_grid .add_row (header , style = Cmd2Style .HELP_HEADER )
4178+ if self .ruler :
4179+ header_grid .add_row (Rule (characters = self .ruler ))
4180+ self .poutput (header_grid )
4181+ self .columnize (cmds , maxcol - 1 )
4182+ self .poutput ()
4183+
4184+ def _print_documented_command_topics (self , header : str , cmds : list [str ], verbose : bool ) -> None :
4185+ """Print topics which are documented commands, switching between verbose or traditional output."""
4186+ import io
4187+
4188+ if not cmds :
4189+ return
4190+
4191+ if not verbose :
4192+ self .print_topics (header , cmds , 15 , 80 )
4193+ return
4194+
4195+ category_grid = Table .grid ()
4196+ category_grid .add_row (header , style = Cmd2Style .HELP_HEADER )
4197+ category_grid .add_row (Rule (characters = self .ruler ))
4198+ topics_table = Table (
4199+ Column ("Name" , no_wrap = True ),
4200+ Column ("Description" , overflow = "fold" ),
4201+ box = SIMPLE_HEAD ,
4202+ border_style = Cmd2Style .RULE_LINE ,
4203+ show_edge = False ,
4204+ )
4205+
4206+ # Try to get the documentation string for each command
4207+ topics = self .get_help_topics ()
4208+ for command in cmds :
4209+ if (cmd_func := self .cmd_func (command )) is None :
4210+ continue
4211+
4212+ doc : str | None
4213+
4214+ # Non-argparse commands can have help_functions for their documentation
4215+ if command in topics :
4216+ help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
4217+ result = io .StringIO ()
4218+
4219+ # try to redirect system stdout
4220+ with contextlib .redirect_stdout (result ):
4221+ # save our internal stdout
4222+ stdout_orig = self .stdout
4223+ try :
4224+ # redirect our internal stdout
4225+ self .stdout = cast (TextIO , result )
4226+ help_func ()
4227+ finally :
4228+ with self .sigint_protection :
4229+ # restore internal stdout
4230+ self .stdout = stdout_orig
4231+ doc = result .getvalue ()
4232+
4233+ else :
4234+ doc = cmd_func .__doc__
4235+
4236+ # Attempt to locate the first documentation block
4237+ cmd_desc = strip_doc_annotations (doc ) if doc else ''
4238+
4239+ # Add this command to the table
4240+ topics_table .add_row (command , cmd_desc )
4241+
4242+ category_grid .add_row (topics_table )
4243+ self .poutput (category_grid , "" )
41224244
41234245 def columnize (self , str_list : list [str ] | None , display_width : int = 80 ) -> None :
41244246 """Display a list of single-line strings as a compact set of columns.
@@ -4132,9 +4254,6 @@ def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None
41324254 self .poutput ("<empty>" )
41334255 return
41344256
4135- nonstrings = [i for i in range (len (str_list )) if not isinstance (str_list [i ], str )]
4136- if nonstrings :
4137- raise TypeError (f"str_list[i] not a string for i in { nonstrings } " )
41384257 size = len (str_list )
41394258 if size == 1 :
41404259 self .poutput (str_list [0 ])
@@ -4162,7 +4281,8 @@ def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None
41624281 # The output is wider than display_width. Print 1 column with each string on its own row.
41634282 nrows = len (str_list )
41644283 ncols = 1
4165- colwidths = [1 ]
4284+ max_width = max (su .str_width (s ) for s in str_list )
4285+ colwidths = [max_width ]
41664286 for row in range (nrows ):
41674287 texts = []
41684288 for col in range (ncols ):
@@ -4175,114 +4295,6 @@ def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None
41754295 texts [col ] = su .align_left (texts [col ], width = colwidths [col ])
41764296 self .poutput (" " .join (texts ))
41774297
4178- def _help_menu (self , verbose : bool = False ) -> None :
4179- """Show a list of commands which help can be displayed for."""
4180- cmds_cats , cmds_doc , cmds_undoc , help_topics = self ._build_command_info ()
4181-
4182- if not cmds_cats :
4183- # No categories found, fall back to standard behavior
4184- self .poutput (self .doc_leader , soft_wrap = False )
4185- self ._print_topics (self .doc_header , cmds_doc , verbose )
4186- else :
4187- # Categories found, Organize all commands by category
4188- self .poutput (self .doc_leader , style = Cmd2Style .HELP_HEADER , soft_wrap = False )
4189- self .poutput (self .doc_header , style = Cmd2Style .HELP_HEADER , end = "\n \n " , soft_wrap = False )
4190- for category in sorted (cmds_cats .keys (), key = self .default_sort_key ):
4191- self ._print_topics (category , cmds_cats [category ], verbose )
4192- self ._print_topics (self .default_category , cmds_doc , verbose )
4193-
4194- self .print_topics (self .misc_header , help_topics , 15 , 80 )
4195- self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
4196-
4197- def _build_command_info (self ) -> tuple [dict [str , list [str ]], list [str ], list [str ], list [str ]]:
4198- # Get a sorted list of help topics
4199- help_topics = sorted (self .get_help_topics (), key = self .default_sort_key )
4200-
4201- # Get a sorted list of visible command names
4202- visible_commands = sorted (self .get_visible_commands (), key = self .default_sort_key )
4203- cmds_doc : list [str ] = []
4204- cmds_undoc : list [str ] = []
4205- cmds_cats : dict [str , list [str ]] = {}
4206- for command in visible_commands :
4207- func = cast (CommandFunc , self .cmd_func (command ))
4208- has_help_func = False
4209- has_parser = func in self ._command_parsers
4210-
4211- if command in help_topics :
4212- # Prevent the command from showing as both a command and help topic in the output
4213- help_topics .remove (command )
4214-
4215- # Non-argparse commands can have help_functions for their documentation
4216- has_help_func = not has_parser
4217-
4218- if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
4219- category : str = getattr (func , constants .CMD_ATTR_HELP_CATEGORY )
4220- cmds_cats .setdefault (category , [])
4221- cmds_cats [category ].append (command )
4222- elif func .__doc__ or has_help_func or has_parser :
4223- cmds_doc .append (command )
4224- else :
4225- cmds_undoc .append (command )
4226- return cmds_cats , cmds_doc , cmds_undoc , help_topics
4227-
4228- def _print_topics (self , header : str , cmds : list [str ], verbose : bool ) -> None :
4229- """Print topics, switching between verbose or traditional output."""
4230- import io
4231-
4232- if cmds :
4233- if not verbose :
4234- self .print_topics (header , cmds , 15 , 80 )
4235- else :
4236- category_grid = Table .grid ()
4237- category_grid .add_row (header , style = Cmd2Style .HELP_TITLE )
4238- category_grid .add_row (Rule (characters = self .ruler ))
4239- topics_table = Table (
4240- Column ("Name" , no_wrap = True ),
4241- Column ("Description" , overflow = "fold" ),
4242- box = SIMPLE_HEAD ,
4243- border_style = Cmd2Style .RULE_LINE ,
4244- show_edge = False ,
4245- )
4246-
4247- # Try to get the documentation string for each command
4248- topics = self .get_help_topics ()
4249- for command in cmds :
4250- if (cmd_func := self .cmd_func (command )) is None :
4251- continue
4252-
4253- doc : str | None
4254-
4255- # Non-argparse commands can have help_functions for their documentation
4256- if command in topics :
4257- help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
4258- result = io .StringIO ()
4259-
4260- # try to redirect system stdout
4261- with contextlib .redirect_stdout (result ):
4262- # save our internal stdout
4263- stdout_orig = self .stdout
4264- try :
4265- # redirect our internal stdout
4266- self .stdout = cast (TextIO , result )
4267- help_func ()
4268- finally :
4269- with self .sigint_protection :
4270- # restore internal stdout
4271- self .stdout = stdout_orig
4272- doc = result .getvalue ()
4273-
4274- else :
4275- doc = cmd_func .__doc__
4276-
4277- # Attempt to locate the first documentation block
4278- cmd_desc = strip_doc_annotations (doc ) if doc else ''
4279-
4280- # Add this command to the table
4281- topics_table .add_row (command , cmd_desc )
4282-
4283- category_grid .add_row (topics_table )
4284- self .poutput (category_grid , "" )
4285-
42864298 @staticmethod
42874299 def _build_shortcuts_parser () -> Cmd2ArgumentParser :
42884300 return argparse_custom .DEFAULT_ARGUMENT_PARSER (description = "List available shortcuts." )
0 commit comments