c# – Как парсить аргументы командной строки? – Stack Overflow на русском

Argparse — парсим аргументы и параметры командной строки с легкостью

Начиная с версии Python 2.7, в набор стандартных библиотек была включена библиотека argparse для обработки аргументов (параметров, ключей) командной строки. Хотелось бы остановить на ней Ваше внимание.

Для начала рассмотрим, что интересного предлагает argparse.

Argparse — это изящный инструмент для:

  • анализа аргументов sys.argv;
  • конвертирования строковых аргументов в объекты Вашей программы и работа с ними;
  • форматирования и вывода информативных подсказок;
  • многого другого.

Одним из аргументов противников включения argparse в Python был довод о том, что в стандартных модулях и без этого содержится две библиотеки для семантической обработки (парсинга) параметров командной строки. Однако, как заявляют разработчики argparse, библиотеки getopt и optparse уступают argparse по нескольким причинам:

  • обладая всей полнотой действий с обычными параметрами командной строки, они не умеют обрабатывать позиционные аргументы (positional arguments). Позиционные аргументы — это аргументы, влияющие на работу программы, в зависимости от порядка, в котором они в эту программу передаются. Простейший пример — программа cp, имеющая минимум 2 таких аргумента («cp source destination»).
  • argparse дает на выходе более качественные сообщения о подсказке при минимуме затрат (в этом плане при работе с optparse часто можно наблюдать некоторую избыточность кода);
  • argparse дает возможность программисту устанавливать для себя, какие символы являются параметрами, а какие нет. В отличие от него, optparse считает опции с синтаксисом наподобии “-pf, -file, rgb, /f и т.п. «внутренне противоречивыми» и «не поддерживается optpars’ом и никогда не будет»;
  • argparse даст Вам возможность использовать несколько значений переменных у одного аргумента командной строки (nargs);
  • argparse поддерживает субкоманды (subcommands). Это когда основной парсер отсылает к другому (субпарсеру), в зависимости от аргументов на входе.

Для начала работы с argparse необходимо задать парсер:

ap.py:
import argparse
parser = argparse.ArgumentParser(description='Great Description To Be Here')

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

parser.add_argument('-n', action='store', dest='n', help='Simple value')

Если действие (action) для данного аргумента не задано, то по умолчанию он будет сохраняться (store) в namespace, причем мы также можем указать тип этого аргумента (int, boolean и тд). Если имя возвращаемого аргумента (dest) задано, его значение будет сохранено в соответствующем атрибуте namespace.

В нашем случае:

print parser.parse_args(['-n', '3'])

Namespace(n='3')

print parser.parse_args([])

Namespace(n=None)

print parser.parse_args(['-a', '3'])
error: unrecognized arguments: -a 3

Простой пример программы, возводящей в квадрат значение позиционного аргумента (square) и формирующей вывод в зависимости от аргумента опционального (-v):


import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbose:
    print("the square of {} equals {}".format(args.square, answer))
else:
    print(answer)

Остановимся на действиях (actions). Они могут быть следующими:
— store: возвращает в пространство имен значение (после необязательного приведения типа). Как уже говорилось, store — действие по умолчанию;

— store_const: в основном используется для флагов. Либо вернет Вам значение, указанное в const, либо (если ничего не указано), None. Пример:

parser.add_argument('--LifetheUniverseandEverything', action='store_const', const=42)
print parser.parse_args(['--LifetheUniverseandEverything'])

Namespace(LifetheUniverseandEverything=42)

— store_true / store_false: аналог store_const, но для булевых True и False;

— append: возвращает список путем добавления в него значений агрументов. Пример:

parser.add_argument('--l', action='append')
print parser.parse_args('--l a --l b --l Y'.split())

Namespace(l=['abY'])

— append_const: возвращение значения, определенного в спецификации аргумента, в список. Пока у меня не было кейса, в котором понадобился бы append_const.

— count: как следует из названия, считает, сколько раз встречается значение данного аргумента. Пример:

parser.add_argument('--verbose', '-v', action='count')
print parser.parse_args('-vvv'.split()) 

Namespace(verbose=3)

В зависимости от переданного в конструктор парсера аргумента add_help (булевого типа), будет определяться, включать или не включать в стандартный вывод по ключам [‘-h’, ‘–help’] сообщения о помощи. То же самое будет иместь место с аргументом version (строкового типа), ключи по умолчанию: [‘-v’, ‘–version’]. При запросе помощи или номера версии, дальнейшее выполнение прерывается.

parser = argparse.ArgumentParser(add_help=True, version='4.0')

Иногда необходимо определить некий набор параметров командной строки, который будет распространяться на все парсеры Вашей программы. В данном случае часто приводят пример необходимости авторизации:

parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--user', action="store")
parent_parser.add_argument('--password', action="store")
child_parser = argparse.ArgumentParser(parents=[parent_parser])
child_parser.add_argument('--show_all', action="store_true")
print child_parser.parse_args(['--user', 'guest'])

Namespace(password=None, show_all=False, user='guest')

Обратите внимание, что родительский парсер создается с параметром add_help=False. Это сделано потому, что каждый парсер будет честно стараться добавить свой обработчик ключа ‘-h’, чем вызовет конфликтную ситуацию. Отсюда возникает вопрос, что делать, если у вашего дочернего парсера имеются те же ключи, что и у родительского и при этом вы хотите их использовать без всяких конфликтов? Делается это простым добавлением аргумента conflict_handler:

parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--user', action="store")
parent_parser.add_argument('--password', action="store")
child_parser = argparse.ArgumentParser(parents=[parent_parser], conflict_handler='resolve')
child_parser.add_argument('--user', action="store", default="Guest")
print child_parser.parse_args()

Namespace(password=None, user='Guest')

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

ap.py
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='List of commands')
# A list command
list_parser = subparsers.add_parser('list', help='List contents')
list_parser.add_argument('dirname', action='store', help='Directory to list')
# A create command
create_parser = subparsers.add_parser('create', help='Create a directory')
create_parser.add_argument('dirname', action='store', help='New directory to create')
create_parser.add_argument('--read-only', default=False, action='store_true',
                           help='Set permissions to prevent writing to the directory',
                           )

Вот что выдаст программа с ключом ‘-h’:

:/>  Как удалить историю RDP соединений в Windows | .

usage: ap.py [-h] {list,create} ...

positional arguments:
{list,create} list of commands
list List contents
create Create a directory

optional arguments:
-h, –help show this help message and exit

В примере можно отметить следующие вещи:
1. позиционные аргументы list, create, передаваемые программе — по сути субпарсеры;
2. аргумент ‘–read-only’ субпарсера create_parser — опциональный, dir_name — необходимый для обоих субпарсеров;
3. предусмотрена справка (помощь) как для парсера, так и для каждго из субпарсеров.

Вообще, argparse — довольно мощная и легкая библиотека, предоставляющая, на мой взгляд, очень удобный интерфейс для работы с параметрами командной строки. В следующий раз постараюсь охватить такие вопросы, как файловые аргументы (для более продвинутой работы с файлами), переменные списки аргументов, группы аргументов и более подробно остановиться на типизации аргументов.
Спасибо за внимание.

Список литературы:
Why aren’t getopt and optparse enough (PEP 389)
Argparse positional and optional arguments
Argparse documentation
Parser for command-line options, arguments and sub-commands

Commandlineparser

CommandLineParser – это библиотека с открытым исходным кодом, созданная Эриком Ньютоном и членами сообщества .NET. Она существует с 2005 года и её скачали более 26 миллионов раз! CommandLineParser «предлагает приложениям CLR простой и лаконичный API для управления аргументами командной строки и связанными задачами, такими как определение переключателей, параметров и команд».

Вместо ручного парсинга массива строк args вы можете просто определить класс, который будет парситься для вас библиотекой на основе набора атрибутов, с которыми вы аннотируете класс.

Вместо того, чтобы создавать еще один пример только для демонстрации этой библиотеки, я буду использовать консольное приложение WinML .NET5, которым я поделился в своем предыдущем посте. Вот исходный код. Начнем с этого и добавим NuGet-пакет CommandLineParser:

Давайте создадим новый класс с именем CommandLineOptions:

using CommandLine;

namespace ImageClassifier
{
    public class CommandLineOptions
    {
        [Value(index: 0, Required = true, HelpText = "Путь к файлу изображения для анализа.")]
        public string Path { get; set; }

        [Option(shortName: 'c', longName: "confidence", Required = false, HelpText = "Minimum confidence.", Default = 0.9f)]
        public float Confidence { get; set; }
    }
}

Это почти все, что нам нужно для использования этой библиотеки. ValueAttribute и OptionAttribute предоставляются пакетом. Я использую именованные параметры, чтобы было ясно, для чего нужен каждый аргумент. Вернемся к нашему методу Program.cs Main, добавим оператор using, чтобы иметь возможность легко использовать классы пакета в этом файле:

using CommandLine;

Давайте изменим тип возвращаемого значения нашего метода Main на Task. Это означает, что любое возвращаемое нами значение int будет возвращено вызывающей стороне нашего процесса, что обычно указывает на успех/неудачу. В этом примере мы просто вернем 0 в случае успеха и любое другое значение, кроме 0, в случае ошибки:

