Как разобрать и выполнить строку в стиле командной строки?

У меня есть конкретный вопрос в конце, но я хочу предоставить много предыстории и контекста, чтобы вы были на одной странице в максимально возможной степени и могли понять мою цель.

Предыстория

Я создаю консольное приложение с ASP.NET MVC 3. Он находится по адресу http://www.u413.com , если вам нужно пойти туда и понять, как это работает. Сама концепция проста: получить строки команд от клиента, проверить, существует ли предоставленная команда и действительны ли аргументы, предоставленные с командой, выполнить команду, вернуть набор результатов.

Внутренняя работа

С В этом приложении я решил проявить немного творчества. Наиболее очевидным решением для приложения терминального типа является создание крупнейшего в мире оператора IF. Выполняйте каждую команду через оператор IF и вызывайте соответствующие функции изнутри. Мне эта идея не понравилась. В более старой версии приложения это было так, и это было огромным беспорядком. Добавление функций в приложение было до смешного сложно.

После долгих раздумий я решил создать собственный объект, названный командным модулем. Идея состоит в том, чтобы строить этот командный модуль с каждым запросом. Объект модуля будет содержать все доступные команды как методы, и сайт затем будет использовать отражение, чтобы проверить, соответствует ли команда, предоставленная пользователем, имени метода. Объект командного модуля находится за интерфейсом ICommandModule , показанным ниже.

namespace U413.Business.Interfaces
{
    /// 
    /// All command modules must ultimately inherit from ICommandModule.
    /// 
    public interface ICommandModule
    {
        /// 
        /// The method that will locate and execute a given command and pass in all relevant arguments.
        /// 
        /// The command to locate and execute.
        /// A list of relevant arguments.
        /// The current command context.
        /// The current controller.
        /// A result object to be passed back tot he client.
        object InvokeCommand(string command, List args, CommandContext commandContext, Controller controller);
    }
}

Метод InvokeCommand () - единственный метод в командном модуле, о котором мой контроллер MVC сразу узнает. В этом случае ответственность этого метода заключается в том, чтобы использовать отражение и посмотреть на его экземпляр и найти все доступные методы команд.

Я использую Ninject для внедрения зависимостей. Мой контроллер MVC имеет зависимость конструктора от ICommandModule . Я создал собственный поставщик Ninject, который создает этот командный модуль при разрешении зависимости ICommandModule . Ninject может создать 4 типа командных модулей:

  1. VisitorCommandModule
  2. UserCommandModule
  3. ModeratorCommandModule
  4. AdministratorCommandModule

