Put subparser command at the beginning of arguments - python

I wanted to create an interface with argparse for my script with subcommands; so, if my script is script.py, I want to call it like python script.py command --foo bar, where command is one of the N possible custom commands.
The problem is, I already tried looking for a solution here on StackOverflow, but it seems like everything I tried is useless.
What I have currently is this:
parser = argparse.ArgumentParser()
parser.add_argument("-x", required=True)
parser.add_argument("-y", required=True)
parser.add_argument("-f", "--files", nargs="+", required=True)
# subparsers for commands
subparsers = parser.add_subparsers(title="Commands", dest="command")
subparsers.required = True
summary_parser = subparsers.add_parser("summary", help="help of summary command")
If I try to run it with:
args = parser.parse_args("-x 1 -y 2 -f a/path another/path".split())
I got this error, as it should be: script.py: error: the following arguments are required: command.
If, however, I run this command:
args = parser.parse_args("summary -x 1 -y 2 -f a/path another/path".split())
I got this error, that I shouldn't have: script.py: error: the following arguments are required: -x, -y, -f/--files.
If I put the command at the end, changing also the order of arguments because of -f, it works.
args = parser.parse_args("-x 1 -f a/path another/path -y 2 summary".split())
If I add the parents keyword in subparser, so substitute the summary_parser line with summary_parser = subparsers.add_parser("summary", help=HELP_CMD_SUMMARY, parents=[parser], add_help=False), then I got:
script.py summary: error: the following arguments are required: command when summary is in front of every other argument;
script.py summary: error: the following arguments are required: -x, -y, -f/--files, command when summary is at the end of the args.
My question is, how I have to setup the parsers to have the behaviour script.py <command> <args>? Every command shares the same args, because they are needed to create certain objects, but at the same time every command can needs other arguments too.

Creating another parser helped me getting what I wanted.
The root parser should add all the optional arguments - and also have add_help=False, to avoid an help message conflict -, then another parser - parser2, with a lot of fantasy - will be created.
The second parser will have subparsers, and they all needs to specify as parents the root parser.
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument() ...
parser2 = argparse.ArgumentParser()
subparsers = parser2.add_subparsers(title="Commands", dest="command")
subparsers.required = True
summary_parser = subparsers.add_parser("summary", parents=[parser])
summary_parser.add_argument("v", "--verbose", action="store_true")
# parse args
args = parser2.parse_args()
Now the output will be this:
usage: script.py [-h] {summary} ...
optional arguments:
-h, --help show this help message and exit
Commands:
{summary}
summary For each report print its summary, then exit
usage: script.py summary [-h] -x X -y Y -f FILES [FILES ...] [-v]
optional arguments:
-h, --help show this help message and exit
-x X
-y Y
-f FILES [FILES ...], --files FILES [FILES ...]
-v, --verbose

Related

How to configure argparse to be like a comment central kind of cli

