Ich implementiere ein Befehlszeilenprogramm mit folgender Schnittstelle:
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
Ich habe die Argparse-Dokumentation durchgesehen . Ich kann GLOBAL_OPTIONS
als optionales Argument mit add_argument
in implementieren argparse
. Und die {command [COMMAND_OPTS]}
Verwendung von Unterbefehlen .
Aus der Dokumentation geht hervor, dass ich nur einen Unterbefehl haben kann. Aber wie Sie sehen, muss ich einen oder mehrere Unterbefehle implementieren. Was ist der beste Weg, um solche Befehlszeilenargumente mit zu analysieren argparse
?
./setup.py
hat auch diese Stil-CLI-Schnittstelle, wäre interessant, in ihren Quellcode zu schauen.Antworten:
Ich habe mir die gleiche Frage gestellt, und anscheinend habe ich eine bessere Antwort.
Die Lösung ist, dass wir Subparser nicht einfach mit einem anderen Subparser verschachteln, sondern Subparser mit einem Parser nach einem anderen Subparser hinzufügen können.
Code sagt Ihnen, wie:
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()
quelle
argparse
erlaubt verschachtelte Unterparser. Aber ich habe sie nur an einer anderen Stelle verwendet gesehen - in einem Testfall für ein Python-Problem, bugs.python.org/issue14365@mgilson hat eine schöne Antwort auf diese Frage. Aber das Problem beim Teilen von sys.argv selbst ist, dass ich all die nette Hilfemeldung verliere, die Argparse für den Benutzer generiert. Also habe ich folgendes gemacht:
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
Nach dem ersten Parsen werden nun alle verketteten Befehle in gespeichert
extra
. Ich repariere es, solange es nicht leer ist, um alle verketteten Befehle abzurufen und separate Namespaces für sie zu erstellen. Und ich bekomme eine schönere Verwendungszeichenfolge, die Argparse generiert.quelle
namespace
durch einen Anruf vom Parser komme, rufenamespace = argparser.parse_args()
ichparse_extra
mitparser
und annamespace
.extra_namespaces = parse_extra( argparser, namespace )
parser
in dem Code, den Sie haben. Ich sehe nur, dass es verwendet wird, um dasextra
Argument hinzuzufügen . Dann haben Sie es im obigen Kommentar noch einmal erwähnt. Ist es sein sollargparser
?argparser
. Wird es bearbeiten.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'])
;; Dies schlägt mit einem Fehler fehlPROG: error: unrecognized arguments: --baz Z
. Der Grund ist, dass während des Parsens voncommand_a
die optionalen Argumente voncommand_b
bereits analysiert werden (und für den Subparser von unbekannt sindcommand_a
).parse_known_args
Gibt einen Namespace und eine Liste unbekannter Zeichenfolgen zurück. Dies ähnelt derextra
in der aktivierten Antwort.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
produziert:
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') []
Eine alternative Schleife würde jedem Subparser einen eigenen Namespace geben. Dies ermöglicht eine Überlappung der Positionsnamen.
argslist = [] while rest: args,rest = parser.parse_known_args(rest) argslist.append(args)
quelle
rest = '--foo 0 cmd2 --foo2 2 --bar cmd3 --foo3 3 cmd1 --foo1 1'.split()
), endet argparse mit,error: too few arguments
anstatt auf die ungültige Option hinzuweisen. Dies liegt daran, dass die schlechte Option so lange beibehalten wird,rest
bis keine Befehlsargumente mehr vorhanden sind.# or sys.argv
sollte sein# or sys.argv[1:]
.Sie können die Befehlszeile jederzeit selbst aufteilen (
sys.argv
auf Ihre Befehlsnamen aufteilen ) und dann nur den Teil übergeben, der dem jeweiligen Befehl entsprichtparse_args
- Sie können sogar denselben verwendenNamespace
mit dem Schlüsselwort namespace verwenden, wenn Sie möchten.Das Gruppieren der Befehlszeile ist einfach mit
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...
ungetestet
quelle
itertools.groupby()
! So habe ich dasselbe gemacht, bevor ich davon wusstegroupby()
.Um die Antwort von @mgilson zu verbessern, habe ich eine kleine Analysemethode geschrieben, die argv in Teile aufteilt und Werte von Argumenten von Befehlen in die Hierarchie von Namespaces einfügt:
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)
Es verhält sich richtig und bietet nette Hilfe:
Für
./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}
Für
./test.py cmd1 --help
:usage: test.py cmd1 [-h] [--foo FOO] optional arguments: -h, --help show this help message and exit --foo FOO
Und erstellt eine Hierarchie von Namespaces mit den Argumentwerten:
./test.py cmd1 --foo 3 cmd3 --foo 4 Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))
quelle
split_argv[0]
die tatsächlich leer istsplit_argv
, weil Sie anhängen[c]
zusplit_argv
(intially gesetzt[[]]
). Wenn Sie Zeile 7 in ändernsplit_argv = []
, funktioniert alles wie erwartet.subparser
verwendet wurde, indem Sie dest zuradd_subparsers
Methode stackoverflow.com/questions/8250010/…Die von @Vikas bereitgestellte Lösung schlägt für unterbefehlsspezifische optionale Argumente fehl, der Ansatz ist jedoch gültig. Hier ist eine verbesserte Version:
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
Dies verwendet
parse_known_args
anstelle vonparse_args
.parse_args
bricht ab, sobald ein dem aktuellen Subparser unbekanntes Argument gefunden wird.parse_known_args
gibt es als zweiten Wert im zurückgegebenen Tupel zurück. Bei diesem Ansatz werden die verbleibenden Argumente erneut dem Parser zugeführt. Daher wird für jeden Befehl ein neuer Namespace erstellt.Beachten Sie, dass in diesem grundlegenden Beispiel alle globalen Optionen nur dem Namespace der ersten Optionen und nicht den nachfolgenden Namespaces hinzugefügt werden.
Dieser Ansatz funktioniert in den meisten Situationen einwandfrei, weist jedoch drei wichtige Einschränkungen auf:
myprog.py command_a --foo=bar command_b --foo=bar
.nargs='?'
odernargs='+'
odernargs='*'
) zu verwenden.PROG --foo command_b command_a --baz Z 12
mit dem obigen Code,--baz Z
wird von verbrauchtcommand_b
, nicht voncommand_a
.Diese Einschränkungen sind eine direkte Einschränkung von Argparse. Hier ist ein einfaches Beispiel, das die Einschränkungen von argparse zeigt - auch wenn ein einzelner Unterbefehl verwendet wird:
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)
Dies wird die erhöhen
error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b')
.Die Ursache ist, dass die interne Methode
argparse.ArgParser._parse_known_args()
zu gierig ist und davon ausgeht, dass diescommand_a
der Wert des optionalenspam
Arguments ist. Insbesondere beim Aufteilen von optionalen und positionellen Argumenten werden_parse_known_args()
nicht die Namen der Arugments (wiecommand_a
odercommand_b
) berücksichtigt , sondern nur, wo sie in der Argumentliste vorkommen. Es wird auch davon ausgegangen, dass jeder Unterbefehl alle verbleibenden Argumente verbraucht. Diese Einschränkungargparse
verhindert auch eine ordnungsgemäße Implementierung von Unterbefehlen mit mehreren Befehlen. Dies bedeutet leider, dass eine ordnungsgemäße Implementierung ein vollständiges Umschreiben derargparse.ArgParser._parse_known_args()
Methode erfordert , dh mehr als 200 Codezeilen.Angesichts dieser Einschränkung kann es eine Option sein, einfach auf ein einzelnes Multiple-Choice-Argument anstelle von Unterbefehlen zurückzugreifen:
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'])
Es ist sogar möglich, die verschiedenen Befehle in den Verwendungsinformationen aufzulisten, siehe meine Antwort https://stackoverflow.com/a/49999185/428542
quelle
Sie könnten Arghandler versuchen . Dies ist eine Erweiterung von argparse mit expliziter Unterstützung für Unterbefehle.
quelle
Ein weiteres Paket, das parallele Parser unterstützt, ist "declative_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)
und Namespace wird:
Namespace( input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>), output=Namespace(format='gif'), verbose=True )
Haftungsausschluss: Ich bin der Autor. Benötigt Python 3.6. So installieren Sie:
Hier ist die Dokumentation und hier ist das Repo auf GitHub .
quelle
Errichtet ein vollständiges Python 3.2 Beispiel mit subparsers ,
parse_known_args
undparse_args
( läuft auf 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()
quelle
Ich hatte mehr oder weniger die gleichen Anforderungen: Globale Argumente setzen und Befehle verketten und in der Reihenfolge der Befehlszeile ausführen können .
Am Ende hatte ich den folgenden Code. Ich habe einige Teile des Codes aus diesem und anderen Threads verwendet.
# 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)
Unten ein Beispiel:
$ 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
quelle
Sie können das Paket optparse verwenden
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
quelle