Есть еще один класс BaseCommandModule , от которого наследуются все остальные классы модулей. Вкратце, вот отношения наследования:

  • BaseCommandModule: ICommandModule
  • VisitorCommandModule: BaseCommandModule
  • UserCommandModule: BaseCommandModule
  • ModeratorCommandModule: UserCommandModule
  • ModeratorCommandModule: UserCommandModule
  • Administrator], как вы можете видеть этот ModemandModule
  • Administrator, как вы можете видеть, как это вы можете видеть? к настоящему времени. В зависимости от статуса членства пользователя (не авторизованный, обычный пользователь, модератор и т. Д.) Ninject предоставит соответствующий командный модуль только с теми командными методами, к которым пользователь должен иметь доступ.

    Все это отлично работает. Моя дилемма возникает, когда я анализирую командную строку и выясняю, как структурировать методы команд в объекте командного модуля.

    Вопрос

    Как следует анализировать и выполнять командную строку?

    Текущее решение

    В настоящее время я разбиваю командную строку (строка, переданная пользователем, содержащая команду и все аргументы) в контроллере MVC. Затем я вызываю метод InvokeCommand () в моем внедренном ICommandModule и передаю команду string и List args .

    Допустим, у меня есть следующая команда:

    TOPIC  [page #] [reply “reply”]
    

    Эта строка определяет команду TOPIC, принимающую требуемый идентификационный номер, необязательный номер страницы и необязательную команду ответа со значением ответа.

    В настоящее время я реализую команду метод вроде этого (Атрибуты над методом предназначены для информации меню справки. Команда СПРАВКА использует отражение, чтобы прочитать все это и отобразить организованное меню справки):

        /// 
        /// Shows a topic and all replies to that topic.
        /// 
        /// A string list of user-supplied arguments.
        [CommandInfo("Displays a topic and its replies.")]
        [CommandArgInfo(Name="ID", Description="Specify topic ID to display the topic and all associated replies.", RequiredArgument=true)]
        [CommandArgInfo(Name="REPLY \"reply\"", Description="Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", RequiredArgument=false)]
        public void TOPIC(List args)
        {
            if ((args.Count == 1) && (args[0].IsInt64()))
                TOPIC_Execute(args); // View the topic.
            else if ((args.Count == 2) && (args[0].IsInt64()))
                if (args[1].ToLower() == "reply")
                    TOPIC_ReplyPrompt(args); // Prompt user to input reply content.
                else
                    _result.DisplayArray.Add("Subcommand Not Found");
            else if ((args.Count >= 3) && (args[0].IsInt64()))
                if (args[1].ToLower() == "reply")
                    TOPIC_ReplyExecute(args); // Post user's reply to the topic.
                else
                    _result.DisplayArray.Add("Subcommand Not Found");
            else
                _result.DisplayArray.Add("Subcommand Not Found");
        }
    

    Моя текущая реализация - огромный беспорядок. Я хотел избежать гигантских операторов IF, но все, что я сделал, это обменял один гигантский оператор IF на все команды, на тонну чуть менее гигантских операторов IF для каждой команды и ее аргументов. Это даже не половина дела; Я упростил эту команду для этого вопроса. В реальной реализации есть еще несколько аргументов, которые могут быть предоставлены этой командой, и этот оператор IF - самая уродливая вещь, которую я когда-либо видел. Это очень избыточно и совсем не СУХОЕ (не повторяйтесь), так как я должен отображать «Подкоманда не найдена» в трех разных местах.

    Достаточно сказать, мне нужно лучшее решение, чем это.

    Идеальная реализация

    В идеале мне хотелось бы структурировать свои командные методы примерно так:

    public void TOPIC(int Id, int? page)
    {
        // Display topic to user, at specific page number if supplied.
    }
    
    public void TOPIC(int Id, string reply)
    {
        if (reply == null)
        {
            // prompt user for reply text.
        }
        else
        {
            // Add reply to topic.
        }
    }
    

    Тогда я бы хотел сделать это:

    1. Получить командную строку от клиента.
    2. Передать командную строку непосредственно в InvokeCommand () на ICommandModule .
    3. InvokeCommand () выполняет некоторый волшебный синтаксический анализ и отражение, чтобы выбрать правильный метод команды с правильными аргументами, и вызывает этот метод, передавая только необходимые Аргументы

    Дилемма с идеальной реализацией

    Я не уверен, как структурировать эту логику. Я уже несколько дней чешу затылок. Мне жаль, что у меня не было второй пары глаз, чтобы помочь мне в этом (поэтому, наконец, прибегая к роману с вопросом SO). В каком порядке все должно происходить?

    Должен ли я извлечь команду, найти все методы с этим именем команды, затем перебрать все возможные аргументы, а затем перебрать аргументы моей командной строки? Как определить, что идет, куда и какие аргументы идут попарно. Например, если я просматриваю свою командную строку и нахожу Reply «reply» , как мне связать содержимое ответа с переменной ответа, встречая номер и предоставляя его для аргумент Id ?

    Я уверен, что сейчас сбиваю вас с толку. Позвольте мне проиллюстрировать несколько примеров командных строк, которые может передать пользователь:

    TOPIC 36 reply // Should prompt the user to enter reply text.
    TOPIC 36 reply "Hey guys what's up?" // Should post a reply to the topic.
    TOPIC 36 // Should display page 1 of the topic.
    TOPIC 36 page 4 // Should display page 4 of the topic.
    

    Как я узнаю, что нужно отправить 36 в параметр Id ? Как мне узнать, что нужно ответить парой «Привет, ребята, как дела?» и передать "Эй, ребята, как дела?" в качестве значения аргумента ответа в методе?

    Чтобы узнать, какую перегрузку метода вызывать, мне нужно знать, сколько аргументов было предоставлено, чтобы я мог сопоставить это число с перегрузкой метода команды, который принимает ту же самую количество аргументов. Проблема в том, `ТЕМА 36 ответ" Эй, ребята, как дела? " на самом деле это два аргумента, а не три в качестве ответа и "Привет, ребята ..."идут вместе как один аргумент.

    Я не против немного (или сильно) раздувать метод InvokeCommand () , если это означает, что весь сложный синтаксический анализ и бессмысленное отражение обрабатываются там и мои командные методы могут оставаться красивыми, чистыми и простыми в написании.

    Думаю, я просто ищу здесь некоторые идеи. Есть ли у кого-нибудь какие-нибудь творческие идеи для решения этой проблемы? Это действительно большая проблема, потому что аргумент Операторы IF в настоящее время очень усложняют написание новых команд для приложения. Команды - это та часть приложения, которую я хочу сделать очень простой, чтобы их можно было легко расширять и обновлять. Вот каков фактический метод команды TOPIC в моем приложении выглядит так:

        /// 
        /// Shows a topic and all replies to that topic.
        /// 
        /// A string list of user-supplied arguments.
        [CommandInfo("Displays a topic and its replies.")]
        [CommandArgInfo("ID", "Specify topic ID to display the topic and all associated replies.", true, 0)]
        [CommandArgInfo("Page#/REPLY/EDIT/DELETE [Reply ID]", "Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", false, 1)]
        public void TOPIC(List args)
        {
            if ((args.Count == 1) && (args[0].IsLong()))
                TOPIC_Execute(args);
            else if ((args.Count == 2) && (args[0].IsLong()))
                if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
                    TOPIC_ReplyPrompt(args);
                else if (args[1].ToLower() == "edit")
                    TOPIC_EditPrompt(args);
                else if (args[1].ToLower() == "delete")
                    TOPIC_DeletePrompt(args);
                else
                    TOPIC_Execute(args);
            else if ((args.Count == 3) && (args[0].IsLong()))
                if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
                    TOPIC_EditReplyPrompt(args);
                else if ((args[1].ToLower() == "delete") && (args[2].IsLong()))
                    TOPIC_DeleteReply(args);
                else if (args[1].ToLower() == "edit")
                    TOPIC_EditExecute(args);
                else if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
                    TOPIC_ReplyExecute(args);
                else if (args[1].ToLower() == "delete")
                    TOPIC_DeleteExecute(args);
                else
                    _result.DisplayArray.Add(DisplayObject.InvalidArguments);
            else if ((args.Count >= 3) && (args[0].IsLong()))
                if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
                    TOPIC_ReplyExecute(args);
                else if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
                    TOPIC_EditReplyExecute(args);
                else if (args[1].ToLower() == "edit")
                    TOPIC_EditExecute(args);
                else
                    _result.DisplayArray.Add(DisplayObject.InvalidArguments);
            else
                _result.DisplayArray.Add(DisplayObject.InvalidArguments);
        }
    

    Разве это не смешно? У каждой команды есть такой монстр, и это недопустимо. Я просто просматриваю в голове сценарии и то, как код может с этим справиться. Я очень гордился своим командным модулем setup, теперь, если бы я мог просто гордиться командным методом

    Хотя я не собираюсь отказываться от всей моей модели (командных модулей) для приложения, я определенно открыт для предложений. Меня больше всего интересуют предложения, связанные с синтаксическим анализом строки командной строки и сопоставлением ее аргументов с правильными перегрузками методов. Я уверен, что любое решение, которое я выберу, потребует значительного изменения дизайна, поэтому не бойтесь предлагать то, что вы считаете ценным, даже если я не обязательно воспользуюсь вашим предложением, это может привести меня на правильный путь. 1265] Edit: Удлинение длинного вопроса

    Я просто хотел очень быстро уточнить, что отображение команд на методы команд на самом деле не то, о чем я беспокоюсь. Меня больше всего беспокоит то, как разбирать и организовывать строку командной строки. В настоящее время метод InvokeCommand () использует очень простое отражение C # для поиска подходящих методов:

        /// 
        /// Invokes the specified command method and passes it a list of user-supplied arguments.
        /// 
        /// The name of the command to be executed.
        /// A string list of user-supplied arguments.
        /// The current command context.
        /// The current controller.
        /// The modified result object to be sent to the client.
        public object InvokeCommand(string command, List args, CommandContext commandContext, Controller controller)
        {
            _result.CurrentContext = commandContext;
            _controller = controller;
    
            MethodInfo commandModuleMethods = this.GetType().GetMethod(command.ToUpper());
            if (commandModuleMethods != null)
            {
                commandModuleMethods.Invoke(this, new object[] { args });
                return _result;
            }
            else
                return null;
        }
    

    Итак, как вы можете видеть, я не беспокоюсь о том, как найти методы команды, поскольку это уже работает . Я просто обдумываю хороший способ проанализировать командную строку, организовать аргументы, а затем использовать эту информацию, чтобы выбрать правильный метод / перегрузку команды с помощью отражения.

    ОБНОВЛЕНИЕ ДОГОВОРА

    Я объявил награду за этот вопрос. Я ищу действительно хороший способ проанализировать передаваемую мной командную строку. Я хочу, чтобы синтаксический анализатор определил несколько вещей:

    • Параметры. Определите параметры в командной строке.
    • Пары имя / значение. Определите пары имя / значение (например, [страница #]
    • Только значение. Только определение значения.

    Я хочу, чтобы они были идентифицированы с помощью метаданных при первой перегрузке метода команды. Вот список примеров методов, которые я хочу написать, украшенный некоторыми метаданными, которые будут использоваться анализатором при выполнении рефлексии. Я дам вам эти примеры методов и несколько примеров командных строк, которые должны соответствовать этому методу. Тогда я оставлю это на ваше усмотрение, хорошие люди SO, чтобы придумать хорошее решение для парсера.

    // Metadata to be used by the HELP command when displaying HELP menu, and by the
    // command string parser when deciding what types of arguments to look for in the
    // string. I want to place these above the first overload of a command method.
    // I don't want to do an attribute on each argument as some arguments get passed
    // into multiple overloads, so instead the attribute just has a name property
    // that is set to the name of the argument. Same name the user should type as well
    // when supplying a name/value pair argument (e.g. Page 3).
    
    [CommandInfo("Test command tests things.")]
    [ArgInfo(
        Name="ID",
        Description="The ID of the topic.",
        ArgType=ArgType.ValueOnly,
        Optional=false
        )]
    [ArgInfo(
        Name="PAGE",
        Description="The page number of the topic.",
        ArgType=ArgType.NameValuePair,
        Optional=true
        )]
    [ArgInfo(
        Name="REPLY",
        Description="Context shortcut to execute a reply.",
        ArgType=ArgType.NameValuePair,
        Optional=true
        )]
    [ArgInfo(
        Name="OPTIONS",
        Description="One or more options.",
        ArgType=ArgType.MultiOption,
        Optional=true
        PossibleValues=
        {
            { "-S", "Sort by page" },
            { "-R", "Refresh page" },
            { "-F", "Follow topic." }
        }
        )]
    [ArgInfo(
        Name="SUBCOMMAND",
        Description="One of several possible subcommands.",
        ArgType=ArgType.SingleOption,
        Optional=true
        PossibleValues=
        {
            { "NEXT", "Advance current page by one." },
            { "PREV", "Go back a page." },
            { "FIRST", "Go to first page." },
                { "LAST", "Go to last page." }
        }
        )]
    public void TOPIC(int id)
    {
        // Example Command String: "TOPIC 13"
    }
    
    public void TOPIC(int id, int page)
    {
        // Example Command String: "TOPIC 13 page 2"
    }
    
    public void TOPIC(int id, string reply)
    {
        // Example Command String: TOPIC 13 reply "reply"
    
        // Just a shortcut argument to another command.
        // Executes actual reply command.
        REPLY(id, reply, { "-T" });
    }
    
    public void TOPIC(int id, List options)
    {
        // options collection should contain a list of supplied options
    
        Example Command String: "TOPIC 13 -S",
                                "TOPIC 13 -S -R",
                                "TOPIC 13 -R -S -F",
                                etc...
    }
    

    Парсер должен принимать командную строку, использовать отражение, чтобы найти все возможные перегрузки командных методов, использовать отражение, чтобы прочитать атрибуты аргументов для помочь определить, как разделить строку на правильный список аргументов, затем вызвать соответствующую перегрузку метода команды, передав правильные аргументы.

8
задан halfer 22 April 2019 в 13:48
поделиться