Wdrażam program wiersza poleceń, który ma następujący interfejs:
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
I przeszły argparse dokumentacji . Mogę zaimplementować GLOBAL_OPTIONS
jako opcjonalny argument używając add_argument
w argparse
. Oraz za {command [COMMAND_OPTS]}
pomocą poleceń podrzędnych .
Z dokumentacji wynika, że mogę mieć tylko jedno polecenie podrzędne. Ale jak widać, muszę zaimplementować jedno lub więcej poleceń podrzędnych. Jaki jest najlepszy sposób analizowania takich argumentów wiersza poleceń przy użyciu argparse
?
./setup.py
ma również interfejs CLI w tym stylu, ciekawie byłoby zajrzeć do ich kodu źródłowego.Odpowiedzi:
Wymyśliłem to samo pytanie i wydaje mi się, że mam lepszą odpowiedź.
Rozwiązaniem jest to, że nie będziemy po prostu zagnieżdżać subparsera z innym subparserem, ale możemy dodać subparser następujący po parserze po innym subparserze.
Kod podpowiada, jak:
parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument('--user', '-u', default=getpass.getuser(), help='username') parent_parser.add_argument('--debug', default=False, required=False, action='store_true', dest="debug", help='debug flag') main_parser = argparse.ArgumentParser() service_subparsers = main_parser.add_subparsers(title="service", dest="service_command") service_parser = service_subparsers.add_parser("first", help="first", parents=[parent_parser]) action_subparser = service_parser.add_subparsers(title="action", dest="action_command") action_parser = action_subparser.add_parser("second", help="second", parents=[parent_parser]) args = main_parser.parse_args()
źródło
argparse
zezwala na zagnieżdżone subparsery. Ale widziałem je tylko w jednym miejscu - w przypadku testowym dotyczącym problemu z Pythonem, bugs.python.org/issue14365@mgilson ma miłą odpowiedź na to pytanie. Ale problem z dzieleniem sys.argv polega na tym, że tracę całą fajną wiadomość pomocy, którą Argparse generuje dla użytkownika. Więc skończyło się na tym:
import argparse ## This function takes the 'extra' attribute from global namespace and re-parses it to create separate namespaces for all other chained commands. def parse_extra (parser, namespace): namespaces = [] extra = namespace.extra while extra: n = parser.parse_args(extra) extra = n.extra namespaces.append(n) return namespaces argparser=argparse.ArgumentParser() subparsers = argparser.add_subparsers(help='sub-command help', dest='subparser_name') parser_a = subparsers.add_parser('command_a', help = "command_a help") ## Setup options for parser_a ## Add nargs="*" for zero or more other commands argparser.add_argument('extra', nargs = "*", help = 'Other commands') ## Do similar stuff for other sub-parsers
Teraz po pierwszej analizie wszystkie powiązane polecenia są przechowywane w plikach
extra
. Ponownie analizuję go, gdy nie jest pusty, aby pobrać wszystkie powiązane polecenia i utworzyć dla nich oddzielne przestrzenie nazw. I otrzymuję ładniejszy ciąg użycia, który generuje argparse.źródło
namespace
z parsera wywołującnamespace = argparser.parse_args()
wzywamparse_extra
zparser
inamespace
.extra_namespaces = parse_extra( argparser, namespace )
parser
w kodzie, który masz. Widzę, że jest używany tylko do dodaniaextra
argumentu. Następnie wspomniałeś o tym ponownie w powyższym komentarzu. Czy tak ma byćargparser
?argparser
. Zmodyfikuje to.parser_b = subparsers.add_parser('command_b', help='command_b help')
:;parser_b.add_argument('--baz', choices='XYZ', help='baz help')
;options = argparser.parse_args(['--foo', 'command_a', 'command_b', '--baz', 'Z'])
; To kończy się niepowodzeniem z powodu błęduPROG: error: unrecognized arguments: --baz Z
. Powodem jest to, że podczas parsowaniacommand_a
opcjonalne argumentycommand_b
są już analizowane (i są nieznane dla subparseracommand_a
).parse_known_args
zwraca Namespace i listę nieznanych ciągów. Jest to podobne doextra
w zaznaczonej odpowiedzi.import argparse parser = argparse.ArgumentParser() parser.add_argument('--foo') sub = parser.add_subparsers() for i in range(1,4): sp = sub.add_parser('cmd%i'%i) sp.add_argument('--foo%i'%i) # optionals have to be distinct rest = '--foo 0 cmd2 --foo2 2 cmd3 --foo3 3 cmd1 --foo1 1'.split() # or sys.argv args = argparse.Namespace() while rest: args,rest = parser.parse_known_args(rest,namespace=args) print args, rest
produkuje:
Namespace(foo='0', foo2='2') ['cmd3', '--foo3', '3', 'cmd1', '--foo1', '1'] Namespace(foo='0', foo2='2', foo3='3') ['cmd1', '--foo1', '1'] Namespace(foo='0', foo1='1', foo2='2', foo3='3') []
Alternatywna pętla nadałaby każdemu subparserowi własną przestrzeń nazw. Pozwala to na nakładanie się nazw pozycji.
argslist = [] while rest: args,rest = parser.parse_known_args(rest) argslist.append(args)
źródło
rest = '--foo 0 cmd2 --foo2 2 --bar cmd3 --foo3 3 cmd1 --foo1 1'.split()
), Argparse kończy się naerror: too few arguments
zamiast wskazywać niepoprawną opcję. Dzieje się tak, ponieważ zła opcja zostanie pozostawiona,rest
dopóki nie skończą się argumenty poleceń.# or sys.argv
powinien być# or sys.argv[1:]
.Zawsze możesz samodzielnie podzielić wiersz poleceń (podzielić
sys.argv
na nazwy poleceń), a następnie przekazać tylko część odpowiadającą danemu poleceniu doparse_args
- Możesz nawet użyć tego samego,Namespace
używając słowa kluczowego namespace, jeśli chcesz.Grupowanie linii poleceń jest łatwe dzięki
itertools.groupby
:import sys import itertools import argparse mycommands=['cmd1','cmd2','cmd3'] def groupargs(arg,currentarg=[None]): if(arg in mycommands):currentarg[0]=arg return currentarg[0] commandlines=[list(args) for cmd,args in intertools.groupby(sys.argv,groupargs)] #setup parser here... parser=argparse.ArgumentParser() #... namespace=argparse.Namespace() for cmdline in commandlines: parser.parse_args(cmdline,namespace=namespace) #Now do something with namespace...
niesprawdzone
źródło
itertools.groupby()
! W ten sposób zrobiłem to samo, zanim się dowiedziałemgroupby()
.Poprawiając odpowiedź @mgilson, napisałem małą metodę parsowania, która dzieli argv na części i umieszcza wartości argumentów poleceń w hierarchii przestrzeni nazw:
import sys import argparse def parse_args(parser, commands): # Divide argv by commands split_argv = [[]] for c in sys.argv[1:]: if c in commands.choices: split_argv.append([c]) else: split_argv[-1].append(c) # Initialize namespace args = argparse.Namespace() for c in commands.choices: setattr(args, c, None) # Parse each command parser.parse_args(split_argv[0], namespace=args) # Without command for argv in split_argv[1:]: # Commands n = argparse.Namespace() setattr(args, argv[0], n) parser.parse_args(argv, namespace=n) return args parser = argparse.ArgumentParser() commands = parser.add_subparsers(title='sub-commands') cmd1_parser = commands.add_parser('cmd1') cmd1_parser.add_argument('--foo') cmd2_parser = commands.add_parser('cmd2') cmd2_parser.add_argument('--foo') cmd2_parser = commands.add_parser('cmd3') cmd2_parser.add_argument('--foo') args = parse_args(parser, commands) print(args)
Zachowuje się poprawnie, zapewniając przyjemną pomoc argparse:
Dla
./test.py --help
:usage: test.py [-h] {cmd1,cmd2,cmd3} ... optional arguments: -h, --help show this help message and exit sub-commands: {cmd1,cmd2,cmd3}
Dla
./test.py cmd1 --help
:usage: test.py cmd1 [-h] [--foo FOO] optional arguments: -h, --help show this help message and exit --foo FOO
I tworzy hierarchię przestrzeni nazw zawierających wartości argumentów:
./test.py cmd1 --foo 3 cmd3 --foo 4 Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))
źródło
split_argv[0]
który jest faktycznie pusty wsplit_argv
, ponieważ dodajesz[c]
dosplit_argv
(wstępnie ustawiono na[[]]
). Jeśli zmienisz wiersz 7 nasplit_argv = []
, wszystko działa zgodnie z oczekiwaniami.subparser
została użyta, dodając dest doadd_subparsers
metody stackoverflow.com/questions/8250010/ ...Rozwiązanie dostarczone przez @Vikas nie działa w przypadku opcjonalnych argumentów specyficznych dla podkomendy, ale podejście jest poprawne. Oto ulepszona wersja:
import argparse # create the top-level parser parser = argparse.ArgumentParser(prog='PROG') parser.add_argument('--foo', action='store_true', help='foo help') subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name') # create the parser for the "command_a" command parser_a = subparsers.add_parser('command_a', help='command_a help') parser_a.add_argument('bar', type=int, help='bar help') # create the parser for the "command_b" command parser_b = subparsers.add_parser('command_b', help='command_b help') parser_b.add_argument('--baz', choices='XYZ', help='baz help') # parse some argument lists argv = ['--foo', 'command_a', '12', 'command_b', '--baz', 'Z'] while argv: print(argv) options, argv = parser.parse_known_args(argv) print(options) if not options.subparser_name: break
To używa
parse_known_args
zamiastparse_args
.parse_args
przerywa działanie po napotkaniu argumentu nieznanego bieżącemu subparserowi,parse_known_args
zwraca je jako drugą wartość w zwracanej krotce. W tym podejściu pozostałe argumenty są ponownie przekazywane do parsera. Dlatego dla każdego polecenia tworzona jest nowa przestrzeń nazw.Zauważ, że w tym podstawowym przykładzie wszystkie opcje globalne są dodawane tylko do pierwszej opcji Przestrzeń nazw, a nie do kolejnych Przestrzeni nazw.
To podejście działa dobrze w większości sytuacji, ale ma trzy ważne ograniczenia:
myprog.py command_a --foo=bar command_b --foo=bar
.nargs='?'
lubnargs='+'
lubnargs='*'
).PROG --foo command_b command_a --baz Z 12
Z powyższym kodem--baz Z
zostanie zużyty przezcommand_b
, a nie przezcommand_a
.Te ograniczenia są bezpośrednim ograniczeniem argparse. Oto prosty przykład, który pokazuje ograniczenia argparse-nawet w przypadku używania pojedynczej komendy-:
import argparse parser = argparse.ArgumentParser() parser.add_argument('spam', nargs='?') subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name') # create the parser for the "command_a" command parser_a = subparsers.add_parser('command_a', help='command_a help') parser_a.add_argument('bar', type=int, help='bar help') # create the parser for the "command_b" command parser_b = subparsers.add_parser('command_b', help='command_b help') options = parser.parse_args('command_a 42'.split()) print(options)
To podniesie
error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b')
.Przyczyną jest to, że metoda wewnętrzna
argparse.ArgParser._parse_known_args()
jest zbyt chciwa i zakłada, żecommand_a
jest to wartośćspam
argumentu opcjonalnego . W szczególności podczas „dzielenia” argumentów opcjonalnych i pozycyjnych_parse_known_args()
nie sprawdza nazw argumentów (takich jakcommand_a
lubcommand_b
), a jedynie miejsca, w których występują one na liście argumentów. Zakłada również, że każda podkomenda zużyje wszystkie pozostałe argumenty. To ograniczenieargparse
uniemożliwia również prawidłową implementację parserów obsługujących wiele poleceń. Oznacza to niestety, że prawidłowa implementacja wymaga pełnego przepisaniaargparse.ArgParser._parse_known_args()
metody, czyli ponad 200 linii kodu.Biorąc pod uwagę te ograniczenia, może to być opcja powrotu do pojedynczego argumentu wielokrotnego wyboru zamiast podpoleceń:
import argparse parser = argparse.ArgumentParser() parser.add_argument('--bar', type=int, help='bar help') parser.add_argument('commands', nargs='*', metavar='COMMAND', choices=['command_a', 'command_b']) options = parser.parse_args('--bar 2 command_a command_b'.split()) print(options) #options = parser.parse_args(['--help'])
W informacjach o użytkowaniu można nawet wymienić różne polecenia, zobacz moją odpowiedź https://stackoverflow.com/a/49999185/428542
źródło
Możesz spróbować Arghandler . To jest rozszerzenie do argparse z jawną obsługą podpoleceń.
źródło
Innym pakietem obsługującym parsery równoległe jest „declarative_parser”.
import argparse from declarative_parser import Parser, Argument supported_formats = ['png', 'jpeg', 'gif'] class InputParser(Parser): path = Argument(type=argparse.FileType('rb'), optional=False) format = Argument(default='png', choices=supported_formats) class OutputParser(Parser): format = Argument(default='jpeg', choices=supported_formats) class ImageConverter(Parser): description = 'This app converts images' verbose = Argument(action='store_true') input = InputParser() output = OutputParser() parser = ImageConverter() commands = '--verbose input image.jpeg --format jpeg output --format gif'.split() namespace = parser.parse_args(commands)
a przestrzeń nazw staje się:
Namespace( input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>), output=Namespace(format='gif'), verbose=True )
Zastrzeżenie: jestem autorem. Wymaga Pythona 3.6. Aby zainstalować użyj:
Tutaj jest dokumentacja, a tutaj repozytorium na GitHub .
źródło
Wbudowany pełną Python 2/3 przykład z subparsers ,
parse_known_args
iparse_args
( działa na IDEone ):from __future__ import print_function from argparse import ArgumentParser from random import randint def main(): parser = get_parser() input_sum_cmd = ['sum_cmd', '--sum'] input_min_cmd = ['min_cmd', '--min'] args, rest = parser.parse_known_args( # `sum` input_sum_cmd + ['-a', str(randint(21, 30)), '-b', str(randint(51, 80))] + # `min` input_min_cmd + ['-y', str(float(randint(64, 79))), '-z', str(float(randint(91, 120)) + .5)] ) print('args:\t ', args, '\nrest:\t ', rest, '\n', sep='') sum_cmd_result = args.sm((args.a, args.b)) print( 'a:\t\t {:02d}\n'.format(args.a), 'b:\t\t {:02d}\n'.format(args.b), 'sum_cmd: {:02d}\n'.format(sum_cmd_result), sep='') assert rest[0] == 'min_cmd' args = parser.parse_args(rest) min_cmd_result = args.mn((args.y, args.z)) print( 'y:\t\t {:05.2f}\n'.format(args.y), 'z:\t\t {:05.2f}\n'.format(args.z), 'min_cmd: {:05.2f}'.format(min_cmd_result), sep='') def get_parser(): # create the top-level parser parser = ArgumentParser(prog='PROG') subparsers = parser.add_subparsers(help='sub-command help') # create the parser for the "sum" command parser_a = subparsers.add_parser('sum_cmd', help='sum some integers') parser_a.add_argument('-a', type=int, help='an integer for the accumulator') parser_a.add_argument('-b', type=int, help='an integer for the accumulator') parser_a.add_argument('--sum', dest='sm', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') # create the parser for the "min" command parser_b = subparsers.add_parser('min_cmd', help='min some integers') parser_b.add_argument('-y', type=float, help='an float for the accumulator') parser_b.add_argument('-z', type=float, help='an float for the accumulator') parser_b.add_argument('--min', dest='mn', action='store_const', const=min, default=0, help='smallest integer (default: 0)') return parser if __name__ == '__main__': main()
źródło
Miałem mniej więcej te same wymagania: być w stanie ustawić globalne argumenty i móc łączyć polecenia i wykonywać je w kolejności wiersza poleceń .
Skończyło się na następującym kodzie. Użyłem niektórych części kodu z tego i innych wątków.
# argtest.py import sys import argparse def init_args(): def parse_args_into_namespaces(parser, commands): ''' Split all command arguments (without prefix, like --) in own namespaces. Each command accepts extra options for configuration. Example: `add 2 mul 5 --repeat 3` could be used to a sequencial addition of 2, then multiply with 5 repeated 3 times. ''' class OrderNamespace(argparse.Namespace): ''' Add `command_order` attribute - a list of command in order on the command line. This allows sequencial processing of arguments. ''' globals = None def __init__(self, **kwargs): self.command_order = [] super(OrderNamespace, self).__init__(**kwargs) def __setattr__(self, attr, value): attr = attr.replace('-', '_') if value and attr not in self.command_order: self.command_order.append(attr) super(OrderNamespace, self).__setattr__(attr, value) # Divide argv by commands split_argv = [[]] for c in sys.argv[1:]: if c in commands.choices: split_argv.append([c]) else: split_argv[-1].append(c) # Globals arguments without commands args = OrderNamespace() cmd, args_raw = 'globals', split_argv.pop(0) args_parsed = parser.parse_args(args_raw, namespace=OrderNamespace()) setattr(args, cmd, args_parsed) # Split all commands to separate namespace pos = 0 while len(split_argv): pos += 1 cmd, *args_raw = split_argv.pop(0) assert cmd[0].isalpha(), 'Command must start with a letter.' args_parsed = commands.choices[cmd].parse_args(args_raw, namespace=OrderNamespace()) setattr(args, f'{cmd}~{pos}', args_parsed) return args # # Supported commands and options # parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--print', action='store_true') commands = parser.add_subparsers(title='Operation chain') cmd1_parser = commands.add_parser('add', formatter_class=argparse.ArgumentDefaultsHelpFormatter) cmd1_parser.add_argument('add', help='Add this number.', type=float) cmd1_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.', default=1, type=int) cmd2_parser = commands.add_parser('mult', formatter_class=argparse.ArgumentDefaultsHelpFormatter) cmd2_parser.add_argument('mult', help='Multiply with this number.', type=float) cmd2_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.', default=1, type=int) args = parse_args_into_namespaces(parser, commands) return args # # DEMO # args = init_args() # print('Parsed arguments:') # for cmd in args.command_order: # namespace = getattr(args, cmd) # for option_name in namespace.command_order: # option_value = getattr(namespace, option_name) # print((cmd, option_name, option_value)) print('Execution:') result = 0 for cmd in args.command_order: namespace = getattr(args, cmd) cmd_name, cmd_position = cmd.split('~') if cmd.find('~') > -1 else (cmd, 0) if cmd_name == 'globals': pass elif cmd_name == 'add': for r in range(namespace.repeat): if args.globals.print: print(f'+ {namespace.add}') result = result + namespace.add elif cmd_name == 'mult': for r in range(namespace.repeat): if args.globals.print: print(f'* {namespace.mult}') result = result * namespace.mult else: raise NotImplementedError(f'Namespace `{cmd}` is not implemented.') print(10*'-') print(result)
Poniżej przykład:
$ python argstest.py --print add 1 -r 2 mult 5 add 3 mult -r 5 5 Execution: + 1.0 + 1.0 * 5.0 + 3.0 * 5.0 * 5.0 * 5.0 * 5.0 * 5.0 ---------- 40625.0
źródło
możesz użyć pakietu optparse
import optparse parser = optparse.OptionParser() parser.add_option("-f", dest="filename", help="corpus filename") parser.add_option("--alpha", dest="alpha", type="float", help="parameter alpha", default=0.5) (options, args) = parser.parse_args() fname = options.filename alpha = options.alpha
źródło