Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 380 Vote(s) - 3.53 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Powershell module: Dynamic mandatory hierarchical parameters

#1
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.
Reply

#2
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][1]. 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:
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;
}
}
}

[1]:
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through