@@ -475,15 +475,22 @@ def __init__(
475
475
# The multiline command currently being typed which is used to tab complete multiline commands.
476
476
self ._multiline_in_progress = ''
477
477
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"
480
483
481
484
# Set header for table listing help topics not related to a command.
482
485
self .misc_header = "Miscellaneous Help Topics"
483
486
484
487
# Set header for table listing commands that have no help info.
485
488
self .undoc_header = "Undocumented Commands"
486
489
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
+
487
494
# The error that prints when no help information can be found
488
495
self .help_error = "No help on {}"
489
496
@@ -551,10 +558,6 @@ def __init__(
551
558
# values are DisabledCommand objects.
552
559
self .disabled_commands : dict [str , DisabledCommand ] = {}
553
560
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
-
558
561
# The default key for sorting string results. Its default value performs a case-insensitive alphabetical sort.
559
562
# If natural sorting is preferred, then set this to NATURAL_SORT_KEY.
560
563
# cmd2 uses this key for sorting:
@@ -4039,6 +4042,45 @@ def complete_help_subcommands(
4039
4042
completer = argparse_completer .DEFAULT_AP_COMPLETER (argparser , self )
4040
4043
return completer .complete_subcommand_help (text , line , begidx , endidx , arg_tokens ['subcommands' ])
4041
4044
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
+
4042
4084
@classmethod
4043
4085
def _build_help_parser (cls ) -> Cmd2ArgumentParser :
4044
4086
help_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (
@@ -4074,7 +4116,24 @@ def do_help(self, args: argparse.Namespace) -> None:
4074
4116
self .last_result = True
4075
4117
4076
4118
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 )
4078
4137
4079
4138
else :
4080
4139
# Getting help for a specific command
@@ -4111,14 +4170,77 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol:
4111
4170
:param cmdlen: unused, even by cmd's version
4112
4171
:param maxcol: max number of display columns to fit into
4113
4172
"""
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 , "" )
4122
4244
4123
4245
def columnize (self , str_list : list [str ] | None , display_width : int = 80 ) -> None :
4124
4246
"""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
4132
4254
self .poutput ("<empty>" )
4133
4255
return
4134
4256
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 } " )
4138
4257
size = len (str_list )
4139
4258
if size == 1 :
4140
4259
self .poutput (str_list [0 ])
@@ -4162,7 +4281,8 @@ def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None
4162
4281
# The output is wider than display_width. Print 1 column with each string on its own row.
4163
4282
nrows = len (str_list )
4164
4283
ncols = 1
4165
- colwidths = [1 ]
4284
+ max_width = max (su .str_width (s ) for s in str_list )
4285
+ colwidths = [max_width ]
4166
4286
for row in range (nrows ):
4167
4287
texts = []
4168
4288
for col in range (ncols ):
@@ -4175,114 +4295,6 @@ def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None
4175
4295
texts [col ] = su .align_left (texts [col ], width = colwidths [col ])
4176
4296
self .poutput (" " .join (texts ))
4177
4297
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
-
4286
4298
@staticmethod
4287
4299
def _build_shortcuts_parser () -> Cmd2ArgumentParser :
4288
4300
return argparse_custom .DEFAULT_ARGUMENT_PARSER (description = "List available shortcuts." )
0 commit comments