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’:
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
}
Здесь вы можете увидеть все изменения по сравнению с предыдущей версией кода.
С этими изменениями приложение корректно анализирует наши аргументы. Для нас даже есть страница помощи, созданная автоматически!
Допустим, вы хотите проанализировать изображение, но хотите получить результат, даже если вы не слишком уверены в нем, скажем, с доверием 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 файла?
в строках
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 «для новичка»
Немного о том, как реализовать всю эту красоту. Наше чудо по внешнему виду и внутреннему устройству очень напоминает регулярные выражения: сначала мы описываем некий шаблон аргументов командной строки, и затем применяем его к реальным аргументам. Результатом такого применения будет объект, который можно будет выполнить. Назовём его командой.
Название команда возникло не просто так — это один из паттернов проектирования, описанный в классическом труде банды четырёх.
Я не знаю, какие командные строки вам приходится разбирать, поэтому сделаю несколько предположений:
- Программа может выполнить за один запуск одну и только одну команду. Команда идёт первой в командной строке. Команда не требует префиксов, таких как дефис или косая черта.
- За командой могут следовать один или несколько параметров. Параметры могут быть именованными и безымянными. Именованные параметры имеют префикс, например, дефис. Именованные параметры могут иметь значение, в этом случае оно отделяется от имени двоеточием.
Приблизительно такие правила используются в большом количестве утилит, например, в архиваторах 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");
Таким образом мы описываем шаблоны команды. Теперь решим вторую задачу — разбор командной строки и создание объекта-команды.
В классе 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 вики.