Functions/GenXdev.FileSystem/Copy-IdenticalParamValues.cs

// ################################################################################
// Part of PowerShell module : GenXdev.FileSystem
// Original cmdlet filename : Copy-IdenticalParamValues.cs
// Original author : René Vaessen / GenXdev
// Version : 1.302.2025
// ################################################################################
// Copyright (c) René Vaessen / GenXdev
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ################################################################################
 
 
 
using System.Collections;
using System.Collections.Concurrent;
using System.Management.Automation;
 
/// <summary>
/// Copies parameter values from bound parameters to a new hashtable based on
/// another function's possible parameters.
/// </summary>
/// <remarks>
/// This function creates a new hashtable containing only the parameter values
/// that match the parameters defined in the specified target function.
/// This can then be used to invoke the function using splatting.
///
/// Switch parameters are only included in the result if they were explicitly
/// provided and set to $true in the bound parameters. Non-present switch
/// parameters are excluded from the result to maintain proper parameter
/// semantics.
/// </remarks>
/// <example>
/// <code>
/// function Test-Function {
/// [CmdletBinding()]
/// param(
/// [Parameter(Mandatory = $true)]
/// [string] $Path,
/// [Parameter(Mandatory = $false)]
/// [switch] $Recurse
/// )
///
/// $params = GenXdev.FileSystem\Copy-IdenticalParamValues `
/// -BoundParameters $PSBoundParameters `
/// -FunctionName 'Get-ChildItem'
///
/// Get-ChildItem @params
/// }
/// </code>
/// </example>
[Cmdlet(VerbsCommon.Copy, "IdenticalParamValues")]
[OutputType(typeof(Hashtable))]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")]
public class CopyIdenticalParamValuesCommand : PSGenXdevCmdlet
{
    #region Parameters
 
    /// <summary>
    /// The bound parameters from which to copy values, typically $PSBoundParameters.
    /// </summary>
    [Parameter(
        Mandatory = true,
        Position = 0,
        HelpMessage = "Source bound parameters to copy from")]
    [ValidateNotNull()]
    public object[] BoundParameters { get; set; }
 
    /// <summary>
    /// The name of the function whose parameter set will be used as a filter.
    /// </summary>
    [Parameter(
        Mandatory = true,
        Position = 1,
        HelpMessage = "Target function name to filter parameters")]
    [ValidateNotNullOrEmpty()]
    public string FunctionName { get; set; }
 
    /// <summary>
    /// Default values for non-switch parameters that are not present in BoundParameters.
    /// Accepts PSVariable[], Hashtable, PSCmdlet instances, or other dictionary types.
    /// </summary>
    [Parameter(
        Mandatory = false,
        Position = 2,
        HelpMessage = "Default values for parameters")]
    public object DefaultValues { get; set; }
 
    #endregion
 
    #region Static Fields
 
    /// <summary>
    /// Common PowerShell parameters to filter out when copying parameters
    /// </summary>
    private static readonly string[] CommonParameterFilter;
 
    #endregion
 
    #region Static Constructor
 
    /// <summary>
    /// Static constructor to initialize static fields
    /// </summary>
    static CopyIdenticalParamValuesCommand()
    {
        CommonParameterFilter = new string[]
        {
                "input",
                "MyInvocation",
                "null",
                "PSBoundParameters",
                "PSCmdlet",
                "PSCommandPath",
                "PSScriptRoot",
                "Verbose",
                "Debug",
                "ErrorAction",
                "ErrorVariable",
                "WarningAction",
                "WarningVariable",
                "InformationAction",
                "InformationVariable",
                "OutVariable",
                "OutBuffer",
                "PipelineVariable",
                "WhatIf",
                "Confirm",
                "OutVariable",
                "ProgressAction",
                "ErrorVariable",
                "Passthru",
                "PassThru"
        };
 
        // CommandInfoCache is now shared with GenXdevCmd
    }
 
    #endregion
 
    #region Private Fields
 
    private Hashtable _results;
    private Hashtable _defaults;
    private CommandInfo _functionInfo;
 
    #endregion
 
    #region Cmdlet Lifecycle
 
