Skip to content

Commit ba7d29c

Browse files
Copilotaishwaryabh
andauthored
Clean up Help action CLI for pack command with refactored argument parsing and ConsoleApp help routing (#4639)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aishwaryabh <37918412+aishwaryabh@users.noreply.github.com> Co-authored-by: Aishwarya Bhandari <aibhandari@microsoft.com>
1 parent 3d7d831 commit ba7d29c

File tree

7 files changed

+102
-32
lines changed

7 files changed

+102
-32
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88

99
#### Changes
1010
- Add `func pack` basic functionality (#4600)
11+
- Clean up HelpAction and add `func pack` to help menu (#4639)

src/Cli/func/Actions/BaseAction.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using Azure.Functions.Cli.Common;
45
using Azure.Functions.Cli.Interfaces;
56
using Fclp;
67
using Fclp.Internals;
@@ -37,6 +38,8 @@ public void SetFlag<T>(string longOption, string description, Action<T> callback
3738
}
3839
}
3940

41+
public virtual IEnumerable<CliArgument> GetPositionalArguments() => Enumerable.Empty<CliArgument>();
42+
4043
public abstract Task RunAsync();
4144
}
4245
}

src/Cli/func/Actions/HelpAction.cs

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> cr
5454
public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> createAction, IAction action, ICommandLineParserResult parseResult)
5555
: this(actions, createAction)
5656
{
57+
_actionTypes = actions
58+
.Where(a => a.Attribute.ShowInHelp)
59+
.Select(a => a.Type)
60+
.Distinct()
61+
.Select(type =>
62+
{
63+
var attributes = type.GetCustomAttributes<ActionAttribute>();
64+
return new ActionType
65+
{
66+
Type = type,
67+
Contexts = attributes.Select(a => a.Context),
68+
SubContexts = attributes.Select(a => a.SubContext),
69+
Names = attributes.Select(a => a.Name),
70+
ParentCommandName = attributes.Select(a => a.ParentCommandName)
71+
};
72+
});
5773
_action = action;
5874
_parseResult = parseResult;
5975
}
@@ -167,17 +183,35 @@ private void DisplayContextHelp(Context context, Context subContext)
167183

168184
private void DisplayActionHelp()
169185
{
170-
if (_parseResult.Errors.All(e => e.Option.HasLongName && !string.IsNullOrEmpty(e.Option.Description)))
186+
if (_action == null)
171187
{
172-
foreach (var error in _parseResult.Errors)
173-
{
174-
ColoredConsole.WriteLine($"Error parsing {error.Option.LongName}. {error.Option.Description}");
175-
}
188+
return;
176189
}
177-
else
190+
191+
// Get all declared names (ActionAttribute.Name) for the current action.
192+
var currentActionNames = _action.GetType()
193+
.GetCustomAttributes<ActionAttribute>()
194+
.Select(a => a.Name)
195+
.Where(n => !string.IsNullOrWhiteSpace(n))
196+
.ToArray();
197+
198+
// Find the ActionType entry representing the current action (if it exists in the filtered _actionTypes set).
199+
var currentActionType = _actionTypes.FirstOrDefault(a => a.Type == _action.GetType());
200+
201+
// Collect subcommands whose ParentCommandName matches any of the current action names (case-insensitive).
202+
var subCommandActionTypes = _actionTypes
203+
.Where(a => a.ParentCommandName.Any(p => !string.IsNullOrEmpty(p) && currentActionNames.Contains(p, StringComparer.OrdinalIgnoreCase)))
204+
.ToList();
205+
206+
var actionsToDisplay = new List<ActionType>();
207+
if (currentActionType != null)
178208
{
179-
ColoredConsole.WriteLine(_parseResult.ErrorText);
209+
actionsToDisplay.Add(currentActionType);
180210
}
211+
212+
actionsToDisplay.AddRange(subCommandActionTypes);
213+
214+
DisplayActionsHelp(actionsToDisplay);
181215
}
182216

