Книга: PyNEng
Назад: Модуль ipaddress
Дальше: Модуль tabulate

Модуль argparse

Модуль argparse

argparse - это модуль для обработки аргументов командной строки.

Примеры того, что позволяет делать модуль:

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

argparse не единственный модуль для обработки аргументов командной строки.
И даже не единственный такой модуль в стандартной библиотеке.

Мы будем рассматривать только argparse. Но, если Вы столкнетесь с необходимостью использовать подобные модули, обязательно посмотрите и на те модули, которые не входят в стандартную библиотеку Python.
Например, на .

, которая сравнивает разные модули обработки аргументов командной строки (рассматриваются argparse, click и docopt).

Пример скрипта ping_function.py:

import subprocess import argparse   def ping_ip(ip_address, count):     '''     Ping IP address and return tuple:     On success: (return code = 0, command output)     On failure: (return code, error output (stderr))     '''     reply = subprocess.run('ping -c {count} -n {ip}'                            .format(count=count, ip=ip_address),                            shell=True,                            stdout=subprocess.PIPE,                            stderr=subprocess.PIPE,                            encoding='utf-8')     if reply.returncode == 0:         return True, reply.stdout     else:         return False, reply.stdout+reply.stderr    parser = argparse.ArgumentParser(description='Ping script')  parser.add_argument('-a', action="store", dest="ip") parser.add_argument('-c', action="store", dest="count", default=2, type=int)  args = parser.parse_args() print(args)  rc, message = ping_ip(args.ip, args.count) print(message) 

Создание парсера:

  • parser = argparse.ArgumentParser(description='Ping script')

Добавление аргументов:

  • parser.add_argument('-a', action="store", dest="ip")
    • аргумент, который передается после опции -a, сохранится в переменную ip
  • parser.add_argument('-c', action="store", dest="count", default=2, type=int)
    • аргумент, который передается после опции -c, будет сохранен в переменную count, но, прежде, будет конвертирован в число. Если аргумент не было указан, по умолчанию будет значение 2

Строка args = parser.parse_args() указывается после того, как определены все аргументы.

После её выполнения в переменной args содержатся все аргументы, которые были переданы скрипту.
К ним можно обращаться, используя синтаксис args.ip.

Попробуем вызвать скрипт с разными аргументами.

Если переданы оба аргумента:

$ python ping_function.py -a 8.8.8.8 -c 5 Namespace(count=5, ip='8.8.8.8') PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=48 time=48.673 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=48 time=49.902 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=48 time=48.696 ms 64 bytes from 8.8.8.8: icmp_seq=3 ttl=48 time=50.040 ms 64 bytes from 8.8.8.8: icmp_seq=4 ttl=48 time=48.831 ms  --- 8.8.8.8 ping statistics --- 5 packets transmitted, 5 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 48.673/49.228/50.040/0.610 ms 

Namespace - это объект, который возвращает метод parse_args()

Передаем только IP-адрес:

$ python ping_function.py -a 8.8.8.8 Namespace(count=2, ip='8.8.8.8') PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=48 time=48.563 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=48 time=49.616 ms  --- 8.8.8.8 ping statistics --- 2 packets transmitted, 2 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 48.563/49.090/49.616/0.526 ms 

Вызов скрипта без аргументов:

$ python ping_function.py Namespace(count=2, ip=None) Traceback (most recent call last):   File "ping_function.py", line 31, in <module>     rc, message = ping_ip( args.ip, args.count )   File "ping_function.py", line 16, in ping_ip     stderr=temp)   File "/usr/local/lib/python3.6/subprocess.py", line 336, in check_output     **kwargs).stdout   File "/usr/local/lib/python3.6/subprocess.py", line 403, in run     with Popen(*popenargs, **kwargs) as process:   File "/usr/local/lib/python3.6/subprocess.py", line 707, in __init__     restore_signals, start_new_session)   File "/usr/local/lib/python3.6/subprocess.py", line 1260, in _execute_child     restore_signals, start_new_session, preexec_fn) TypeError: expected str, bytes or os.PathLike object, not NoneType 

