Powershell模块:动态强制分层参数

时间:2015-04-13 14:03:15

标签: c# powershell

所以我真正想要的是在PS模块中有些可用的标签完成。 ValidateSet似乎是去这里的方式。

不幸的是我的数据是动态的,因此我无法预先使用所有有效值来注释参数。 DynamicParameters / IDynamicParameters似乎是 问题的解决方案。

把这些东西放在一起(并将我的失败减少到简单的测试用例)我们最终得到:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;

namespace PSDummy
{
    [Cmdlet(VerbsCommon.Get, "BookDetails")]
    public class GetBookDetails : Cmdlet, IDynamicParameters
    {
        IDictionary<string, string[]> m_dummyData = new Dictionary<string, string[]> {
            {"Terry Pratchett", new [] {"Small Gods", "Mort", "Eric"}},
            {"Douglas Adams", new [] {"Hitchhiker's Guide", "The Meaning of Liff"}}
        };

        private RuntimeDefinedParameter m_authorParameter;
        private RuntimeDefinedParameter m_bookParameter;

        protected override void ProcessRecord()
        {
              // Do stuff here..
        }

        public object GetDynamicParameters()
        {
            var parameters = new RuntimeDefinedParameterDictionary();

            m_authorParameter = CreateAuthorParameter();
            m_bookParameter = CreateBookParameter();

            parameters.Add(m_authorParameter.Name, m_authorParameter);
            parameters.Add(m_bookParameter.Name, m_bookParameter);
            return parameters;
        }

        private RuntimeDefinedParameter CreateAuthorParameter()
        {
            var p = new RuntimeDefinedParameter(
                "Author",
                typeof(string),
                new Collection<Attribute>
                {
                    new ParameterAttribute {
                        ParameterSetName = "BookStuff",
                        Position = 0,
                        Mandatory = true
                    },
                    new ValidateSetAttribute(m_dummyData.Keys.ToArray()),
                    new ValidateNotNullOrEmptyAttribute()
                });

            // Actually this is always mandatory, but sometimes I can fall back to a default
            // value. How? p.Value = mydefault?

            return p;
        }

        private RuntimeDefinedParameter CreateBookParameter()
        {
            // How to define a ValidateSet based on the parameter value for
            // author?
            var p = new RuntimeDefinedParameter(
                "Book",
                typeof(string),
                new Collection<Attribute>
                {
                    new ParameterAttribute {
                        ParameterSetName = "BookStuff",
                        Position = 1,
                        Mandatory = true
                    },
                    new ValidateSetAttribute(new string[1] { string.Empty }/* cannot fill this, because I cannot access the author */),
                    new ValidateNotNullOrEmptyAttribute()
                });

            return p;
        }
    }
}

