in src/Cli/func/ConsoleApp.cs [206:391]
internal IAction Parse()
{
// If there is no args are passed, display help.
// If args are passed and any it matched any of the strings in _helpArgs with a "-" then display help.
// Otherwise, continue parsing.
if (_args.Length == 0 ||
(_args.Length == 1 && _helpArgs.Any(ha => _args[0].Replace("-", string.Empty).Equals(ha, StringComparison.OrdinalIgnoreCase))))
{
_telemetryEvent.CommandName = "help";
_telemetryEvent.IActionName = typeof(HelpAction).Name;
_telemetryEvent.Parameters = new List<string>();
return new HelpAction(_actionAttributes, CreateAction);
}
bool isHelp = false;
var argsToParse = Enumerable.Empty<string>();
// this supports the format:
// `func help <context: optional> <subContext: optional> <action: optional>`
// but help has to be the first word. So `func azure help` for example doesn't work
// but `func help azure` should work.
if (_args.First().Equals("help", StringComparison.OrdinalIgnoreCase))
{
argsToParse = _args.Skip(1);
isHelp = true;
}
else
{
// This is for passing --help anywhere in the command line.
var argsHelpIntersection = _args
.Where(a => a.StartsWith("-"))
.Select(a => a.ToLowerInvariant().Replace("-", string.Empty))
.Intersect(_helpArgs)
.ToArray();
isHelp = argsHelpIntersection.Any();
argsToParse = isHelp
? _args.Where(a => !a.StartsWith("-") || argsHelpIntersection.Contains(a.Replace("-", string.Empty).ToLowerInvariant()))
: _args;
}
// We'll need to grab context arg: string, subcontext arg: string, action arg: string
var contextStr = string.Empty;
var subContextStr = string.Empty;
var actionStr = string.Empty;
// These start out as None, but if contextStr and subContextStr hold a value, they'll
// get parsed into these
var context = Context.None;
var subContext = Context.None;
// If isHelp, skip one and parse the rest of the command as usual.
var argsStack = new Stack<string>(argsToParse.Reverse());
// Grab the first string, but don't pop it off the stack.
// If it's indeed a valid context, will remove it later.
// Otherwise, it could be just an action. Actions are allowed not to have contexts.
contextStr = argsStack.Peek();
// Use this to collect all the invoking commands such as - "host start" or "azure functionapp publish"
var invokeCommand = new StringBuilder();
if (Enum.TryParse(contextStr, true, out context))
{
// It is a valid context, so pop it out of the stack.
argsStack.Pop();
invokeCommand.Append(contextStr);
if (argsStack.Any())
{
// We still have items in the stack, do the same again for subContext.
// This means we only support 2 levels of contexts only. Main, and Sub.
// There is currently no way to declaratively specify any more.
// If we ever need more than 2 contexts, we should switch to a more generic mechanism.
subContextStr = argsStack.Peek();
if (Enum.TryParse(subContextStr, true, out subContext))
{
argsStack.Pop();
invokeCommand.Append(" ");
invokeCommand.Append(subContextStr);
}
}
}
if (argsStack.Any())
{
// If there are still more items in the stack, then it's an actionStr
actionStr = argsStack.Pop();
}
if (string.IsNullOrEmpty(actionStr) || isHelp)
{
// It's ok to log invoke command here because it only contains the
// strings we were able to match with context / subcontext.
var invokedCommand = invokeCommand.ToString();
_telemetryEvent.CommandName = string.IsNullOrEmpty(invokedCommand) ? "help" : invokedCommand;
_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;
// 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.
return new HelpAction(_actionAttributes, CreateAction, contextStr, subContextStr);
}
// Find the matching action type.
// We expect to find 1 and only 1 IAction that matches all 3 (context, subContext, action)
var actionType = _actionAttributes
.Where(a => a.Attribute.Name.Equals(actionStr, StringComparison.OrdinalIgnoreCase) &&
a.Attribute.Context == context &&
a.Attribute.SubContext == subContext)
.SingleOrDefault();
// If none is found, display help passing in all the info we have right now.
if (actionType == null)
{
// If we did not find the action,
// we cannot log any invoked keywords as they may have PII
_telemetryEvent.CommandName = "help";
_telemetryEvent.IActionName = typeof(HelpAction).Name;
_telemetryEvent.Parameters = new List<string>();
_telemetryEvent.ParseError = true;
return new HelpAction(_actionAttributes, CreateAction, contextStr, subContextStr);
}
// If we are here that means actionStr is a legit action
if (invokeCommand.Length > 0)
{
invokeCommand.Append(" ");
}
invokeCommand.Append(actionStr);
// Create the IAction
var action = CreateAction(actionType.Type);
// Grab whatever is left in the stack of args into an array.
// This will be passed into the action as actions can optionally take args for their options.
var args = argsStack.ToArray();
try
{
// Give the action a change to parse its args.
var parseResult = action.ParseArgs(args);
if (parseResult.HasErrors)
{
// If we matched the action, we can log the invoke command
_telemetryEvent.CommandName = invokeCommand.ToString();
_telemetryEvent.IActionName = typeof(HelpAction).Name;
_telemetryEvent.Parameters = new List<string>();
_telemetryEvent.ParseError = true;
// There was an error with the args, pass it to the HelpAction.
return new HelpAction(_actionAttributes, CreateAction, action, parseResult);
}
else
{
_telemetryEvent.CommandName = invokeCommand.ToString();
_telemetryEvent.IActionName = action.GetType().Name;
_telemetryEvent.Parameters = TelemetryHelpers.GetCommandsFromCommandLineOptions(action.MatchedOptions);
// Action is ready to run.
return action;
}
}
catch (CliArgumentsException)
{
// TODO: we can probably display help here as well.
// This happens for actions that expect an ordered untyped options.
// If we matched the action, we can log the invoke command
_telemetryEvent.CommandName = invokeCommand.ToString();
_telemetryEvent.IActionName = action.GetType().Name;
_telemetryEvent.Parameters = new List<string>();
_telemetryEvent.ParseError = true;
_telemetryEvent.IsSuccessful = false;
throw;
}
}