    /// <summary>
    /// BeginProcessing - Initialize hashtables and get function information
    /// </summary>
    protected override void BeginProcessing()
    {
        // Initialize results hashtable
        _results = new Hashtable();
 
        // Create hashtable of default parameter values
        _defaults = CreateDefaultsHashtable2();
 
        // Get function info for parameter validation (with caching)
        _functionInfo = GetCachedCommandInfo(FunctionName);
 
        if (_functionInfo?.Parameters == null)
        {
            var errorRecord = new ErrorRecord(
                new ArgumentException($"Function '{FunctionName}' not found"),
                "FunctionNotFound",
                ErrorCategory.ObjectNotFound,
                FunctionName);
            WriteError(errorRecord);
            return;
        }
 
        WriteVerbose($"Found function with {_functionInfo.Parameters.Count} parameters");
    }
 
    /// <summary>
    /// ProcessRecord - Main processing logic
    /// </summary>
    protected override void ProcessRecord()
    {
        if (_functionInfo?.Parameters == null)
            return;
 
        // Get the first bound parameters object (PowerShell passes as object[])
        var boundParamsObject = BoundParameters?.FirstOrDefault();
        if (boundParamsObject == null)
        {
            WriteObject(_results);
            return;
        }
 
        // Convert to hashtable-like access
        var boundParamsDict = ConvertToParameterDictionary(boundParamsObject);
 
        // Iterate through all parameters of the target function
        foreach (var parameterKvp in _functionInfo.Parameters)
        {
            var paramName = parameterKvp.Key;
            var paramInfo = parameterKvp.Value;
 
            if (Array.IndexOf(CommonParameterFilter, paramName) >= 0) continue;
 
            // Check if parameter exists in bound parameters
            if (boundParamsDict.ContainsKey(paramName))
            {
                WriteVerbose($"Copying value for parameter '{paramName}'");
                var paramValue = boundParamsDict[paramName];
 
                // For switch parameters, only include if explicitly set to $true
                if (paramInfo.ParameterType == typeof(SwitchParameter))
                {
                    if (IsTrue(paramValue))
                    {
                        _results[paramName] = true;
                        WriteVerbose($"Including switch parameter '{paramName}' (explicitly set to true)");
                    }
                }
                else
                {
                    _results[paramName] = paramValue;
                }
            }
            else if (_defaults.ContainsKey(paramName) && _defaults[paramName] != null)
            {
                // Only add default values for non-switch parameters
                if (paramInfo.ParameterType == typeof(SwitchParameter))
                {
                    var defaultValue = _defaults[paramName];
                    if (IsTrue(defaultValue))
                    {
                        _results[paramName] = true;
                        WriteVerbose($"Using default value for '{paramName}': $True");
                    }
                }
                else
                {
                    var defaultValue = _defaults[paramName];
                    _results[paramName] = defaultValue;
 
                    // Convert to JSON for verbose output, matching PowerShell behavior
                    var jsonValue = ConvertToJsonString(defaultValue);
                    WriteVerbose($"Using default value for '{paramName}': {jsonValue}");
                }
            }
        }
    }
 
    /// <summary>
    /// EndProcessing - Output final results
    /// </summary>
    protected override void EndProcessing()
    {
        WriteVerbose($"Returning hashtable with {_results.Count} parameters");
        WriteObject(_results);
    }
 
    #endregion
 
    #region Private Helper Methods
 
    /// <summary>
    /// Gets command info with caching to improve performance
    /// </summary>
    /// <param name="functionName">Name of the function to get command info for</param>
    /// <returns>CommandInfo object or null if not found</returns>
    private CommandInfo GetCachedCommandInfo(string functionName)
    {
        // Try to get from cache first
        if (PSGenXdevCmdlet.CommandInfoCache.TryGetValue(functionName, out var cachedInfo))
        {
            WriteVerbose($"Using cached command info for function '{functionName}'");
            return cachedInfo;
        }
 
        // Not in cache, retrieve from PowerShell
        WriteVerbose($"Getting command info for function '{functionName}'");
 
        var getCommandScript = $"Microsoft.PowerShell.Core\\Get-Command -Name '{functionName}' -ErrorAction SilentlyContinue";
        var commandResults = InvokeCommand.InvokeScript(getCommandScript);
 
        CommandInfo commandInfo = null;
        if (commandResults?.Any() == true)
        {
            commandInfo = commandResults.FirstOrDefault()?.BaseObject as CommandInfo;
        }
 
        // Cache the result (even if null) to avoid repeated lookups for non-existent functions
        PSGenXdevCmdlet.CommandInfoCache.TryAdd(functionName, commandInfo);
 
        return commandInfo;
    }
 
