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 m_dummyData = new Dictionary { {"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 { 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 { 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 ,则cmd.exe会这样做)。 因此,选项卡完成实际上会中断调用(如果上述问题将被解决, Get-BookDetails Ter会扩展到Get-BookDetails Terry Pratchett ,它将姓氏放在参数位置1,即“book”。

不应该这么难,肯定有人做过类似的事吗?

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

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

这是代码:

 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 values) { var returnList = new List(); foreach (string currentValue in values) { returnList.Add((new DynParamQuotedString(currentValue)).ToString()); } return returnList.ToArray(); } } [Cmdlet(VerbsCommon.Get, "BookDetails")] public class GetBookDetails : PSCmdlet, IDynamicParameters { IDictionary m_dummyData = new Dictionary(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 { 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 { 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; } } }