Если бы функция была вызвана без аргументов, когда не используется argparse, возникла бы ошибка, что не все аргументы указаны.

Но, из-за argparse, фактически аргумент передается, только он равен None.
Это видно в строке Namespace(count=2, ip=None).

В таком скрипте, очевидно, IP-адрес необходимо указывать всегда.
И в argparse можно указать, что аргумент является обязательным.

Надо изменить опцию -a: добавить в конце required=True:

parser.add_argument('-a', action="store", dest="ip", required=True) 

Теперь, если вызвать скрипт без аргументов, вывод будет таким:

$ python ping_function.py usage: ping_function.py [-h] -a IP [-c COUNT] ping_function.py: error: the following arguments are required: -a 

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

Также, благодаря argparse, доступен help:

$ python ping_function.py -h usage: ping_function.py [-h] -a IP [-c COUNT]  Ping script  optional arguments:   -h, --help  show this help message and exit   -a IP   -c COUNT 

Обратите внимание, что в сообщении все опции находятся в секции optional arguments.
argparse сам определяет, что указаны опции, так как они начинаются с - и в имени только одна буква.

Зададим IP-адрес как позиционный аргумент.

Файл ping_function_ver2.py:

import subprocess from tempfile import TemporaryFile  import argparse   def ping_ip(ip_address, count):     '''     Ping IP address and return tuple:     On success: (return code = 0, command output)     On failure: (return code, error output (stderr))     '''     reply = subprocess.run('ping -c {count} -n {ip}' .format(count=count, ip=ip_address),                            shell=True,                            stdout=subprocess.PIPE,                            stderr=subprocess.PIPE,                            encoding='utf-8')     if reply.returncode == 0:         return True, reply.stdout     else:         return False, reply.stdout+reply.stderr    parser = argparse.ArgumentParser(description='Ping script')  parser.add_argument('host', action="store", help="IP or name to ping") parser.add_argument('-c', action="store", dest="count", default=2, type=int,                     help="Number of packets")  args = parser.parse_args() print(args)  rc, message = ping_ip( args.host, args.count ) print(message) 

Теперь, вместо указания опции -a, можно просто передать IP-адрес.
Он будет автоматически сохранен в переменной host.
И автоматически считается обязательным.

То есть, теперь не нужно указывать required=True и dest="ip".

Кроме того, в скрипте указаны сообщения, которые будут выводиться при вызове help.

Теперь вызов скрипта выглядит так:

$ python ping_function_ver2.py 8.8.8.8 -c 2 Namespace(host='8.8.8.8', count=2) PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=48 time=49.203 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=48 time=51.764 ms  --- 8.8.8.8 ping statistics --- 2 packets transmitted, 2 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 49.203/50.484/51.764/1.280 ms 

А сообщение help так:

$ python ping_function_ver2.py -h usage: ping_function_ver2.py [-h] [-c COUNT] host  Ping script  positional arguments:   host        IP or name to ping  optional arguments:   -h, --help  show this help message and exit   -c COUNT    Number of packets 

Вложенные парсеры

Рассмотрим один из способов организации более сложной иерархии аргументов.

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

Файл parse_dhcp_snooping.py:

