Python argparse: Rendi obbligatorio almeno un argomento


92

Sto usando argparseper un programma Python che può -process, -uploado entrambi:

parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('-process', action='store_true')
parser.add_argument('-upload',  action='store_true')
args = parser.parse_args()

Il programma non ha senso senza almeno un parametro. Come posso configurare argparseper forzare la scelta di almeno un parametro?

AGGIORNARE:

A seguito dei commenti: qual è il modo pitonico per parametrizzare un programma con almeno un'opzione?


9
-xè universalmente una bandiera e facoltativa. Taglia il -se necessario.

1
Non è possibile processimpostare il comportamento predefinito (senza la necessità di specificare alcuna opzione) e consentire all'utente di modificarlo in uploadse tale opzione è impostata? Di solito, le opzioni dovrebbero essere opzionali, da cui il nome. Le opzioni richieste dovrebbero essere evitate (questo è anche nei argparse documenti).
Tim Pietzcker

@AdamMatan Sono passati quasi tre anni da quando hai posto la tua domanda, ma mi è piaciuta la sfida nascosta e ho sfruttato il vantaggio della disponibilità di nuove soluzioni per questo tipo di attività.
Jan Vlcinsky

Risposte:


107
if not (args.process or args.upload):
    parser.error('No action requested, add -process or -upload')

1
Questo è probabilmente l'unico modo, se argparsenon ha opzioni integrate per questo.
Adam Matan

29
args = vars(parser.parse_args())
if not any(args.values()):
    parser.error('No arguments provided.')

3
+1 per una soluzione generalizzata. Anche come l'uso di vars(), che è utile anche per passare opzioni accuratamente denominate a un costruttore con **.
Lenna

Ed è esattamente quello che ci sto facendo. Grazie!
brentlance

1
Dang, mi piace vars. L'ho appena fatto .__dict__e mi sono sentito stupido prima.
Theo Belaire

1
ottime risposte. Sia "vars" che "any" erano nuovi per me :-)
Vivek Jha

21

Se non la parte "o entrambe" (inizialmente mi ero persa) potresti usare qualcosa del genere:

parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('--process', action='store_const', const='process', dest='mode')
parser.add_argument('--upload',  action='store_const', const='upload', dest='mode')
args = parser.parse_args()
if not args.mode:
    parser.error("One of --process or --upload must be given")

Tuttavia, probabilmente sarebbe un'idea migliore usare invece i sottocomandi .


4
Penso che voglia consentire l' --processOR --upload, non lo XOR. Ciò impedisce che entrambe le opzioni vengano impostate contemporaneamente.
phihag

+1 perché hai menzionato i sottocomandi. Tuttavia, come qualcuno ha sottolineato nei commenti -xe --xxxsono parametri tipicamente opzionali.
mac

20

So che è vecchio come lo sporco, ma il modo per richiedere un'opzione ma vietarne più di una (XOR) è così:

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-process', action='store_true')
group.add_argument('-upload',  action='store_true')
args = parser.parse_args()
print args

Produzione:

>opt.py  
usage: multiplot.py [-h] (-process | -upload)  
multiplot.py: error: one of the arguments -process -upload is required  

>opt.py -upload  
Namespace(process=False, upload=True)  

>opt.py -process  
Namespace(process=True, upload=False)  

>opt.py -upload -process  
usage: multiplot.py [-h] (-process | -upload)  
multiplot.py: error: argument -process: not allowed with argument -upload  

3
Sfortunatamente, l'OP non vuole uno XOR. È uno o entrambi, ma non nessuno, quindi il tuo ultimo caso di test non soddisfa i loro requisiti.
kdopen

2
@kdopen: l'intervistato ha chiarito che questa è una variazione della domanda originale, che ho trovato utile: "il modo per richiedere un'opzione ma vietarne più di una" Forse l'etichetta di Stack Exchange richiederebbe invece una nuova domanda . Ma avere questa risposta presente qui mi ha aiutato ...
erik.weathers

2
Questo post non risponde alla domanda iniziale
Marc

2
Come risponde questo alla domanda "almeno uno"?
xaxxon

2
Sfortunatamente, l'OP non vuole uno XOR.
duckman_1991

8

Revisione dei requisiti

  • usa argparse(ignorerò questo)
  • consentire il richiamo di una o due azioni (almeno una richiesta).
  • provalo con Pythonic (lo chiamerei piuttosto "POSIX")