I'd like to do something like:
my-cli.py todo1 --todo1-option1 --todo1-option2 ...
my-cli.py todo2 --todo2-option1 --todo2-option2 ...
In addition, I'm hoping to declare --todo1-option1 is required if the command is todo1 and it is invalid parameter if I specify parameters for different command (i.e. todo2)
I'm also looking to have different -h for different command (i.e. todo1 or todo2).
I'm sorry if my English is a little broken. I hope you get what I mean.
The way I understand some tutorials, the main python app is the action itself like for example:
cp -f file1 file2
At first I thought there's some clever trick using the add_mutually_exclusive_group but the answer to my question is in fact the subparsers
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')
# A todo1 command
todo1_parser = subparsers.add_parser(
'todo1', help='List contents')
todo1_parser.add_argument(
'--todo1-option1', action='store', required=True,
help='Todo1 Option 1')
todo1_parser.add_argument(
'--todo1-option2', action='store',
help='Todo1 Option 2')
# A todo2 command
todo2_parser = subparsers.add_parser(
'todo2', help='List contents')
todo2_parser.add_argument(
'--todo2-option1', action='store', required=True,
help='Todo2 Option 1')
todo2_parser.add_argument(
'--todo2-option2', action='store',
help='Todo2 Option 2')
print(parser.parse_args())
Test for list of commands:
$ python test_argparse.py
python test_argparse.py usage: test_argparse.py [-h] {todo1,todo2} ...
test_argparse.py: error: too few arguments
Test for Todo1 required parameters:
$ python test_argparse.py todo1
usage: test_argparse.py todo1 [-h] --todo1-option1 TODO1_OPTION1 [--todo1-option2 TODO1_OPTION2]
test_argparse.py todo1: error: the following arguments are required: --todo1-option1
Test for Todo1 required parameters:
$python test_argparse.py todo2
usage: test_argparse.py todo2 [-h] --todo2-option1 TODO2_OPTION1 [--todo2-option2 TODO2_OPTION2]
test_argparse.py todo2: error: the following arguments are required: --todo2-option1
Test for Todo2 shouldn't be availble to Todo1:
$python test_argparse.py todo1 --todo2-option1 aaa
usage: test_argparse.py [-h] {todo1,todo2} ... test_argparse.py: error:
unrecognized arguments: --todo2-option1 aaa

"Invalid choice" error while using argparse

I'm writing a python script which takes in arguments from command line in the following format:
./myfile subcommand --change --host=myhost --user=whatever
--change is in a way required, meaning that for now this is only option available, but later I will add other options, but eventually user needs to choose between those options.
--host is required. It's not a positional argument.
--user is not required, and there is a default value for it. It's not a positional argument.
I'm using the following code to create this command, but
#!/usr/bin/env python
import argparse
parser = argparse.ArgumentParser(description='My example explanation')
subparsers = parser.add_subparsers(dest='action')
subparsers.required = True
subcommand_parser = subparsers.add_parser('subcommand')
subcommand_parser_subparser = subcommand_parser.add_subparsers()
change_subcommand_parser = subcommand_parser_subparser.add_parser('--change')
change_subcommand_parser.add_argument('--host', required=True)
change_subcommand_parser.add_argument('--user', required=False, default='Salam')
print(parser.parse_args())
When I try this code with the following command in CLI:
./myscript something --change --host hello --user arian
I get the following error:
usage: myscript subcommand [-h] {--change} ...
myscript subcommand: error: invalid choice: 'hello' (choose from '--change')
My questions are:
What's wrong with this code?
How can I make users type --user & --host, and not use them as positional arguments?

argparse - Reproduce behavior of "--help" argument with a specific argument

