1- // Copyright (c) .NET Foundation. All rights reserved.
1+ // Copyright (c) .NET Foundation. All rights reserved.
22// Licensed under the MIT License. See LICENSE in the project root for license information.
33
44using System . ComponentModel ;
@@ -18,6 +18,9 @@ namespace Azure.Functions.Cli.Actions
1818{
1919 internal class HelpAction : BaseAction
2020 {
21+ // Standardized indentation
22+ private const int IndentSize = 4 ;
23+
2124 private readonly string _context ;
2225 private readonly string _subContext ;
2326 private readonly IAction _action ;
@@ -42,7 +45,8 @@ public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> cr
4245 Type = type ,
4346 Contexts = attributes . Select ( a => a . Context ) ,
4447 SubContexts = attributes . Select ( a => a . SubContext ) ,
45- Names = attributes . Select ( a => a . Name )
48+ Names = attributes . Select ( a => a . Name ) ,
49+ ParentCommandName = attributes . Select ( a => a . ParentCommandName )
4650 } ;
4751 } ) ;
4852 }
@@ -54,6 +58,8 @@ public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> cr
5458 _parseResult = parseResult ;
5559 }
5660
61+ private static string Indent ( int levels = 1 ) => new string ( ' ' , IndentSize * ( levels < 0 ? 0 : levels ) ) ;
62+
5763 public override async Task RunAsync ( )
5864 {
5965 var latestVersionMessageTask = VersionHelper . IsRunningAnOlderVersion ( ) ;
@@ -187,7 +193,8 @@ private void DisplayGeneralHelp()
187193 . WriteLine ( "Usage: func [context] <action> [-/--options]" )
188194 . WriteLine ( ) ;
189195 DisplayContextsHelp ( contexts ) ;
190- var actions = _actionTypes . Where ( a => a . Contexts . Contains ( Context . None ) ) ;
196+ var actions = _actionTypes
197+ . Where ( a => a . Contexts . Contains ( Context . None ) ) ;
191198 DisplayActionsHelp ( actions ) ;
192199 }
193200
@@ -211,15 +218,80 @@ private void DisplayActionsHelp(IEnumerable<ActionType> actions)
211218 if ( actions . Any ( ) )
212219 {
213220 ColoredConsole . WriteLine ( TitleColor ( "Actions: " ) ) ;
221+
222+ // Group actions by parent command
223+ var parentCommands = actions
224+ . Where ( a => a . ParentCommandName . All ( p => string . IsNullOrEmpty ( p ) ) ) // Actions with no parent
225+ . ToList ( ) ;
226+
227+ var subCommands = actions
228+ . Where ( a => a . ParentCommandName . Any ( p => ! string . IsNullOrEmpty ( p ) ) ) // Actions with a parent
229+ . ToList ( ) ;
230+
214231 var longestName = actions . Select ( a => a . Names ) . SelectMany ( n => n ) . Max ( n => n . Length ) ;
215232 longestName += 2 ; // for coloring chars
216- foreach ( var action in actions )
233+
234+ // Display parent commands first
235+ foreach ( var parentAction in parentCommands )
217236 {
218- ColoredConsole . WriteLine ( GetActionHelp ( action , longestName ) ) ;
219- DisplaySwitches ( action ) ;
237+ // Display parent command
238+ ColoredConsole . WriteLine ( GetActionHelp ( parentAction , longestName ) ) ;
239+ DisplaySwitches ( parentAction ) ;
240+
241+ // Find and display child commands for this parent
242+ var parentName = parentAction . Names . First ( ) ;
243+ var childCommands = subCommands
244+ . Where ( s => s . ParentCommandName . Any ( p => p . Equals ( parentName , StringComparison . OrdinalIgnoreCase ) ) )
245+ . ToList ( ) ;
246+
247+ if ( childCommands . Any ( ) )
248+ {
249+ ColoredConsole . WriteLine ( ) ; // Add spacing before subcommands
250+
251+ foreach ( var childCommand in childCommands )
252+ {
253+ DisplaySubCommandHelp ( childCommand ) ;
254+ }
255+ }
256+
257+ ColoredConsole . WriteLine ( ) ;
220258 }
259+ }
260+ }
221261
222- ColoredConsole . WriteLine ( ) ;
262+ private void DisplaySubCommandHelp ( ActionType subCommand )
263+ {
264+ // Ensure subCommand is valid
265+ if ( subCommand is null )
266+ {
267+ return ;
268+ }
269+
270+ // Extract the runtime name from the full command name
271+ // E.g., "pack dotnet" -> "Dotnet"
272+ var fullCommandName = subCommand . Names ? . FirstOrDefault ( ) ;
273+
274+ string runtimeName = null ;
275+ if ( ! string . IsNullOrWhiteSpace ( fullCommandName ) )
276+ {
277+ var parts = fullCommandName . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
278+ runtimeName = parts . Length > 1 && ! string . IsNullOrEmpty ( parts [ 1 ] )
279+ ? char . ToUpper ( parts [ 1 ] [ 0 ] ) + parts [ 1 ] . Substring ( 1 ) . ToLower ( )
280+ : fullCommandName ;
281+ }
282+
283+ // Fall back to a safe default if we couldn't determine a runtime name
284+ runtimeName ??= subCommand . Type ? . Name ?? "subcommand" ;
285+
286+ var description = subCommand . Type ? . GetCustomAttributes < ActionAttribute > ( ) ? . FirstOrDefault ( ) ? . HelpText ;
287+
288+ // Display indented subcommand header with standardized indentation
289+ ColoredConsole . WriteLine ( $ "{ Indent ( 1 ) } { runtimeName . DarkCyan ( ) } { Indent ( 2 ) } { description } ") ;
290+
291+ // Display subcommand switches with extra indentation
292+ if ( subCommand . Type != null )
293+ {
294+ DisplaySwitches ( subCommand ) ;
223295 }
224296 }
225297
@@ -261,7 +333,7 @@ private void DisplayPositionalArguments(IEnumerable<CliArgument> arguments)
261333 longestName += 4 ; // 4 for coloring and <> characters
262334 foreach ( var argument in arguments )
263335 {
264- var helpLine = string . Format ( $ " {{0, { - longestName } }} {{1}}", $ "<{ argument . Name } >". DarkGray ( ) , argument . Description ) ;
336+ var helpLine = string . Format ( $ "{ Indent ( 1 ) } {{0, { - longestName } }} {{1}}", $ "<{ argument . Name } >". DarkGray ( ) , argument . Description ) ;
265337 if ( helpLine . Length < SafeConsole . BufferWidth )
266338 {
267339 ColoredConsole . WriteLine ( helpLine ) ;
@@ -277,7 +349,7 @@ private void DisplayPositionalArguments(IEnumerable<CliArgument> arguments)
277349 }
278350 }
279351
280- private static void DisplayOptions ( IEnumerable < ICommandLineOption > options )
352+ private static void DisplayOptions ( IEnumerable < ICommandLineOption > options , bool addExtraIndent = false )
281353 {
282354 var longestName = options . Max ( o =>
283355 {
@@ -311,7 +383,7 @@ private static void DisplayOptions(IEnumerable<ICommandLineOption> options)
311383 stringBuilder . Append ( $ " [-{ option . ShortName } ]") ;
312384 }
313385
314- var helpSwitch = string . Format ( $ " {{0, { - longestName } }} ", stringBuilder . ToString ( ) . DarkGray ( ) ) ;
386+ var helpSwitch = string . Format ( $ "{ ( addExtraIndent ? Indent ( 2 ) : Indent ( 1 ) ) } {{0, { - longestName } }} ", stringBuilder . ToString ( ) . DarkGray ( ) ) ;
315387 var helpSwitchLength = helpSwitch . Length - 2 ; // helpSwitch contains 2 formatting characters.
316388 var helpText = option . Description ;
317389 if ( string . IsNullOrWhiteSpace ( helpText ) )
0 commit comments