Подтвердить что ты не робот

Модуль Powershell: динамические обязательные иерархические параметры

Так что я действительно хочу, это несколько полезное дополнение вкладки в модуле PS. Кажется, что ValidateSet - путь сюда.

К сожалению, мои данные динамичны, поэтому я не могу аннотировать параметр со всеми допустимыми значениями upfront. 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 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 - не повезло. Возможно ли это?

  • Как вы определяете значение по умолчанию для обязательного параметра? Не подходит этот глупый пример, но позвольте сказать, что вы можете хранить своего любимого автора. Отныне я хочу по умолчанию этого автора, но наличие указателя на автора по-прежнему является обязательным. Либо вы дали мне значение по умолчанию (и все еще можете указать что-то еще), либо вам нужно быть явным.

  • Завершение вкладок для строк с пробелами кажется странным/сломанным/ограниченным - потому что оно не заключает значение с кавычками (например, cmd.exe будет делать, например, если вы наберете dir C:\Program <tab>). Таким образом, завершение табуляции фактически прерывает вызов (если проблемы, описанные выше, будут разрешены, Get-BookDetails Ter<tab> будет/будет расширяться до Get-BookDetails Terry Pratchett, который помещает фамилию в позицию параметра 1 aka 'book'.

Не должно быть так сложно, конечно, кто-то уже сделал что-то подобное?

Обновление: после другого хорошего дня возиться и обманывать, я не вижу способа сделать эту работу. Командный элемент без апатии и будет повторяться снова и снова. В тот момент, когда я могу определить динамические параметры (GetDynamicParameters), я не могу получить доступ к их (текущим) значениям/видеть, к чему они привязаны - например. MyInvocation.BoundParameters равно нулю. Я оставлю вопрос открытым, но кажется, что это просто не поддерживается. Все примеры, которые я вижу, добавляют динамический параметр, основанный на значении статического, и это не имеет значения. Мудак.

4b9b3361

Ответ 1

Я думаю, что это работает. К сожалению, он использует рефлексию, чтобы получить доступ к некоторым частным членам командлета для вашей первой пули. Я получил идею от Garrett Serack. Я не уверен, полностью ли я понял, как сделать автора по умолчанию, поэтому я сделал это так, чтобы последний действительный автор был сохранен в статическом поле, поэтому вам не нужен -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<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 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 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 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;
        }
    }
}