Ci sono anche alcuni requisiti impliciti quando si vive sulla riga di comando:

  • spiegare l'utilizzo all'utente in modo facile da capire
  • le opzioni devono essere facoltative
  • consente di specificare flag e opzioni
  • consentire la combinazione con altri parametri (come nome file o nomi).

Soluzione di esempio utilizzando docopt(file managelog.py):

"""Manage logfiles
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  Password

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
    from docopt import docopt
    args = docopt(__doc__)
    print args

Prova a eseguirlo:

$ python managelog.py
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Mostra l'aiuto:

$ python managelog.py -h
Manage logfiles
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  P    managelog.py [options] upload -- <logfile>...

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>

E usalo:

$ python managelog.py -V -U user -P secret upload -- alfa.log beta.log
{'--': True,
 '--pswd': 'secret',
 '--user': 'user',
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': False,
 'upload': True}

Alternativa breve short.py

Può esserci una variante anche più breve:

"""Manage logfiles
Usage:
    short.py [options] (process|upload)... -- <logfile>...
    short.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  Password

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
    from docopt import docopt
    args = docopt(__doc__)
    print args

L'utilizzo è simile a questo:

$ python short.py -V process upload  -- alfa.log beta.log
{'--': True,
 '--pswd': None,
 '--user': None,
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': 1,
 'upload': 1}

Nota che invece dei valori booleani per le chiavi "process" e "upload" ci sono dei contatori.

Si scopre che non possiamo impedire la duplicazione di queste parole:

$ python short.py -V process process upload  -- alfa.log beta.log
{'--': True,
 '--pswd': None,
 '--user': None,
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': 2,
 'upload': 1}

Conclusioni

Progettare una buona interfaccia a riga di comando può essere difficile a volte.

Ci sono più aspetti del programma basato sulla riga di comando:

  • buon design della riga di comando
  • selezionando / utilizzando un parser appropriato

argparse offre molto, ma limita gli scenari possibili e può diventare molto complesso.