不幸的是,这个小小的片段已经引发了很多问题。有序下降:

  • 我无法看到如何在参数之间建立连接。如果您选择作者,则应该只能选择与作者匹配的书籍。到目前为止GetDynamicParameters()似乎总是无状态:我看不到访问不同/早期动态参数的值的方法。尝试将其保留在一个字段中,尝试搜索MyInvocation - 没有运气。这甚至可能吗?

  • 如何定义必需参数的默认值?不适合这个愚蠢的例子,但我们可以说你可以存储你最喜欢的作者。从现在开始,我想默认为该作者,但是仍然必须指向作者。你给了我一个默认值(并且仍然可以指定别的东西)或者你需要明确。

  • 带空格的字符串的标签完成看起来很奇怪/破坏/有限 - 因为它没有用引号括起该值(例如,如果您键入dir C:\Program <tab>,cmd.exe就会这样做)。因此,标签完成实际上打破调用(如果上述问题将得到解决,Get-BookDetails Ter<tab>会/将扩展为Get-BookDetails Terry Pratchett,这将姓氏放在参数位置1又称&# 39;书&#39;

难道不是那么难,肯定有人做过类似的事吗?

更新:经过另一个修补和愚弄的好日子后,我没有看到一种方法来完成这项工作。命令行开关是无状态的,将一遍又一遍地实例化。在我可以定义动态参数(GetDynamicParameters)的时间点,我无法访问它们的(当前)值/看到它们被绑定的内容 - 例如MyInvocation.BoundParameters为零。我将这个问题保持开放,但似乎并不支持这个问题。我看到的所有示例都根据静态值的值添加动态参数 - 这与此处不相关。开溜。

1 个答案:

答案 0 :(得分:9)

我认为这很有效。不幸的是,它使用反射来获取第一个项目符号的某些cmdlet私有成员。我从Garrett Serack得到了这个想法。我不确定我是否完全理解如何做默认作者,所以我这样做是为了让最后一位有效的作者存储在一个静态字段中,这样你就不需要了 - 下一次使用。

以下是代码:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;

namespace PSDummy
{
    internal class DynParamQuotedString {
        /*
            This works around the PowerShell bug where ValidateSet values aren't quoted when necessary, and
            adding the quotes breaks it. Example:

            ValidateSet valid values = 'Test string'  (The quotes are part of the string)

            PowerShell parameter binding would interperet that as [Test string] (no single quotes), which wouldn't match
            the valid value (which has the quotes). If you make the parameter a DynParamQuotedString, though,
            the parameter binder will coerce [Test string] into an instance of DynParamQuotedString, and the binder will
            call ToString() on the object, which will add the quotes back in.
        */

        internal static string DefaultQuoteCharacter = "'";

        public DynParamQuotedString(string quotedString) : this(quotedString, DefaultQuoteCharacter) {}
        public DynParamQuotedString(string quotedString, string quoteCharacter) {
            OriginalString = quotedString;
            _quoteCharacter = quoteCharacter;
        }

        public string OriginalString { get; set; }
        string _quoteCharacter;

        public override string ToString() {
            // I'm sure this is missing some other characters that need to be escaped. Feel free to add more:
            if (System.Text.RegularExpressions.Regex.IsMatch(OriginalString, @"\s|\(|\)|""|'")) {
                return string.Format("{1}{0}{1}", OriginalString.Replace(_quoteCharacter, string.Format("{0}{0}", _quoteCharacter)), _quoteCharacter);
            }
            else {
                return OriginalString;
            }
        }

        public static string[] GetQuotedStrings(IEnumerable<string> values) {
            var returnList = new List<string>();
            foreach (string currentValue in values) {
                returnList.Add((new DynParamQuotedString(currentValue)).ToString());
            }
            return returnList.ToArray();
        }
    }


    [Cmdlet(VerbsCommon.Get, "BookDetails")]
    public class GetBookDetails : PSCmdlet, IDynamicParameters
    {
        IDictionary<string, string[]> m_dummyData = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase) {
            {"Terry Pratchett", new [] {"Small Gods", "Mort", "Eric"}},
            {"Douglas Adams", new [] {"Hitchhiker's Guide", "The Meaning of Liff"}},
            {"An 'Author' (notice the ')", new [] {"A \"book\"", "Another 'book'","NoSpace(ButCharacterThatShouldBeEscaped)", "NoSpace'Quoted'", "NoSpace\"Quoted\""}}   // Test value I added
        };

        protected override void ProcessRecord()
        {
            WriteObject(string.Format("Author = {0}", _author));
            WriteObject(string.Format("Book = {0}", ((DynParamQuotedString) MyInvocation.BoundParameters["Book"]).OriginalString));
        }

        // Making this static means it should keep track of the last author used
        static string _author;
        public object GetDynamicParameters()
        {
            // Get 'Author' if found, otherwise get first unnamed value
            string author = GetUnboundValue("Author", 0) as string;
            if (!string.IsNullOrEmpty(author)) { 
                _author = author.Trim('\'').Replace(
                    string.Format("{0}{0}", DynParamQuotedString.DefaultQuoteCharacter), 
                    DynParamQuotedString.DefaultQuoteCharacter
                ); 
            }

            var parameters = new RuntimeDefinedParameterDictionary();

            bool isAuthorParamMandatory = true;
            if (!string.IsNullOrEmpty(_author) && m_dummyData.ContainsKey(_author)) {
                isAuthorParamMandatory = false;
                var m_bookParameter = new RuntimeDefinedParameter(
                    "Book",
                    typeof(DynParamQuotedString),
                    new Collection<Attribute>
                    {
                        new ParameterAttribute {
                            ParameterSetName = "BookStuff",
                            Position = 1,
                            Mandatory = true
                        },
                        new ValidateSetAttribute(DynParamQuotedString.GetQuotedStrings(m_dummyData[_author])),
                        new ValidateNotNullOrEmptyAttribute()
                    }
                );

                parameters.Add(m_bookParameter.Name, m_bookParameter);
            }

            // Create author parameter. Parameter isn't mandatory if _author
            // has a valid author in it
            var m_authorParameter = new RuntimeDefinedParameter(
                "Author",
                typeof(DynParamQuotedString),
                new Collection<Attribute>
                {
                    new ParameterAttribute {
                        ParameterSetName = "BookStuff",
                        Position = 0,
                        Mandatory = isAuthorParamMandatory
                    },
                    new ValidateSetAttribute(DynParamQuotedString.GetQuotedStrings(m_dummyData.Keys.ToArray())),
                    new ValidateNotNullOrEmptyAttribute()
                }
            );
            parameters.Add(m_authorParameter.Name, m_authorParameter);

            return parameters;
        }

        /*
            TryGetProperty() and GetUnboundValue() are from here: https://gist.github.com/fearthecowboy/1936f841d3a81710ae87
            Source created a dictionary for all unbound values; I had issues getting ValidateSet on Author parameter to work
            if I used that directly for some reason, but changing it into a function to get a specific parameter seems to work
        */

        object TryGetProperty(object instance, string fieldName) {
            var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public;

            // any access of a null object returns null. 
            if (instance == null || string.IsNullOrEmpty(fieldName)) {
                return null;
            }

            var propertyInfo = instance.GetType().GetProperty(fieldName, bindingFlags);

            if (propertyInfo != null) {
                try {
                    return propertyInfo.GetValue(instance, null);
                } 
                catch {
                }
            }

            // maybe it's a field
            var fieldInfo = instance.GetType().GetField(fieldName, bindingFlags);

            if (fieldInfo!= null) {
                try {
                    return fieldInfo.GetValue(instance);
                } 
                catch {
                }
            }

            // no match, return null.
            return null;
        }

        object GetUnboundValue(string paramName) {
            return GetUnboundValue(paramName, -1);
        }

        object GetUnboundValue(string paramName, int unnamedPosition) {

            // If paramName isn't found, value at unnamedPosition will be returned instead
            var context = TryGetProperty(this, "Context");
            var processor = TryGetProperty(context, "CurrentCommandProcessor");
            var parameterBinder = TryGetProperty(processor, "CmdletParameterBinderController");
            var args = TryGetProperty(parameterBinder, "UnboundArguments") as System.Collections.IEnumerable;

            if (args != null) {
                var currentParameterName = string.Empty;
                object unnamedValue = null;
                int i = 0;
                foreach (var arg in args) {
                    var isParameterName = TryGetProperty(arg, "ParameterNameSpecified");
                    if (isParameterName != null && true.Equals(isParameterName)) {
                        string parameterName = TryGetProperty(arg, "ParameterName") as string;
                        currentParameterName = parameterName;

                        continue;
                    }

                    // Treat as a value:
                    var parameterValue = TryGetProperty(arg, "ArgumentValue");

                    if (currentParameterName != string.Empty) {
                        // Found currentParameterName's value. If it matches paramName, return
                        // it
                        if (currentParameterName.Equals(paramName, StringComparison.OrdinalIgnoreCase)) {
                            return parameterValue;
                        }
                    }
                    else if (i++ == unnamedPosition) {
                        unnamedValue = parameterValue;  // Save this for later in case paramName isn't found
                    }

                    // Found a value, so currentParameterName needs to be cleared
                    currentParameterName = string.Empty;
                }

                if (unnamedValue != null) {
                    return unnamedValue;
                }
            }

            return null;
        }
    }
}