Дата и время публикации:
Проблемы и решение
1. Суть проблемы
Выпущенная версия python-shellenv-1.5, в конце марта 2022 года, имела один недостаток — отсутствие аргументов командной строки, что приводило к печати на консоль вывода, полученного в результате выполения подмодуля shellenv.testrun:functionality_check(), как показано в дампе 1.1
Дамп 1.1
(venv-shellenv) ~/.../venv-shellenv/src/python-shellenv/$ bin/py-shellenv Script's name: testrun.py Call print_all() from module shellenv: *** Print all environment variables *** PASSED Call nmaxhitsenv()/nminhitsenv() from module shellenv: *** Scans for hits *** SHELL:0 may be a maximum compliance with: SHERRYHW_NAME SESSION_MANAGER:1 may be a minimum compliance with: SHERRYHW_NAME PASSED Call setenv('SHERRYHW_NAME') from module shellenv: PASSED Call run_unsetenv('MYVAR' and 'SHERRYHW_NAME') from module shellenv: *** test to unsetenv() when envar does not exists *** Sure, MYVAR's does not exists - it's right *** test to unsetenv() test to unsetenv() to removes SHERRYHW_NAME *** It was not unexpected that SHERRYHW_NAME had the value 'Sherry platform' PASSED Call dotenv from module shellenv: *** test to read file [${HOME}/.config/shellenv/].env *** Create tested directory: /.config/shellenv/ Create tested dotfile: /home/user/.config/shellenv/.env Try to read recently recorded envars: SHELLENV_DIR: /.config/shellenv/ DOMAIN: localhost USER_EMAIL: user@localhost PASSED
2. Решение
2.1 Модуль разбора комадной строки argparse
Первое, что мне понадобилось — это видоизменить точку входа в модуль shellenv.__main__:main для разбора командной строки, как показано в листинге 2.1
Листинге 2.1
... import typing as t import argparse ... def main(argv: t.Sequence[str] = None) -> None: ... parser = argparse.ArgumentParser(prog='shellenv', usage='%(prog)s [options]') ...
Разбор командной строки осуществляется с использованием модуля argparse, который предоставляет большую функциональность чем давно существующие модули из стандартной библиотеки, такие как getopt и optparse [3.3]. Кроме того, этот модуль поддерживает не только опции позиционные аргументы, необязательные и обязательные опции, а так же опции с иным синтаксисом чем в *nix-системах, наподобие “/f” и “+rgb” , а так же подкоманды и многие другие опции [3.4]. При этом, в своем модуле shellenv решил придерживаться стилистики представления опций в командной строке, как приянто в *nix-системах.
2.2 Определение необязательных опций
К необязательным опциям, в моем случае, относится встроенная опция -h, --help и объявленная -V, --version, как показано в листинге 2.2.1
Листинге 2.2.1
... # Unessential option parser.add_argument( "-V","--version", action="vprog", version=str('The %(prog)s'+'-'+__version__), help=str("returns help page and exit immediate") ) #These also include help options which built into ArgumentParser ...
Как показано в листинге 2.2, присвоенное строковое значение "version" атрибуту action является одним из установленных действий, которому устанавливается базовый класс [3.7], выбираемый с помощью 'store_const', 'store_true', 'store_false', 'append', 'append_const', 'count', 'help', 'version', 'parsers' [3.6].
2.3 Разбор аргументов командной строки
В версии модуля shellenv-1.6 был добавлен подмодуль cmdlineargs пакета python-shellenv, в котором была внесена функция shellenv.cmdlineargs:mloop_exec() , в котором во время разбора аргументов командной строки производится запуск обработчиков, назначенных атрибуту action [3.5] и образованных от базового класса argparse.Action и порожденного от него класса _CommonAction, как показано в листинге 2.3.1
Листинге 2.3.1
class _CommonAction(argparse.Action): """ Defining the Common Action to set an option handler """ def __init__(self, catcher, **kwargs ) : ... ... class _TestRunAction(_CommonAction): """ Defining Action for option --test """ def __init__(self, option_strings, dest, nargs=None, **kwargs ): ... @classmethod def catcher(cls, **kwargs ): ... ... class _OutputAllRunAction(_CommonAction): """ Defining Action for option --output-all """ def __init__(self, option_strings, dest, nargs=None, **kwargs ): ... @classmethod def catcher(cls, **kwargs ): ... ... class _GetenvAllRunAction(_CommonAction): """ Defining Action for option --getenv vkey """ def __init__(self, option_strings, dest, nargs=None, **kwargs ): ... @classmethod def catcher(cls, **kwargs ): ...
При этом, как можно заметить, что конструктор в базовом классе _CommonAction() принимает переменное число параметров в **kwargs и метод обратного вызова, который будет исполняться в методе catcher в производных классах _TestRunAction(), _OutputAllRunAction(), _GetenvAllRunAction(), как показано в дампе 2.3.1
Вышеперечисленные производные классы позволяют засчет сериализации следовать принятому в Python принципу неповторяемости кода (Don't Repeat Yourself, DRY), который заключается в устранении повторяемости кода при назначении класса, образующий объект действия и которое должно соответствовать следующему образцу, как показано в 2.3.2
Листинге 2.3.2
... Сlass _MyAction(argparse.Action): def __init__(self, option_strings, dest, nargs=None, **kwargs ): super(_MyAction, self).__init__( option_strings=option_strings, dest=dest, nargs=nargs ,**kwargs ) def __call__(self, parser, namespace, values, option_string=None): print('Run option: namespace=%r, values=%r, option_string=%r' % (namespace, values, option_string)) setattr(namespace, self.dest, values) ...
В конструкторе класса _MyAction:__init__() непосредственно осуществляется вызов к конструктору базового класса argparse.Action:__init__() через встроенную функцию super() в CPython
При этом,
- в конструкторе _MyAction:__init__() принято указывать первые три принимаемых функцией parser.add_argument() параметра option_strings, dest и nargs, а остальные передаются в **kwargs, как и было показано в листинге 2.3.2;
- если не выполнять сериализацию с использованием класса _CommonAction, по сути один и тот же код из листинга 2.3.2 будет многократно растирожирован и будет нарушен, приянтый в Python принцип DRY
Функция parser.add_argument() принимает и передает на конструктор класса _MyAction:__init__() следующие параметры, которые были использованы в модуле python-shellenv, в настоящей версии:
- option_strings — список строк командной строки, которые должны ассоциироваться с назначаемым действием, обычно в них содержится текстовое занчение опции,
- dest — имя атрибута, который содержит создаваемый объект,
- nargs — указывается число передаваемых аргументов, в данном пример, N=1, как показано в листинге 2.3.3
- help — текстовая строка со справкой, если такая используется, она будет выведена во время вывода --help
- required — должна иметь булево значение True, если данная опция должна быть всегда указана в комнадной строке. Иначе, она должна быть иметь значение False ;
- action — указывается имя одного из классов _TestRunAction, _OutputAllRunAction и т.д., которые реализуют обработку аргументов и выполнения указанного действия согласно текстовой метки опции в option_strings
В настоящем примере для функции mloop_exec(), аргументы командной строки [3.1] могут быть поданы в единственном параметре argv или беруться из sys.argv() [3.2], как показано в листинге 2.3.3
Листинге 2.3.3
def mloop_exec(self,argv: t.Sequence[str] = None)→None: ... if argv is None: # argv[0] is the script name so we may be skip it and start from # first item argv = sys.argv[1:] parser = argparse.ArgumentParser(prog='shellenv', usage='%(prog)s [options]') # Unessential option parser.add_argument( "-V","--version", action="version", version=str('The %(prog)s'+'-'+__version__), help=str("returns help page and exit immediate") ) parser.add_argument( "-t","--test", required=False, help=str("tests all methods for functionality checking"), action=_TestRunAction ) parser.add_argument( "-O","--output-all", required=False, help=str("output all of shell enviroment variables"), action=_OutputAllRunAction ) parser.add_argument("--getenv", nargs=1, required=False, help=str("""finds and prints all shell variables on its keyword or only one variable when it has matched vkey"""), action=_GetenvAllRunAction ) args = parser.parse_args(argv) parser.print_help() sys.exit(1)
После установки обработчиков опциям и аргументам командной строки с использованием parser.add_argument() выполняется разбор с помощью parser.parse_args() . И, если ни одна из опций "--test", "--output-all" или "--getenv" в ней не была обнаружена, соответственно не было выполнено ни одно из действий назначенных в классов _TestRunAction, _OutputAllRunAction и т.д., будет выведена справка parser.print_help() и скрипт завершится с возвращаемым положительным значением.
При этом, в argparse.ArgumentParser() устанавливается имя программы в атрибуте prog= и ее вывод в справки в атрибуте usage через магическую строку '%(prog)s', которая присвоит название выполняемой программы.
3. Библиография
3.1 How to use sys.argv in Python
3.2 Command-line list of the module sys in python
3.3 argparse— Parser for command-line options, arguments and sub-commands
3.4 PEP 389 — argparse — New Command Line Parsing Module
3.5 argparse — attribute Action