static async Task Main(string[] args)
{
    return await Parser.Default.ParseArguments<CommandLineOptions>(args)
        .MapResult(async (CommandLineOptions opts) =>
        {
            try
            {
                // У нас есть полученные аргументы, поэтому давайте просто передадим их
                return await AnalyzeFileAsync(opts.Path, opts.Confidence);
            }
            catch
            {
                Console.WriteLine("Error!");
                return -3; // Unhandled error
            }
        },
        errs => Task.FromResult(-1)); // Invalid arguments
}

Здесь вы можете увидеть все изменения по сравнению с предыдущей версией кода.

:/>  Windows 7 домашняя базовая системные требования

С этими изменениями приложение корректно анализирует наши аргументы. Для нас даже есть страница помощи, созданная автоматически!

Допустим, вы хотите проанализировать изображение, но хотите получить результат, даже если вы не слишком уверены в нем, скажем, с доверием 30%. Теперь это легко сделать с помощью аргумента -c (–confidence). С этим изображением:

Вы можете получить этот результат, используя –confidence:

String parsing using cmd commands

I have a cmd command that gives me output similar to below

“Targetid = 12345”

The above string consists of r t n as well.
Can you please suggest a cmd command that can help me just get the number “12345” when passed with such a string

Edit: the cmd command that i use to get above string is as follow

"D:mytestmySoftware Query <customerGUID> | findstr /R /C:""Targetid"" "

Как парсить txt файл с помощью bat файла?

в cmd для того чтобы присвоенное значение использовалось далее
в строках

set foldername=%%a
  md %Src%%foldername%

надо использовать

set foldername=%%a
  md %Src%!foldername!

и в самом начале написать
SETLOCAL ENABLEDELAYEDEXPANSION

учтите п-та: в скрипте еще несколько ошибок имеется…

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

Если параметров много, лучше всего вынести их разбор в отдельный класс или набор классов. Когда 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 СommandLinePattern commandLinePattern = 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 «для новичка»

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

Название команда возникло не просто так — это один из паттернов проектирования, описанный в классическом труде банды четырёх.

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

  1. Программа может выполнить за один запуск одну и только одну команду. Команда идёт первой в командной строке. Команда не требует префиксов, таких как дефис или косая черта.
  2. За командой могут следовать один или несколько параметров. Параметры могут быть именованными и безымянными. Именованные параметры имеют префикс, например, дефис. Именованные параметры могут иметь значение, в этом случае оно отделяется от имени двоеточием.

Приблизительно такие правила используются в большом количестве утилит, например, в архиваторах 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<string> Parameters { get; set; }

    public List<string> Options { get; set; }
}

Например, команда help может иметь такой шаблон

program Help CommandName [-Verbose] [-PageSize:num]

Которому соответствует экзмепляр класса CommandLinePattern:

var pattern = new CommandLinePattern
{
    Name = "Help",
    Parameters = new List<string> { "CommandName" },
    Options = new List<string> { "Verbose", "PageSize" },
};

Нам нужно решить две задачи: построить такой шаблон, и применить его. В коде выше мы воспользовались статическим классом Command для того, чтобы начать конструировать шаблон:

public static class Command
{
    public static CommandLinePattern WithName(string name)
    {
        return new CommandLinePattern
        {
            Name = name,
            Parameters = new List<string>(),
            Options = new List<string>(),
        };
    }
}

Остальные методы конструирования разместим непосредственно в классе 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");

Таким образом мы описываем шаблоны команды. Теперь решим вторую задачу — разбор командной строки и создание объекта-команды.

:/>  Using DISM cleanup-image for a Simple Windows 10 Repair

В классе 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<string, string>();
    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  ];

            var value = args[i];
            properties.Add(name, value);
        }
    }

    // Для команды с имененем Help мы найдём класс HelpCommand:
    var className = Name   "Command";
    var type = Type.GetType(className);
    // И создадим его экземпляр:
    result = (ICommand)Activator.CreateInstance(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, out result))
            return true;

        return right.TryParse(args, out 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();

UPDATE — готовая реализация (декабрь 2021)

В конце концов написал готовый код и выложил его в виде NuGet-проекта.

https://www.nuget.org/packages/Binateq.CommandLine/ — пакет

https://github.com/binateq/command-line-parser — исходный код на GitHub

Заключение

Пакет NuGet CommandLineParser – очень мощный помощник, который упрощает эту часто повторяющуюся задачу до простого декларативного подхода. Кроме того, он даже еще более кастомизируемый, чем я продемонстрировал здесь. Вы можете найти его документацию на их странице GitHub вики.

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