# -*- coding: utf-8 -*- import argparse  # Default values: DFLT_DB_NAME = 'dhcp_snooping.db' DFLT_DB_SCHEMA = 'dhcp_snooping_schema.sql'   def create(args):     print("Creating DB {} with DB schema {}".format((args.name, args.schema)))   def add(args):     if args.sw_true:         print("Adding switch data to database")     else:         print("Reading info from file(s) \n{}".format(', '.join(args.filename)))         print("\nAdding data to db {}".format(args.db_file))   def get(args):     if args.key and args.value:         print("Geting data from DB: {}".format(args.db_file))         print("Request data for host(s) with {} {}".format((args.key, args.value)))     elif args.key or args.value:         print("Please give two or zero args\n")         print(show_subparser_help('get'))     else:         print("Showing {} content...".format(args.db_file))   parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(title='subcommands',                                    description='valid subcommands',                                    help='description')   create_parser = subparsers.add_parser('create_db', help='create new db') create_parser.add_argument('-n', metavar='db-filename', dest='name',                            default=DFLT_DB_NAME, help='db filename') create_parser.add_argument('-s', dest='schema', default=DFLT_DB_SCHEMA,                            help='db schema filename') create_parser.set_defaults( func=create )   add_parser = subparsers.add_parser('add', help='add data to db') add_parser.add_argument('filename', nargs='+', help='file(s) to add to db') add_parser.add_argument('--db', dest='db_file', default=DFLT_DB_NAME, help='db name') add_parser.add_argument('-s', dest='sw_true', action='store_true',                         help='add switch data if set, else add normal data') add_parser.set_defaults( func=add )   get_parser = subparsers.add_parser('get', help='get data from db') get_parser.add_argument('--db', dest='db_file', default=DFLT_DB_NAME, help='db name') get_parser.add_argument('-k', dest="key",                         choices=['mac', 'ip', 'vlan', 'interface', 'switch'],                         help='host key (parameter) to search') get_parser.add_argument('-v', dest="value", help='value of key') get_parser.add_argument('-a', action='store_true', help='show db content') get_parser.set_defaults( func=get )    if __name__ == '__main__':     args = parser.parse_args()     if not vars(args):         parser.print_usage()     else:         args.func(args) 

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

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

Например, в этой части создан вложенный парсер create_db, и к нему добавлена опция -n:

create_parser = subparsers.add_parser('create_db', help='create new db') create_parser.add_argument('-n', dest='name', default=DFLT_DB_NAME,                            help='db filename') 

Синтаксис создания вложенных парсеров и добавления к ним аргументов одинаков:

create_parser = subparsers.add_parser('create_db', help='create new db') create_parser.add_argument('-n', metavar='db-filename', dest='name',                            default=DFLT_DB_NAME, help='db filename') create_parser.add_argument('-s', dest='schema', default=DFLT_DB_SCHEMA,                            help='db schema filename') create_parser.set_defaults( func=create ) 

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

В строке create_parser.set_defaults( func=create ) указывается, что при вызове парсера create_parser будет вызвана функция create.

Функция create получает как аргумент все аргументы, которые были переданы.
И внутри функции можно обращаться к нужным:

def create(args):     print("Creating DB {} with DB schema {}".format((args.name, args.schema))) 

Если вызвать help для этого скрипта, вывод будет таким:

$ python parse_dhcp_snooping.py -h usage: parse_dhcp_snooping.py [-h] {create_db,add,get} ...  optional arguments:   -h, --help           show this help message and exit  subcommands:   valid subcommands    {create_db,add,get}  description     create_db          create new db     add                add data to db     get                get data from db 

Обратите внимание, что каждый вложенный парсер, который создан в скрипте, отображается как команда в подсказке usage:

usage: parse_dhcp_snooping.py [-h] {create_db,add,get} ... 

У каждого вложенного парсера теперь есть свой help:

$ python parse_dhcp_snooping.py create_db -h usage: parse_dhcp_snooping.py create_db [-h] [-n db-filename] [-s SCHEMA]  optional arguments:   -h, --help      show this help message and exit   -n db-filename  db filename   -s SCHEMA       db schema filename 

Кроме вложенных парсеров, в этом примере также есть несколько новых возможностей argparse.

metavar

В парсере create_parser используется новый аргумент - metavar:

create_parser.add_argument('-n', metavar='db-filename', dest='name',                            default=DFLT_DB_NAME, help='db filename') create_parser.add_argument('-s', dest='schema', default=DFLT_DB_SCHEMA,                            help='db schema filename') 

Аргумент metavar позволяет указывать имя аргумента для вывода в сообщении usage и help:

$ python parse_dhcp_snooping.py create_db -h usage: parse_dhcp_snooping.py create_db [-h] [-n db-filename] [-s SCHEMA]  optional arguments:   -h, --help      show this help message and exit   -n db-filename  db filename   -s SCHEMA       db schema filename 

Посмотрите на разницу между опциями -n и -s:

  • после опции -n и в usage, и в help указывается имя, которое указано в параметре metavar
  • после опции -s указывается имя переменной, в которую сохраняется значение