Con le docoptcose vanno molto più brevi preservando la leggibilità e offrendo un alto grado di flessibilità. Se riesci a ottenere argomenti analizzati dal dizionario ed esegui alcune conversioni (a interi, apertura di file ..) manualmente (o tramite un'altra libreria chiamata schema), potresti trovare una docoptbuona soluzione per l'analisi da riga di comando.


Mai sentito parlare di docopt, ottimo suggerimento!
Ton van den Heuvel

@TonvandenHeuvel Bene. Voglio solo confermare, lo sto ancora usando come soluzione preferita per le interfacce della riga di comando.
Jan Vlcinsky

Migliore risposta evar, grazie per gli esempi dettagliati.
jnovack

5

Se si richiede che un programma Python venga eseguito con almeno un parametro, aggiungere un argomento che non abbia il prefisso dell'opzione (- o - per impostazione predefinita) e impostare nargs=+(minimo un argomento richiesto). Il problema con questo metodo che ho trovato è che se non specifichi l'argomento, argparse genererà un errore di "troppo pochi argomenti" e non stamperà il menu di aiuto. Se non hai bisogno di quella funzionalità, ecco come farlo nel codice:

import argparse

parser = argparse.ArgumentParser(description='Your program description')
parser.add_argument('command', nargs="+", help='describe what a command is')
args = parser.parse_args()

Io penso che quando si aggiunge una discussione con i prefissi di opzione, nargs governa l'intero parser argomento e non solo l'opzione. (Quello che voglio dire è che, se hai un --optionflag con nargs="+", --optionflag si aspetta almeno un argomento. Se hai optioncon nargs="+", si aspetta almeno un argomento in generale.)


Potresti aggiungere choices=['process','upload']a questo argomento.
hpaulj

5

Per http://bugs.python.org/issue11588 sto esplorando modi per generalizzare ilmutually_exclusive_group concetto per gestire casi come questo.

Con questo sviluppo argparse.py, https://github.com/hpaulj/argparse_issues/blob/nested/argparse.py sono in grado di scrivere:

parser = argparse.ArgumentParser(prog='PROG', 
    description='Log archiver arguments.')
group = parser.add_usage_group(kind='any', required=True,
    title='possible actions (at least one is required)')
group.add_argument('-p', '--process', action='store_true')
group.add_argument('-u', '--upload',  action='store_true')
args = parser.parse_args()
print(args)

che produce quanto segue help:

usage: PROG [-h] (-p | -u)

Log archiver arguments.

optional arguments:
  -h, --help     show this help message and exit

possible actions (at least one is required):
  -p, --process
  -u, --upload

Accetta input come '-u', '-up', '--proc --up' ecc.

Finisce per eseguire un test simile a https://stackoverflow.com/a/6723066/901925 , anche se il messaggio di errore deve essere più chiaro:

usage: PROG [-h] (-p | -u)
PROG: error: some of the arguments process upload is required

Mi chiedo:

  • i parametri sono kind='any', required=Trueabbastanza chiari (accettare uno qualsiasi del gruppo; almeno uno è richiesto)?

  • l'uso è (-p | -u)chiaro? Un mutually_exclusive_group richiesto produce la stessa cosa. C'è qualche notazione alternativa?

  • usare un gruppo come questo è più intuitivo del phihag'ssemplice test?


Non riesco a trovare alcuna menzione add_usage_groupin questa pagina: docs.python.org/2/library/argparse.html ; puoi fornire un collegamento alla relativa documentazione?
P. Myer Nore

@ P.MyerNore, ho fornito un collegamento - all'inizio di questa risposta. Questo non è stato messo in produzione.
hpaulj

5

Il modo migliore per farlo è usare il modulo integrato in Python add_mutually_exclusive_group .

parser = argparse.ArgumentParser(description='Log archiver arguments.')
group = parser.add_mutually_exclusive_group()
group.add_argument('-process', action='store_true')
group.add_argument('-upload',  action='store_true')
args = parser.parse_args()

Se si desidera selezionare un solo argomento dalla riga di comando, utilizzare solo required = True come argomento per il gruppo

group = parser.add_mutually_exclusive_group(required=True)

2
In che modo questo ti fa ottenere "almeno uno" - non ti fa ottenere "esattamente uno"?
xaxxon

3
Sfortunatamente, l'OP non vuole uno XOR. OP sta cercando OR
duckman_1991

Questo non ha risposto alla domanda di OP, ma ha risposto alla mia, quindi grazie comunque ¯_ (ツ) _ / ¯
rosstex

2

Forse usi sub-parser?

import argparse

parser = argparse.ArgumentParser(description='Log archiver arguments.')
subparsers = parser.add_subparsers(dest='subparser_name', help='sub-command help')
parser_process = subparsers.add_parser('process', help='Process logs')
parser_upload = subparsers.add_parser('upload', help='Upload logs')
args = parser.parse_args()

print("Subparser: ", args.subparser_name)

Ora --helpmostra:

$ python /tmp/aaa.py --help
usage: aaa.py [-h] {process,upload} ...

Log archiver arguments.

positional arguments:
  {process,upload}  sub-command help
    process         Process logs
    upload          Upload logs

optional arguments:
  -h, --help        show this help message and exit
$ python /tmp/aaa.py
usage: aaa.py [-h] {process,upload} ...
aaa.py: error: too few arguments
$ python3 /tmp/aaa.py upload
Subparser:  upload

Puoi anche aggiungere ulteriori opzioni a questi sub-parser. Inoltre, invece di usarlo dest='subparser_name', puoi anche associare funzioni da chiamare direttamente su un dato sottocomando (vedi documentazione).


2

Questo raggiunge lo scopo e questo sarà anche relfected nell'output autogenerato di argparse --help, che è ciò che vuole la maggior parte dei programmatori sani (funziona anche con argomenti opzionali):

parser.add_argument(
    'commands',
    nargs='+',                      # require at least 1
    choices=['process', 'upload'],  # restrict the choice
    help='commands to execute'
)

Documenti ufficiali su questo: https://docs.python.org/3/library/argparse.html#choices


1

Usa append_const per un elenco di azioni e quindi controlla che l'elenco sia popolato:

parser.add_argument('-process', dest=actions, const="process", action='append_const')
parser.add_argument('-upload',  dest=actions, const="upload", action='append_const')

args = parser.parse_args()

if(args.actions == None):
    parser.error('Error: No actions requested')

Puoi anche specificare i metodi direttamente all'interno delle costanti.

def upload:
    ...

parser.add_argument('-upload',  dest=actions, const=upload, action='append_const')
args = parser.parse_args()

if(args.actions == None):
    parser.error('Error: No actions requested')

else:
    for action in args.actions:
        action()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.