Jak analizować wiele zagnieżdżonych poleceń podrzędnych za pomocą języka Python argparse?


82

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_OPTIONSjako opcjonalny argument używając add_argumentw 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?


2
Nie sądzę, aby do tego służyły komendy podrzędne. Z dokumentacji wynika, że ​​zasadniczo służy to do sterowania oddzielnymi odrębnymi podprogramami . Czy spojrzałeś na grupy argumentujące ?
Chris

distutils ./setup.pyma również interfejs CLI w tym stylu, ciekawie byłoby zajrzeć do ich kodu źródłowego.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:


27

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()   

Tak, argparsezezwala 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
hpaulj

9
Zakłada się, że polecenia mają zagnieżdżoną strukturę. Ale pytanie dotyczy poleceń „równoległych”
augurar

25

@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.


2
@Flavius po otrzymuję namespacez parsera wywołując namespace = argparser.parse_args()wzywam parse_extraz parseri namespace. extra_namespaces = parse_extra( argparser, namespace )
Vikas

Myślę, że rozumiem logikę, ale co jest parserw kodzie, który masz. Widzę, że jest używany tylko do dodania extraargumentu. Następnie wspomniałeś o tym ponownie w powyższym komentarzu. Czy tak ma być argparser?
jmlopez,

@jmlopez yeah, powinno być argparser. Zmodyfikuje to.
Vikas,

1
Należy zauważyć, że to rozwiązanie nie działa w przypadku opcjonalnych argumentów specyficznych dla podkomend. Zobacz moje rozwiązanie poniżej ( stackoverflow.com/a/49977713/428542 ), aby uzyskać alternatywne rozwiązanie.
MacFreek

1
Oto przykład, jak to się nie udaje. Dodaj następujące 3 wiersze 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łędu PROG: error: unrecognized arguments: --baz Z. Powodem jest to, że podczas parsowania command_aopcjonalne argumenty command_bsą już analizowane (i są nieznane dla subparsera command_a).
MacFreek

14

parse_known_argszwraca Namespace i listę nieznanych ciągów. Jest to podobne do extraw 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)

Działa ładnie. Jest jednak wada: jeśli gdzieś jest błędnie napisana opcja (np. rest = '--foo 0 cmd2 --foo2 2 --bar cmd3 --foo3 3 cmd1 --foo1 1'.split()), Argparse kończy się na error: too few argumentszamiast wskazywać niepoprawną opcję. Dzieje się tak, ponieważ zła opcja zostanie pozostawiona, restdopóki nie skończą się argumenty poleceń.
Adrian W

Komentarz # or sys.argvpowinien być # or sys.argv[1:].
Adrian W

5

Zawsze możesz samodzielnie podzielić wiersz poleceń (podzielić sys.argvna nazwy poleceń), a następnie przekazać tylko część odpowiadającą danemu poleceniu do parse_args- Możesz nawet użyć tego samego, Namespaceuż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


1
Dzięki mgilson. To dobre rozwiązanie mojego pytania, ale skończyło się na tym, że zrobiłem to trochę inaczej. Dodałem kolejną odpowiedź .
Vikas

1
Niezłe użycie itertools.groupby()! W ten sposób zrobiłem to samo, zanim się dowiedziałem groupby().
kzyapkov

5

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'))

Przeglądając powyższy kod, napotkałem jeden problem. W linii 18. odwołujesz się do tego, split_argv[0]który jest faktycznie pusty w split_argv, ponieważ dodajesz [c]do split_argv(wstępnie ustawiono na [[]]). Jeśli zmienisz wiersz 7 na split_argv = [], wszystko działa zgodnie z oczekiwaniami.
HEADLESS_0NE

2
Zrobiłem jeszcze kilka poprawek (znowu) na kodzie udostępnionym (fixing pewne problemy biegałam do) i skończyło się tak: gist.github.com/anonymous/f4be805fc3ff9e132eb1e1aa0b4f7d4b
HEADLESS_0NE

Ta odpowiedź jest całkiem przyzwoita, możesz określić, która subparserzostała użyta, dodając dest do add_subparsersmetody stackoverflow.com/questions/8250010/ ...
wizebin

5

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_argszamiast parse_args. parse_argsprzerywa 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:

  • Nie jest możliwe użycie tego samego opcjonalnego argumentu dla różnych podpoleceń, takich jak myprog.py command_a --foo=bar command_b --foo=bar.
  • Nie jest możliwe użycie argumentów pozycyjnych o zmiennej długości z komendami ( nargs='?'lub nargs='+'lub nargs='*').
  • Każdy znany argument jest analizowany bez „przerywania” nowego polecenia. Np. PROG --foo command_b command_a --baz Z 12Z powyższym kodem --baz Zzostanie zużyty przez command_b, a nie przez command_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, że command_ajest to wartość spamargumentu opcjonalnego . W szczególności podczas „dzielenia” argumentów opcjonalnych i pozycyjnych _parse_known_args()nie sprawdza nazw argumentów (takich jak command_alub command_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 ograniczenie argparseuniemożliwia również prawidłową implementację parserów obsługujących wiele poleceń. Oznacza to niestety, że prawidłowa implementacja wymaga pełnego przepisania argparse.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


4

Możesz spróbować Arghandler . To jest rozszerzenie do argparse z jawną obsługą podpoleceń.


3
arghandler zapewnia przyjemny sposób deklarowania podpoleceń. Jednak nie widzę, jak to pomaga rozwiązać pytanie OP: parsowanie wielu podkomend. Pierwsza przeanalizowana komenda pochłonie wszystkie pozostałe argumenty, więc dalsze polecenia nigdy nie zostaną przeanalizowane. Podaj podpowiedź, jak rozwiązać ten problem z arghandlerem. Dzięki.
Adrian W

1

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:

pip3 install declarative_parser

Tutaj jest dokumentacja, a tutaj repozytorium na GitHub .


1

Wbudowany pełną Python 2/3 przykład z subparsers , parse_known_argsi parse_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()

0

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

-4

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

1
To naprawdę nie odpowiada na pytanie. Ponadto optparse jest przestarzałe (z dokumentacji Pythona „Moduł optparse jest przestarzały i nie będzie dalej rozwijany; rozwój będzie kontynuowany z modułem argparse”).
Chris

Przepraszam za przeciw, ale to nie jest odpowiedzią na pytanie, które zadałem.
Vikas
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.