    /// <summary>
    /// Creates the defaults hashtable from DefaultValues parameter
    /// </summary>
    private Hashtable CreateDefaultsHashtable2()
    {
        var defaultsHash = new Hashtable();
 
        if (DefaultValues == null)
            return defaultsHash;
 
        if (DefaultValues is PSObject dv)
        {
            DefaultValues = dv.BaseObject;
        }
 
        if (DefaultValues is Hashtable hash)
        {
            foreach (DictionaryEntry entry in hash)
            {
                if (entry.Key is string key)
                {
                    defaultsHash[key] = entry.Value;
                }
            }
        }
        // Handle PSCmdlet instances
        else if (DefaultValues is PSCmdlet cmdlet)
        {
            var cmdletType = cmdlet.GetType();
 
            try
            {
                // Create a new instance to get default values
                var newInstance = System.Activator.CreateInstance(cmdletType);
 
                foreach (var property in cmdletType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
                {
                    // Only consider properties that are cmdlet parameters
                    if (property.CanRead && property.CanWrite &&
                        System.Attribute.IsDefined(property, typeof(ParameterAttribute)))
                    {
                        try
                        {
                            var value = property.GetValue(newInstance);
                            // Only include non-null values for default parameters
                            if (value != null)
                            {
                                defaultsHash[property.Name] = value;
                            }
                        }
                        catch (Exception ex)
                        {
                            WriteVerbose($"Failed to get value for parameter property {property.Name}: {ex.Message}");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                WriteWarning($"Failed to extract default values from cmdlet {cmdletType.Name}: {ex.Message}");
            }
        }
        // Handle other dictionary types
        else if (DefaultValues is IDictionary dict)
        {
            foreach (DictionaryEntry entry in dict)
            {
                if (entry.Key is string key)
                {
                    defaultsHash[key] = entry.Value;
                }
            }
        }
        else if (DefaultValues is IEnumerable variables && !(DefaultValues is string))
        {
            foreach (var variable in variables)
            {
                var v = variable;
                if (variable is PSObject) { v = ((PSObject)v).BaseObject; }
 
                PSVariable psv = v as PSVariable;
 
                if (psv == null || psv.Value == null) continue;
 
                // Filter out variables with Options != None (matching PowerShell behavior)
                if (psv.Options == ScopedItemOptions.None)
                {
                    // Check if variable name is in filter list
                    if (Array.IndexOf(CommonParameterFilter, psv.Name) < 0)
                    {
                        // Skip null or whitespace string values
                        if (!(psv.Value is string strValue && string.IsNullOrWhiteSpace(strValue)))
                        {
                            if (psv.Value != null)
                            {
                                defaultsHash[psv.Name] = psv.Value;
                            }
                        }
                    }
                }
            }
        }
 
        return defaultsHash;
    }
 
    /// <summary>
    /// Converts bound parameters object to dictionary for easy access
    /// </summary>
    private Dictionary<string, object> ConvertToParameterDictionary(object boundParamsObject)
    {
        var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
 
        if (boundParamsObject is IDictionary dict)
        {
            foreach (DictionaryEntry entry in dict)
            {
                if (entry.Key is string key)
                {
                    result[key] = entry.Value;
                }
            }
        }
        else if (boundParamsObject is PSObject psObj)
        {
            foreach (var property in psObj.Properties)
            {
                result[property.Name] = property.Value;
            }
        }
 
        return result;
    }
 
    /// <summary>
    /// Converts object to JSON string for verbose output (mimicking PowerShell ConvertTo-Json behavior)
    /// </summary>
    private string ConvertToJsonString(object value)
    {
        try
        {
            // Use base class ConvertToJson method
            return ConvertToJson(value, 1);
        }
        catch
        {
            // Fall back to simple string representation
        }
 
        return value?.ToString() ?? "null";
    }
 
    #endregion
}