183217
private void DisplayGeneralHelp()
@@ -286,23 +320,32 @@ private void DisplaySubCommandHelp(ActionType subCommand)
286320
var description = subCommand.Type?.GetCustomAttributes<ActionAttribute>()?.FirstOrDefault()?.HelpText;
287321

288322
// Display indented subcommand header with standardized indentation
289-
ColoredConsole.WriteLine($"{Indent(1)}{runtimeName.DarkCyan()}{Indent(2)}{description}");
323+
ColoredConsole.WriteLine($"{Indent(1)}{runtimeName.DarkGreen()}{Indent(2)}{description}");
290324

291325
// Display subcommand switches with extra indentation
292326
if (subCommand.Type != null)
293327
{
294-
DisplaySwitches(subCommand);
328+
DisplaySwitches(subCommand, true);
295329
}
296330
}
297331

298-
private void DisplaySwitches(ActionType actionType)
332+
private void DisplaySwitches(ActionType actionType, bool shouldIndent = false)
299333
{
300334
var action = _createAction.Invoke(actionType.Type);
301335
try
302336
{
303337
var options = action.ParseArgs(Array.Empty<string>());
338+
var arguments = action.GetPositionalArguments();
339+
340+
if (arguments.Any())
341+
{
342+
ColoredConsole.WriteLine(TitleColor("Arguments:"));
343+
DisplayPositionalArguments(arguments);
344+
}
345+
304346
if (options.UnMatchedOptions.Any())
305347
{
348+
ColoredConsole.WriteLine(shouldIndent ? Indent(1) + TitleColor("Options:") : TitleColor("Options:"));
306349
DisplayOptions(options.UnMatchedOptions);
307350
ColoredConsole.WriteLine();
308351
}
@@ -334,18 +377,13 @@ private void DisplayPositionalArguments(IEnumerable<CliArgument> arguments)
334377
foreach (var argument in arguments)
335378
{
336379
var helpLine = string.Format($"{Indent(1)}{{0, {-longestName}}} {{1}}", $"<{argument.Name}>".DarkGray(), argument.Description);
337-
if (helpLine.Length < SafeConsole.BufferWidth)
380+
while (helpLine.Length > SafeConsole.BufferWidth)
338381
{
339-
ColoredConsole.WriteLine(helpLine);
340-
}
341-
else
342-
{
343-
while (helpLine.Length > SafeConsole.BufferWidth)
344-
{
345-
var segment = helpLine.Substring(0, SafeConsole.BufferWidth - 1);
346-
helpLine = helpLine.Substring(SafeConsole.BufferWidth);
347-
}
382+
var segment = helpLine.Substring(0, SafeConsole.BufferWidth - 1);
383+
helpLine = helpLine.Substring(SafeConsole.BufferWidth);
348384
}
385+
386+
ColoredConsole.WriteLine(helpLine);
349387
}
350388
}
351389

src/Cli/func/Actions/LocalActions/PackAction/PackAction.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using System;
45
using Azure.Functions.Cli.Common;
56
using Azure.Functions.Cli.Helpers;
67
using Azure.Functions.Cli.Interfaces;
78
using Fclp;
89

910
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
1011
{
11-
[Action(Name = "pack", HelpText = "Pack function app into a zip that's ready to deploy.", ShowInHelp = false)]
12+
[Action(Name = "pack", HelpText = "Pack function app into a zip that's ready to deploy with optional argument to pass in path of folder to pack.", ShowInHelp = true)]
1213
internal class PackAction : BaseAction
1314
{
1415
private readonly ISecretsManager _secretsManager;
@@ -35,8 +36,8 @@ public override ICommandLineParserResult ParseArgs(string[] args)
3536

3637
Parser
3738
.Setup<bool>("no-build")
38-
.WithDescription("Do not build the project before packaging. Optionally provide a directory when func pack as the first argument that has the build contents." +
39-
"Otherwise, default is the current directory.")
39+
.WithDescription("Do not build the project before packaging. Optionally provide a directory when func pack as the first argument that has the build contents. " +
40+
"Otherwise, default is the current directory")
4041
.Callback(n => NoBuild = n);
4142

4243
if (args.Any() && !args.First().StartsWith("-"))
@@ -49,6 +50,18 @@ public override ICommandLineParserResult ParseArgs(string[] args)
4950
return base.ParseArgs(args);
5051
}
5152

53+
public override IEnumerable<CliArgument> GetPositionalArguments()
54+
{
55+
return new[]
56+
{
57+
new CliArgument
58+
{
59+
Name = "PROJECT | SOLUTION",
60+
Description = "Folder path of Azure functions project or solution to pack. If a path is not specified, the command will pack the current directory."
61+
}
62+
};
63+
}
64+
5265
public override async Task RunAsync()
5366
{
5467
// Get the original command line args to pass to subcommands

src/Cli/func/ConsoleApp.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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

44
using System.Collections;
@@ -292,7 +292,7 @@ internal IAction Parse()
292292
actionStr = argsStack.Pop();
293293
}
294294

295-
if (string.IsNullOrEmpty(actionStr) || isHelp)
295+
if (string.IsNullOrEmpty(actionStr))
296296
{
297297
// It's ok to log invoke command here because it only contains the
298298
// strings we were able to match with context / subcontext.
@@ -301,17 +301,14 @@ internal IAction Parse()
301301
_telemetryEvent.IActionName = typeof(HelpAction).Name;
302302
_telemetryEvent.Parameters = new List<string>();
303303

304-
// If this wasn't a help command, actionStr was empty or null implying a parseError.
305-
_telemetryEvent.ParseError = !isHelp;
304+
// actionStr was empty or null implying a parseError.
305+
_telemetryEvent.ParseError = true;
306306

307307
// At this point we have all we need to create an IAction:
308308
// context
309309
// subContext
310310
// action
311-
// However, if isHelp is true, then display help for that context.
312-
// Action Name is ignored with help since we don't have action specific help yet.
313-
// There is no need so far for action specific help since general context help displays
314-
// the help for all the actions in that context anyway.
311+
// Display general help for the context.
315312
return new HelpAction(_actionAttributes, CreateAction, contextStr, subContextStr);
316313
}
317314

@@ -354,6 +351,18 @@ internal IAction Parse()
354351
{
355352
// Give the action a change to parse its args.
356353
var parseResult = action.ParseArgs(args);
354+
355+
// If help was requested, show action-specific help
356+
if (isHelp)
357+
{
358+
_telemetryEvent.CommandName = invokeCommand.ToString();
359+
_telemetryEvent.IActionName = typeof(HelpAction).Name;
360+
_telemetryEvent.Parameters = new List<string>();
361+
362+
// Display action-specific help
363+
return new HelpAction(_actionAttributes, CreateAction, action, parseResult);
364+
}
365+
357366
if (parseResult.HasErrors)
358367
{
359368
// If we matched the action, we can log the invoke command

src/Cli/func/Interfaces/IAction.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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

4+
using Azure.Functions.Cli.Common;
45
using Fclp;
56
using Fclp.Internals;
67

@@ -15,5 +16,7 @@ internal interface IAction
1516
internal ICommandLineParserResult ParseArgs(string[] args);
1617

1718
internal Task RunAsync();
19+
20+
internal IEnumerable<CliArgument> GetPositionalArguments();
1821
}
1922
}

test/Cli/Func.UnitTests/ActionsTests/HelpActionTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using System.Text;
5+
using Azure.Functions.Cli.Actions;
6+
using Azure.Functions.Cli.Interfaces;
47
using FluentAssertions;
58
using Xunit;
69

0 commit comments

Comments
 (0)