File: ForwardedOptionExtensions.cs
Web Access
Project: src\src\sdk\src\Cli\Microsoft.DotNet.Cli.CommandLine\Microsoft.DotNet.Cli.CommandLine.csproj (Microsoft.DotNet.Cli.CommandLine)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.CommandLine.Parsing;

namespace Microsoft.DotNet.Cli.CommandLine;

/// <summary>
/// Extensions for tracking and invoking forwarding functions on options and arguments.
/// Forwarding functions are used to translate the parsed value of an option or argument
/// into a set of zero or more string values that will be passed to an inner command.
/// </summary>
public static class ForwardedOptionExtensions
{
    private static readonly Dictionary<Symbol, Func<ParseResult, IEnumerable<string>>> s_forwardingFunctions = [];
    private static readonly Lock s_lock = new();

    extension(Option option)
    {
        /// <summary>
        /// If this option has a forwarding function, this property will return it; otherwise, it will be null.
        /// </summary>
        /// <remarks>
        /// This getter is on the untyped Option because much of the _processing_ of option forwarding
        /// is done at the ParseResult level, where we don't have the generic type parameter.
        /// </remarks>
        public Func<ParseResult, IEnumerable<string>>? ForwardingFunction => s_forwardingFunctions.GetValueOrDefault(option);
    }

    extension<TValue>(Option<TValue> option)
    {
        /// <summary>
        /// Internal-only helper function that ensures the provided forwarding function is only called
        /// if the option actually has a value.
        /// </summary>
        private Func<ParseResult, IEnumerable<string>> GetForwardingFunction(Func<TValue, IEnumerable<string>> func)
        {
            return (ParseResult parseResult) =>
            {
                if (parseResult.GetResult(option) is OptionResult r)
                {
                    if (r.GetValueOrDefault<TValue>() is TValue value)
                    {
                        return func(value);
                    }
                    else
                    {
                        return [];
                    }
                }
                return [];
            };
        }

        /// <summary>
        /// Internal-only helper function that ensures the provided forwarding function is only called
        /// if the option actually has a value.
        /// </summary>
        private Func<ParseResult, IEnumerable<string>> GetForwardingFunction(Func<TValue, ParseResult, IEnumerable<string>> func)
        {
            return (ParseResult parseResult) =>
            {
                if (parseResult.GetResult(option) is OptionResult r)
                {
                    if (r.GetValueOrDefault<TValue>() is TValue value)
                    {
                        return func(value, parseResult);
                    }
                    else
                    {
                        return [];
                    }
                }
                return [];
            };
        }

        /// <summary>
        /// Forwards the option using the provided function to convert the option's value to zero or more string values.
        /// The function will only be called if the option has a value.
        /// </summary>
        public Option<TValue> SetForwardingFunction(Func<TValue?, IEnumerable<string>> func)
        {
            lock (s_lock)
            {
                s_forwardingFunctions[option] = option.GetForwardingFunction(func);
            }
            return option;
        }

        /// <summary>
        /// Forward the option using the provided function to convert the option's value to a single string value.
        /// The function will only be called if the option has a value.
        /// </summary>
        public Option<TValue> SetForwardingFunction(Func<TValue, string> format)
        {
            lock (s_lock)
            {
                s_forwardingFunctions[option] = option.GetForwardingFunction(o => [format(o)]);
            }
            return option;
        }

        /// <summary>
        /// Forward the option using the provided function to convert the option's value to a single string value.
        /// The function will only be called if the option has a value.
        /// </summary>
        public Option<TValue> SetForwardingFunction(Func<TValue?, ParseResult, IEnumerable<string>> func)
        {
            lock (s_lock)
            {
                s_forwardingFunctions[option] = option.GetForwardingFunction(func);
            }
            return option;
        }

        /// <summary>
        /// Forward the option as multiple calculated string values from whatever the option's value is.
        /// </summary>
        /// <param name="format"></param>
        /// <returns></returns>
        public Option<TValue> ForwardAsMany(Func<TValue?, IEnumerable<string>> format) => option.SetForwardingFunction(format);

        /// <summary>
        /// Forward the option as its own name.
        /// </summary>
        /// <returns></returns>
        public Option<TValue> Forward() => option.SetForwardingFunction((TValue? o) => [option.Name]);

        /// <summary>
        /// Forward the option as a string value. This value will be forwarded as long as the option has a OptionResult - which means that
        /// any implicit value calculation will cause the string value to be forwarded.
        /// </summary>
        public Option<TValue> ForwardAs(string value) => option.SetForwardingFunction((TValue? o) => [value]);

        /// <summary>
        /// Forward the option as a singular calculated string value.
        /// </summary>
        public Option<TValue> ForwardAsSingle(Func<TValue, string> format) => option.SetForwardingFunction(format);

