Страницы

Поиск по вопросам

суббота, 6 октября 2018 г.

Как парсить аргументы командной строки?

Пишу консольное приложение которое должно принимать более 5 параметров. При этом есть обязательные параметры есть не обязательные, одни параметры должны работать только с другими и т.д.. Я уже погряз в различных if..else if..else. Как облегчить себе жизнь? Есть ли рекомендации и стандартные приёмы по работе с аргументами командной строки?


Ответ

Если параметров много, лучше всего вынести их разбор в отдельный класс или набор классов. Когда if/else образуют сложную структуру, можно использовать «повышенную декларативность» C# и описывать структуру команд, например, так:
var commandLinePattern = Command.WithName('add') .HasOption('-fileName')
| Command.WithName('help') .HasParameter("CommandName");
Конечно, это потребует определённой изобретательности при проектировании, так что ниже изложу подробнее.
Следующим шагом мы опишем наши команды:
public interface ICommand { void Run(); }
public HelpCommand : ICommand { public string CommandName { get; set; }
public bool Verbose { get; set; }
public int PageSize { get; set; }
public void Run() { if (CommandName == "add") { Console.WriteLine("Help on the ADD command"); } . . . } }
Шаблон командной строке (переменная commandLinePattern в первом примере) можно применить к конкретной строке параметров, чтобы построить экземпляр конкретного класса команды, и заполнить его свойства, использую рефлексию:
var command = commandLinePattern.Parse(args); command.Run();
UPDATE
Итого, получается приблизительно такой код:
static class Program { private static readonly commandLinePatterm = Command.WithName('add') .HasOption('-fileName') .HasOption('-move')
| Command.WithName('help') .Parameter("CommandName");
static void Main(string[] args) { var command = commandLinePattern.Parse(args); command.Run(); } }
UPDATE «для новичка»
Немного о том, как реализовать всю эту красоту. Наше чудо по внешнему виду и внутреннему устройству очень напоминает регулярные выражения: сначала мы описываем некий шаблон аргументов командной строки, и затем применяем его к реальным аргументам. Результатом такого применения будет объект, который можно будет выполнить. Назовём его командой
Название команда возникло не просто так — это один из паттернов проектирования, описанный в классическом труде банды четырёх.
Я не знаю, какие командные строки вам приходится разбирать, поэтому сделаю несколько предположений:
Программа может выполнить за один запуск одну и только одну команду. Команда идёт первой в командной строке. Команда не требует префиксов, таких как дефис или косая черта. За командой могут следовать один или несколько параметров. Параметры могут быть именованными и безымянными. Именованные параметры имеют префикс, например, дефис. Именованные параметры могут иметь значение, в этом случае оно отделяется от имени двоеточием.
Приблизительно такие правила используются в большом количестве утилит, например, в архиваторах pkzip, arj и rar; в утилитах контроля версий git и hg — то есть этих двух правил действительно хвататет для широкого спектра задач.
arj add archive *.c -m0
arj — это имя программы, add — команда, archive (имя архива) и *.c (что архивировать) — безымянные параметры; -m — именованный параметр (опция), 0 — значение параметра.
В arj значение параметра не требуется отделять символом : или =. Мы пойдём другим путём, исключительно для из-за того, что пример демонстрационный.
Такой шаблон параметра командной строки может быть описан простым классом:
public class CommandLinePattern { public string Name { get; set; }
public List Parameters { get; set; }
public List Options { get; set; } }
Например, команда help может иметь такой шаблон
program Help CommandName [-Verbose] [-PageSize:num]
Которому соответствует экзмепляр класса CommandLinePattern
var pattern = new CommandLinePattern { Name = "Help", Parameters = new List { "CommandName" }, Options = new List { "Verbose", "PageSize" }, };
Нам нужно решить две задачи: построить такой шаблон, и применить его. В коде выше мы воспользовались статическим классом Command для того, чтобы начать конструировать шаблон:
public static class Command { public static CommandLinePattern WithName(string name) { return new CommandLinePattern { Name = name, Parameters = new List(), Options = new List(), }; } }
Остальные методы конструирования разместим непосредственно в классе CommandLinePattern
public CommandLinePattern HasOption(string name) { Options.Add(name);
return this; }
public CommandLinePattern HasParameter(string name) { Parameters.Add(name);
return this; }
Благодаря конструкции return this; мы можем строить объект последовательно, увязывая методы в цепочку:
var pattern = Command.WithName("Help") .HasParameter("CommandName") .HasOption("Verbose") .HasOption("PageSize");
Таким образом мы описываем шаблоны команды. Теперь решим вторую задачу — разбор командной строки и создание объекта-команды.
В классе CommandLinePattern реализуем метод TryParse, который будет пробовать разобрать командную строку и в случае успеха создавать объект, реализующий такой интерфейс ICommand
public virtual bool TryParse(string[] args, out ICommand result) { result = null; // Конечно, наш шаблон не может соответствовать пустой командной строке. if (args.Length == 0) return false;
// И он не может соответствовать какой-то другой команде. if (args[0] != Name) return false;
var properties = new Dictionary(); var nextParameterIndex = 0;
for (int i = 1; i < args.Length; i++) { if (args[i].StartsWith("-")) { var parameterWithoutHyphen = args[i].Substring(1); var nameValue = parameterWithoutHyphen.Split(':'); if (nameValue.Lenth == 1) properties.Add(nameValue[0], null); else properties.Add(nameValue[0], nameValue[1]); } else { var name = Parameters[nextParameterIndex]; nextParameterIndex++;
var value = args[i]; properties.Add(name, value); } }
// Для команды с имененем Help мы найдём класс HelpCommand: var className = Name + "Command"; var type = Type.GetType(className); // И создадим его экземпляр: result = (ICommand)Activator.CreateClass(type);
// Теперь значения всех параметров запишем в свойства // только что созданного экземляра: foreach (var property in properties) { var name = property.Key; var value = property.Value;
type.GetProperty(name) .SetValue(result, value); }
return true; }
public virtual ICommand Parse(string[] args) { ICommand result; if (TryParse(args, out result)) return result;
throw new FormatException(); }
Имейте в виду, что такая запись значений будет работать только со строковыми свойствами — в реальной программе вам потребуется конвертировать строковые значения в типы свойств.
Теперь у нас уже всё готово. Мы можем написать наш разбор параметров, но «для красоты» сделаем ещё один финальный штрих.
Создадим класс, который позволит объединить два шаблона и проверять сначала левый, а затем правый:
public class OrCommandLinePattern : CommandLinePattern { private readonly CommandLinePattern left; private readonly CommandLinePattern right;
public OrCommandLinePattern(CommandLinePattern left, CommandLinePattern right) { this.left = left; this.right = right; }
public override bool TryParse(string[] args, out ICommand result) { if (left.TryParse(args, result)) return true;
return right.TryParse(args, result); } }
В базовый класс добавим реализацию оператора ИЛИ
public static CommandLinePattern operator |(CommandLinePattern left, CommandLinePattern right) { return new OrCommandLinePattern(left, right); }
Теперь мы можем объединить несколько паттернов с помощью вертикальной черты:
var commandLinePattern = Command.WithName('add') .HasOption('-fileName')
| Command.WithName('help') .HasParameter("CommandName");
И в конце запускаем парсинг с помощью вызова одного единственного метода:
var command = commandLinePattern.Parse(args);
Результатом работы парсера будет экземпляр класса, реализующего интерфейс ICommand с заполненными свойствами. Нам остаётся только запустить его:
command.Run();

Комментариев нет:

Отправить комментарий