internal IAction Parse()

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;
            }
        }