        /// <summary>
        /// Configures the Option to only forward if it has an explicitly provided value, and not to forward if the value is implicitly provided (either via a default value or via implicit value inference for zero-arity options). This is useful for options where you only want to forward if the user explicitly provided the option, and not forward if the option just happens to have a value.
        /// This is most commonly useful for boolean options with no default value factory, where S.CL will infer a value of 'true' if the option is present and 'false' if it is absent.
        /// </summary>
        public Option<TValue> IfExplicitlyProvided()
        {
            if (s_forwardingFunctions.TryGetValue(option, out var existingFunc))
            {
                // wrap the existing forwarding function with an additional check to ensure the option's value is explicitly provided
                Func<ParseResult, IEnumerable<string>> wrapped = pr =>
                {
                    if (pr.GetResult(option) is OptionResult r && !r.Implicit)
                    {
                        return existingFunc(pr);
                    }
                    else
                    {
                        return [];
                    }
                };
                lock (s_forwardingFunctions)
                {
                    s_forwardingFunctions[option] = wrapped;
                }
            }
            return option;
        }
    }

    extension(Option<bool> option)
    {
        /// <summary>
        /// Forward the boolean option as a string value. This value will be forwarded as long as the option has a OptionResult - which means that
        /// any implicit value calculation will cause the string value to be forwarded. For boolean options specifically, if the option is zero arity
        /// and has no default value factory, S.CL will synthesize a true or false value based on whether the option was provided or not, so we need to
        /// add an additional implicit 'value is true' check to prevent accidentally forwarding the option for flags that are absent..
        /// </summary>
        public Option<bool> ForwardIfEnabled(string value) => option.SetForwardingFunction((bool o) => o ? [value] : []);
        /// <summary>
        /// Forward the boolean option as an array of string values. This value will be forwarded as long as the option has a OptionResult - which means that
        /// any implicit value calculation will cause the string value to be forwarded. For boolean options specifically, if the option is zero arity
        /// and has no default value factory, S.CL will synthesize a true or false value based on whether the option was provided or not, so we need to
        /// add an additional implicit 'value is true' check to prevent accidentally forwarding the option for flags that are absent..
        /// </summary>
        public Option<bool> ForwardIfEnabled(string[] value) => option.SetForwardingFunction((bool o) => o ? value : []);

        /// <inheritdoc cref="ForwardIfEnabled(Option{bool}, string)"/>
        public Option<bool> ForwardAs(string value) => option.ForwardIfEnabled(value);
    }

    extension(Option<IEnumerable<string>> option)
    {
        /// <summary>
        /// Foreach argument in the option's value, yield the <paramref name="alias"/> followed by the argument.
        /// </summary>
        public Option<IEnumerable<string>> ForwardAsManyArgumentsEachPrefixedByOption(string alias) =>
            option.ForwardAsMany(o => ForwardedArguments(alias, o));
    }

    extension(ParseResult parseResult)
    {
        /// <summary>
        /// Calls the forwarding functions for all options that have declared a forwarding function (via <see cref="ForwardedOptionExtensions"/>'s extension members) in the provided <see cref="ParseResult"/>.
        /// </summary>
        /// <param name="command">If not provided, uses the <see cref="ParseResult.CommandResult" />'s <see cref="CommandResult.Command"/>.</param>
        public IEnumerable<string> OptionValuesToBeForwarded(Command? command = null)
            => parseResult.OptionValuesToBeForwarded((command ?? parseResult.CommandResult.Command).Options);

        /// <summary>
        /// Calls the forwarding functions for all options that have declared a forwarding function (via <see cref="ForwardedOptionExtensions"/>'s extension members) in the provided <see cref="ParseResult"/>.
        /// </summary>
        /// <param name="command">If not provided, uses the <see cref="ParseResult.CommandResult" />'s <see cref="CommandResult.Command"/>.</param>
        public IEnumerable<string> OptionValuesToBeForwarded(IEnumerable<Option> options)
            => options
                .Select(o => o.ForwardingFunction)
                .SelectMany(f => f is not null ? f(parseResult) : []);

        /// <summary>
        /// Tries to find the first option named <paramref name="alias"/> in <paramref name="command"/>, and if found,
        /// invokes its forwarding function (if any) and returns the result. If no option with that name is found, or if the option
        /// has no forwarding function, returns an empty enumeration.
        /// </summary>
        public IEnumerable<string> ForwardedOptionValues(Command command, string alias)
        {
            var func = command.Options?
                .Where(o =>
                    (o.Name.Equals(alias) || o.Aliases.Contains(alias))
                    && o.ForwardingFunction is not null)
                .FirstOrDefault()?.ForwardingFunction;
            return func?.Invoke(parseResult) ?? [];
        }
    }

    /// <summary>
    /// For each argument in <paramref name="arguments"/>, yield the <paramref name="alias"/> followed by the argument.
    /// </summary>
    private static IEnumerable<string> ForwardedArguments(string alias, IEnumerable<string>? arguments)
    {
        foreach (string arg in arguments ?? [])
        {
            yield return alias;
            yield return arg;
        }
    }
}