So what I really want is somewhat usable tab completion in a PS module. ValidateSet seems to be the way to go here.
Unfortunately my data is dynamic, so I cannot annotate the parameter with all valid values upfront. DynamicParameters/IDynamicParameters seems to be the solution for that problem.
Putting these things together (and reducing my failure to a simple test case) we end up with:
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; } } }
Unfortunately this tiny snippet causes a lot of issues already. Ordered descending:
I fail to see how I can create a connection between the parameters. If you pick an author, you should only be able to pick a book that matches the author. So far GetDynamicParameters()
always seems stateless though: I see no way to access the value of a different/earlier dynamic parameter. Tried keeping it in a field, tried searching MyInvocation
- no luck. Is that even possible?
How do you define a default value for mandatory parameter? Doesn't fit the silly example, but let's say you can store your favorite author. From now on I want to default to that author, but having a pointer to an author is still mandatory. Either you gave me a default (and can still specify something else) or you need to be explicit.
Tab completion for strings with spaces seems weird/broken/limited - because it doesn't enclose the value with quotes (like cmd.exe would do, for example, if you type dir C:\Program <tab>
). So tab completion actually breaks the invocation (if the issues above would be resolved, Get-BookDetails Ter<tab>
would/will expand to Get-BookDetails Terry Pratchett
which puts the last name in parameter position 1 aka 'book'.
Shouldn't be so hard, surely someone did something similar already?
Update: After another good day of tinkering and fooling around I don't see a way to make this work. The commandlet is stateless and will be instantiated over and over again. At the point in time when I can define dynamic parameters (GetDynamicParameters) I cannot access their (current) values/see what they'd be bound to - e.g. MyInvocation.BoundParameters is zero. I'll leave the question open, but it seems as if this just isn't supported. All the examples I see add a dynamic parameter based on the value of a static one - and that's not relevant here. Bugger.
I think this works. Unfortunately, it uses reflection to get at some of the cmdlet's private members for your first bullet. I got the idea from Garrett Serack. I'm not sure if I completely understood how to do the default author, so I made it so that the last valid author is stored in a static field so you don't need -Author the next time.
Here's the code:
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; } } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With