nargs

В парсере add_parser используется nargs:

add_parser.add_argument('filename', nargs='+', help='file(s) to add to db') 

nargs позволяет указать, что в этот аргумент должно попасть определенное количество элементов.
В этом случае все аргументы, которые были переданы скрипту после имени аргумента filename,
попадут в список nargs.
Но должен быть передан хотя бы один аргумент.

Сообщение help в таком случае выглядит так:

$ python parse_dhcp_snooping.py add -h usage: parse_dhcp_snooping.py add [-h] [--db DB_FILE] [-s]                                   filename [filename ...]  positional arguments:   filename      file(s) to add to db  optional arguments:   -h, --help    show this help message and exit   --db DB_FILE  db name   -s            add switch data if set, else add normal data 

Если передать несколько файлов, они попадут в список.
А так как функция add просто выводит имена файлов, вывод получится таким:

$ python parse_dhcp_snooping.py add filename test1.txt test2.txt Reading info from file(s) filename, test1.txt, test2.txt  Adding data to db dhcp_snooping.db 

nargs поддерживает такие значения:

  • N - должно быть указанное количество аргументов. Аргументы будут в списке (даже если указан 1)
  • ? - 0 или 1 аргумент
  • * - все аргументы попадут в список
  • + - все аргументы попадут в список, но должен быть передан хотя бы один аргумент

choices

В парсере get_parser используется choices:

get_parser.add_argument('-k', dest="key",                         choices=['mac', 'ip', 'vlan', 'interface', 'switch'],                         help='host key (parameter) to search') 

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

Для этого парсера help выглядит так:

$ python parse_dhcp_snooping.py get -h usage: parse_dhcp_snooping.py get [-h] [--db DB_FILE]                                   [-k {mac,ip,vlan,interface,switch}]                                   [-v VALUE] [-a]  optional arguments:   -h, --help            show this help message and exit   --db DB_FILE          db name   -k {mac,ip,vlan,interface,switch}                         host key (parameter) to search   -v VALUE              value of key   -a                    show db content 

А если выбрать неправильный вариант:

$ python parse_dhcp_snooping.py get -k test usage: parse_dhcp_snooping.py get [-h] [--db DB_FILE]                                   [-k {mac,ip,vlan,interface,switch}]                                   [-v VALUE] [-a] parse_dhcp_snooping.py get: error: argument -k: invalid choice: 'test' (choose from 'mac', 'ip', 'vlan', 'interface', 'switch') 

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

Импорт парсера

В файле parse_dhcp_snooping.py последние две строки будут выполняться только в том случае, если скрипт был вызван как основной.

if __name__ == '__main__':     args = parser.parse_args()     args.func(args) 

А значит, если импортировать файл, эти строки не будут вызваны.

Попробуем импортировать парсер в другой файл (файл call_pds.py):

from parse_dhcp_snooping import parser  args = parser.parse_args() args.func(args) 

Вызов сообщения help:

$ python call_pds.py -h usage: call_pds.py [-h] {create_db,add,get} ...  optional arguments:   -h, --help           show this help message and exit  subcommands:   valid subcommands    {create_db,add,get}  description     create_db          create new db     add                add data to db     get                get data from db 

Вызов аргумента:

$ python call_pds.py add test.txt test2.txt Reading info from file(s) test.txt, test2.txt  Adding data to db dhcp_snooping.db 

Всё работает без проблем.

Передача аргументов вручную

Последняя особенность argparse - возможность передавать аргументы вручную.

Аргументы можно передать как список при вызове метода parse_args() (файл call_pds2.py):

from parse_dhcp_snooping import parser, get  args = parser.parse_args('add test.txt test2.txt'.split()) args.func(args) 

Необходимо использовать метод split(), так как метод parse_args ожидает список аргументов.

Результат будет таким, как если бы скрипт был вызван с аргументами:

$ python call_pds2.py Reading info from file(s) test.txt, test2.txt  Adding data to db dhcp_snooping.db 
Назад: Модуль ipaddress
Дальше: Модуль tabulate