I have some trouble while working with the argparse module for Python v2.7.
Basically, what I have is a script that works with 5 mandatory arguments :
url
method
login
password
output
An example of the syntax would look like this :
script.py -w/--url [URL] -m/--method [METHOD] -l/--login [LOGIN] -p/--password [PASSWORD] -o/--output [OUTPUT]
What I'd like to do is this :
add an optional argument -t/--test
its behavior would be that, based on the url used with the -w/--url argument, it would bypass completely the -m/--method, -l/--login and -p/--password arguments but, for it to work, I need to tell argparse to stop processing arguments if -t/--test is provided (but only with -w/--url).
Is this behavior even possible? I tried to play with argparse sub-commands but it seems to be (at least to my small knowledge) a bit overkill.
NB: Here is my original code :
# Description : parses script arguments
# Argument(s) : all
# Return value : all arguments values
def testArgs():
parser = argparse.ArgumentParser(description='Foo', formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-w','--url', help='URL', required=True)
parser.add_argument('-t','--test', help='Test command', action='store_true')
parser.add_argument('-m','--method', help='METHOD', required=True)
parser.add_argument('-u','--login_name', help='LOGIN', required=True)
parser.add_argument('-p','--login_password', help='PASSWORD', required=True)
parser.add_argument('-o','--output_format', help='OUTPUT', required=True, choices=['json', 'yaml', 'python'], default='json')
args = parser.parse_args()
return args
EDIT : After a lot of testing, I have managed the following :
def testArgs():
parser = argparse.ArgumentParser(description='DESCRIPTION')
subparsers = parser.add_subparsers()
p_list = subparsers.add_parser('test', help='List all available methods')
p_list.add_argument('-w', help='URL', required=True)
p_list.add_argument('-t', help='Test', action='store_true', required=True)
p_cmd = subparsers.add_parser('cmd', help='Executes command')
p_cmd.add_argument('-w', help='URL', required=True)
p_cmd.add_argument('-m', help='Method', required=True)
p_cmd.add_argument('-l', help='Login', required=True)
p_cmd.add_argument('-p', help='Password', required=True)
p_cmd.add_argument('-o', help='Output', required=True)
args = parser.parse_args()
return args
Which exhibits the following behavior :
$ python testArgparse.py -h
usage: testArgeparse.py [-h] {test,cmd} ...
DESCRIPTION
positional arguments:
{test,cmd}
test Lists all available methods
cmd Executes command
optional arguments:
-h, --help show this help message and exit
But to access help on the others arguments, I need to do the following :
$ python testArgparse.py test -h
usage: testArgparse.py test [-h] -w W -t
optional arguments:
-h, --help show this help message and exit
-w W URL
-t Test
$ python testArgparse.py cmd -h
usage: testArgparse.py cmd [-h] -w W -m M -l L -p P -o O
optional arguments:
-h, --help show this help message and exit
-w W URL
-m M Method
-l L Login
-p P Password
-o O Output
I'd like to be able to, at least, display help about all arguments without having to use --help for both test and cmd arguments.
Ideally, what I'd like is this behavior :
$ python testArgparse.py [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
required=True with store_true does not make sense. The default is False, but if it is required the returned value will always be True.
Since only -w is required unconditionally, I would drop the required parameter on everything else. Then test for the required values after parse_args. I can still issue an argparse error with usage at that time. In other words, do my own testing rather than try something fancy in argparse.
def testArgs():
usage = '[-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]'
parser = argparse.ArgumentParser(description='DESCRIPTION',usage=usage)
parser.add_argument('-w', help='URL', required=True)
parser.add_argument('-t', help='Test', action='store_true')
parser.add_argument('-m', help='Method')
parser.add_argument('-l', help='Login')
parser.add_argument('-p', help='Password')
parser.add_argument('-o', help='Output')
args = parser.parse_args()
# sample test, streamline and refine to suit your needs
# this assumes the default for all these args is None
if not args.t: # '-t' in in argv
if any([args.m is None, args.l is None, args.p is None, args.o is None]):
parser.error('m,l,p,o are all required')
return args
1216:~/mypy$ python2.7 stack21070971.py
usage: [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
stack21070971.py: error: argument -w is required
1213:~/mypy$ python2.7 stack21070971.py -w url
usage: [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
stack21070971.py: error: m,l,p,o are all required
1213:~/mypy$ python2.7 stack21070971.py -w url -t
Namespace(l=None, m=None, o=None, p=None, t=True, w='url')
1214:~/mypy$ python2.7 stack21070971.py -w url -m mmm
usage: [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
stack21070971.py: error: m,l,p,o are all required
...
1215:~/mypy$ python2.7 stack21070971.py -w url -m mmm -l uuu -p ppp -o ooo
Namespace(l='uuu', m='mmm', o='ooo', p='ppp', t=False, w='url')

Using mutually exclusive between groups

I'm trying to have a mutually exclusive group between different groups:
I have the arguments -a,-b,-c, and I want to have a conflict with -a and -b together, or -a and -c together. The help should show something like [-a | ([-b] [-c])].
The following code does not seem to do have mutually exclusive options:
import argparse
parser = argparse.ArgumentParser(description='My desc')
main_group = parser.add_mutually_exclusive_group()
mysub_group = main_group.add_argument_group()
main_group.add_argument("-a", dest='a', action='store_true', default=False, help='a help')
mysub_group.add_argument("-b", dest='b', action='store_true',default=False,help='b help')
mysub_group.add_argument("-c", dest='c', action='store_true',default=False,help='c help')
parser.parse_args()
Parsing different combinations - all pass:
> python myscript.py -h
usage: myscript.py [-h] [-a] [-b] [-c]
My desc
optional arguments:
-h, --help show this help message and exit
-a a help
> python myscript.py -a -c
> python myscript.py -a -b
> python myscript.py -b -c
I tried changing the mysub_group to be add_mutually_exclusive_group turns everything into mutually exclusive:
> python myscript.py -h
usage: myscript.py [-h] [-a | -b | -c]
My desc
optional arguments:
-h, --help show this help message and exit
-a a help
-b b help
-c c help
How can I add arguments for [-a | ([-b] [-c])]?
So, this has been asked a number of times. The simple answer is "with argparse, you can't". However, that's the simple answer. You could make use of subparsers, so:
import argparse
parser = argparse.ArgumentParser(description='My desc')
sub_parsers = parser.add_subparsers()
parser_a = sub_parsers.add_parser("a", help='a help')
parser_b = sub_parsers.add_parser("b", help='b help')
parser_b.add_argument("-c", dest='c', action='store_true',default=False,help='c help')
parser.parse_args()
You then get:
$ python parser -h
usage: parser [-h] {a,b} ...
My desc
positional arguments:
{a,b}
a a help
b b help
optional arguments:
-h, --help show this help message and exit
and:
$ python parser b -h
usage: parser b [-h] [-c]
optional arguments:
-h, --help show this help message and exit
-c c help
If you would prefer your arguments as stated in the question, have a look at docopt, it looks lovely, and should do exactly what you want.
argument_groups don't affect the parsing. They just contribute to the help formatting. So defining a group within an mutually_exclusive_group does not help with this problem.
There is a proposed patch, http://bugs.python.org/issue10984, 'argparse add_mutually_exclusive_group should accept existing arguments to register conflicts', that would allow you to define two mutually_exclusive_groups, one [-a | -b] and the other [-a | -c]. Creating a 2nd group that includes an argument (-a) that is already defined is the trivial part of this patch. Producing a meaningful usage line is more difficult, and required a rewrite of several HelpFormatter methods.
import argparse
parser = argparse.ArgumentParser(description='My desc')
group1 = parser.add_mutually_exclusive_group()
action_a = group1.add_argument("-a", dest='a', action='store_true', default=False, help='a help')
group1.add_argument("-b", dest='b', action='store_true',default=False,help='b help')
group2 = parser.add_mutually_exclusive_group()
group2.add_argument("-c", dest='c', action='store_true',default=False,help='c help')
group2._group_actions.append(action_a) # THE KLUDGE
print parser.format_usage()
# usage: stack16769409.py [-h] [-a | -b] [-c]
args = parser.parse_args()
Usage does not show the 2 groups correctly. But it does accept -b -c, while objecting to -a -b and -a -c. But you can write a custom usage line.

Argparse subparser: hide metavar in command listing

I'm using the Python argparse module for command line subcommands in my program. My code basically looks like this:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="subcommands", metavar="<command>")
subparser = subparsers.add_parser("this", help="do this")
subparser = subparsers.add_parser("that", help="do that")
parser.parse_args()
When running "python test.py --help" I would like to list the available subcommands. Currently I get this output:
usage: test.py [-h] <command> ...
optional arguments:
-h, --help show this help message and exit
subcommands:
<command>
this do this
that do that
Can I somehow remove the <command> line in the subcommands listing and still keep it in the usage line? I have tried to give help=argparse.SUPPRESS as argument to add_subparsers, but that just hides all the subcommands in the help output.
I solved it by adding a new HelpFormatter that just removes the line if formatting a PARSER action:
class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter):
def _format_action(self, action):
parts = super(argparse.RawDescriptionHelpFormatter, self)._format_action(action)
if action.nargs == argparse.PARSER:
parts = "\n".join(parts.split("\n")[1:])
return parts

Categories