Skip to content
Merged
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
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

#### Changes
- Add `func pack` basic functionality (#4600)
- Clean up HelpAction and add `func pack` to help menu (#4639)
3 changes: 3 additions & 0 deletions src/Cli/func/Actions/BaseAction.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Azure.Functions.Cli.Common;
using Azure.Functions.Cli.Interfaces;
using Fclp;
using Fclp.Internals;
Expand Down Expand Up @@ -37,6 +38,8 @@ public void SetFlag<T>(string longOption, string description, Action<T> callback
}
}

public virtual IEnumerable<CliArgument> GetPositionalArguments() => Enumerable.Empty<CliArgument>();

public abstract Task RunAsync();
}
}
78 changes: 58 additions & 20 deletions src/Cli/func/Actions/HelpAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> cr
public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> createAction, IAction action, ICommandLineParserResult parseResult)
: this(actions, createAction)
{
_actionTypes = actions
.Where(a => a.Attribute.ShowInHelp)
.Select(a => a.Type)
.Distinct()
.Select(type =>
{
var attributes = type.GetCustomAttributes<ActionAttribute>();
return new ActionType
{
Type = type,
Contexts = attributes.Select(a => a.Context),
SubContexts = attributes.Select(a => a.SubContext),
Names = attributes.Select(a => a.Name),
ParentCommandName = attributes.Select(a => a.ParentCommandName)
};
});
_action = action;
_parseResult = parseResult;
}
Expand Down Expand Up @@ -167,17 +183,35 @@ private void DisplayContextHelp(Context context, Context subContext)

private void DisplayActionHelp()
{
if (_parseResult.Errors.All(e => e.Option.HasLongName && !string.IsNullOrEmpty(e.Option.Description)))
if (_action == null)
{
foreach (var error in _parseResult.Errors)
{
ColoredConsole.WriteLine($"Error parsing {error.Option.LongName}. {error.Option.Description}");
}
return;
}
else

// Get all declared names (ActionAttribute.Name) for the current action.
var currentActionNames = _action.GetType()
.GetCustomAttributes<ActionAttribute>()
.Select(a => a.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.ToArray();

// Find the ActionType entry representing the current action (if it exists in the filtered _actionTypes set).
var currentActionType = _actionTypes.FirstOrDefault(a => a.Type == _action.GetType());

// Collect subcommands whose ParentCommandName matches any of the current action names (case-insensitive).
var subCommandActionTypes = _actionTypes
.Where(a => a.ParentCommandName.Any(p => !string.IsNullOrEmpty(p) && currentActionNames.Contains(p, StringComparer.OrdinalIgnoreCase)))
.ToList();

var actionsToDisplay = new List<ActionType>();
if (currentActionType != null)
{
ColoredConsole.WriteLine(_parseResult.ErrorText);
actionsToDisplay.Add(currentActionType);
}

actionsToDisplay.AddRange(subCommandActionTypes);

DisplayActionsHelp(actionsToDisplay);
}

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

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

// Display subcommand switches with extra indentation
if (subCommand.Type != null)
{
DisplaySwitches(subCommand);
DisplaySwitches(subCommand, true);
}
}

private void DisplaySwitches(ActionType actionType)
private void DisplaySwitches(ActionType actionType, bool shouldIndent = false)
{
var action = _createAction.Invoke(actionType.Type);
try
{
var options = action.ParseArgs(Array.Empty<string>());
var arguments = action.GetPositionalArguments();

if (arguments.Any())
{
ColoredConsole.WriteLine(TitleColor("Arguments:"));
DisplayPositionalArguments(arguments);
}

if (options.UnMatchedOptions.Any())
{
ColoredConsole.WriteLine(shouldIndent ? Indent(1) + TitleColor("Options:") : TitleColor("Options:"));
DisplayOptions(options.UnMatchedOptions);
ColoredConsole.WriteLine();
}
Expand Down Expand Up @@ -334,18 +377,13 @@ private void DisplayPositionalArguments(IEnumerable<CliArgument> arguments)
foreach (var argument in arguments)
{
var helpLine = string.Format($"{Indent(1)}{{0, {-longestName}}} {{1}}", $"<{argument.Name}>".DarkGray(), argument.Description);
if (helpLine.Length < SafeConsole.BufferWidth)
while (helpLine.Length > SafeConsole.BufferWidth)
{
ColoredConsole.WriteLine(helpLine);
}
else
{
while (helpLine.Length > SafeConsole.BufferWidth)
{
var segment = helpLine.Substring(0, SafeConsole.BufferWidth - 1);
helpLine = helpLine.Substring(SafeConsole.BufferWidth);
}
var segment = helpLine.Substring(0, SafeConsole.BufferWidth - 1);
helpLine = helpLine.Substring(SafeConsole.BufferWidth);
}

ColoredConsole.WriteLine(helpLine);
}
}

Expand Down
19 changes: 16 additions & 3 deletions src/Cli/func/Actions/LocalActions/PackAction/PackAction.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using Azure.Functions.Cli.Common;
using Azure.Functions.Cli.Helpers;
using Azure.Functions.Cli.Interfaces;
using Fclp;

namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
{
[Action(Name = "pack", HelpText = "Pack function app into a zip that's ready to deploy.", ShowInHelp = false)]
[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)]
internal class PackAction : BaseAction
{
private readonly ISecretsManager _secretsManager;
Expand All @@ -35,8 +36,8 @@ public override ICommandLineParserResult ParseArgs(string[] args)

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

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

public override IEnumerable<CliArgument> GetPositionalArguments()
{
return new[]
{
new CliArgument
{
Name = "PROJECT | SOLUTION",
Description = "Folder path of Azure functions project or solution to pack. If a path is not specified, the command will pack the current directory."
}
};
}

public override async Task RunAsync()
{
// Get the original command line args to pass to subcommands
Expand Down
25 changes: 17 additions & 8 deletions src/Cli/func/ConsoleApp.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections;
Expand Down Expand Up @@ -292,7 +292,7 @@ internal IAction Parse()
actionStr = argsStack.Pop();
}

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

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

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

Expand Down Expand Up @@ -354,6 +351,18 @@ internal IAction Parse()
{
// Give the action a change to parse its args.
var parseResult = action.ParseArgs(args);

// If help was requested, show action-specific help
if (isHelp)
{
_telemetryEvent.CommandName = invokeCommand.ToString();
_telemetryEvent.IActionName = typeof(HelpAction).Name;
_telemetryEvent.Parameters = new List<string>();

// Display action-specific help
return new HelpAction(_actionAttributes, CreateAction, action, parseResult);
}

if (parseResult.HasErrors)
{
// If we matched the action, we can log the invoke command
Expand Down
5 changes: 4 additions & 1 deletion src/Cli/func/Interfaces/IAction.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Azure.Functions.Cli.Common;
using Fclp;
using Fclp.Internals;

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

internal Task RunAsync();

internal IEnumerable<CliArgument> GetPositionalArguments();
}
}
3 changes: 3 additions & 0 deletions test/Cli/Func.UnitTests/ActionsTests/HelpActionTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Text;
using Azure.Functions.Cli.Actions;
using Azure.Functions.Cli.Interfaces;
using FluentAssertions;
using Xunit;

Expand Down