(The title should be OptionParser: show extra help, but the word help is not allowed in the title)
I have this OptionParser:
parser = OptionParser(
usage='usage: %s [options]' % (args[0]),
version='%s Version %s' % (args[0], version))
parser.add_option('-a', '--action', choices=['menu'] , help='Allowed actions are: menu, and any menu action', default='menu')
parser.add_option('-1', '--file1', type='string', help='First file')
parser.add_option('-2', '--file2', type='string', help='Second file')
parser.add_option('--debug', action='store_true', help='run in debug mode')
Calling with --help gives me:
Usage: /home/gonvaled/projects/bin/process_json.py [options]
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-a ACTION, --action=ACTION
Allowed actions are: menu, and any menu action
-1 FILE1, --file1=FILE1
First file
-2 FILE2, --file2=FILE2
Second file
--debug run in debug mode
This is fine, but there are missing actions. The problem is that some actions are defined in a Menu object (my implementation), and can only be shown by doing:
menu.showCommands()
That is, those actions are not defined for OptionParser, but are still accessible via the command line, since any action other than menu will be passed transparently to the Menu object to be processed. Thus, the help provided by OptionParser is not aware of those actions.
How can I tell the OptionParser object that when showing the help text, some external piece of information must be added? Appending at the end would be enough. I have taken a look at the docs, but no such option seems available.
EDIT
Actually running my program has shown that not only the help is missing. Even worse: optparse complains about unknown actions:
gonvaled#pegasus ~ » process_json.py -a prettyfy-from-obj
Usage: /home/gonvaled/projects/bin/process_json.py [options]
process_json.py: error: option -a: invalid choice: 'prettyfy-from-obj' (choose from 'menu')
How can I tell optparse to accept unspecified values for action?
The list you send to the choices parameter must be an exhaustive list of all possible parameters that the option can allow. If the list is ['menu'], then the only value allowed will be menu. That explains the errors you were getting.
Note: the optparse module is deprecated, you should use argparse
The simpler solution
Don't use choices and validate the argument afterwards.
parser.add_option('-a', '--action', help='Allowed actions are: menu, and any menu action', default='menu')
#...
options, args = parser.parse_args()
if options.action not in menu.showCommands():
parser.error('Invalid action specified: %s' % options.action)
Alternatively, if you can't do that, if you use the action calling menu.call(action, params) and it raises a UnavailableAction exception:
parser.add_option('-a', '--action', help='Allowed actions are: menu, and any menu action', default='menu')
#...
options, args = parser.parse_args()
# do something as normal
try:
menu.call(options.action, params)
except UnavailableAction:
print "Incorrect option" # (1)
# crash appropriately
If the parser object is available, you can change (1) for the following:
parser.error('Invalid action specified: %s' % options.action)
NOTE: this completely defeats the purpose of using choices, but can be applicable if you can not do the validation in the scope of the function/method/module that parses the CLI options.
Any other solution to this issue depend on your ability to introspect the menu object to get the list of available actions.
Using choices
Assuming that menu.showCommands just provides a list of strings of the available commands, my first guess would be to do something like:
menu_choices = menu.showCommands().split()
menu_choices.append('menu')
parser.add_option('-a', '--action', choices=menu_choices , help='Allowed actions are: menu, and any menu action', default='menu')
If there is no way to get the list of actions in that format, you may have to "parse" the output of menu.showCommands
Using callback
You could also implement it as a callback for the -a option:
def menu_option(option, opt_str, value, parser):
if not value not in menu.showCommands().split():
raise OptionValueError('Invalid action for menu')
parser.add_option('-a', '--action', action='callback', callback=menu_option, help='Allowed actions are: menu, and any menu action', default='menu')
This option is a little more flexible. For example, if you use a function called has_action(menu, string) to check if the menuobject has a string action available, you could do:
def menu_option(option, opt_str, value, parser):
if not has_action(menu, value):
raise OptionValueError('Invalid action specified for menu: %s' % value)
More complicated solutions
There are probably a lot more complicated solutions. Some ideas (that may or - more likely - not work) are:
subclassing OptionParser and create your own that has what you need (so that choices has no effect)
if menu has its own OptionParser, chain-load it using callbacks
if menu has its own sub parser, have argparse use it directly
Related
I am building a command line tool which should work as follows:
mytool [-h] [-c|--config FILE] [-l|--list] ACTION
positional arguments:
ACTION the action to be performed
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-l, --list show list of configured actions and exit
-c, --config CONFIG use CONFIG instead of the default configuration file
I am using argparse to parse the command line and I am facing what seems to be a limitation of the library. Let me explain through some use-cases.
Use-case 1
$ mytool -h
$ mytool -c path/to/file -h
$ mytool -l -h
$ mytool some-action -h
All the above invocations of mytool shall print the help message and exit, exactly as it is shown above, more importantly showing ACTIONS to be mandatory.
Use-case 2
$ mytool -l
$ mytool -c path/to/file --list
$ mytool --list some-action
$ mytool --list --config path/to/file
All the above invocations must list the configured actions given the content of the configuration files, and exit. The allowed values of ACTION depend on the content of the configuration file, they are not simply hard-coded in the program.
Notice that even if an action is given, it is ignored because -l|--list has a higher precendance, similar to how -h works against other flags and arguments.
Also, please note that solutions such as this, which implement custom argparse.Action sub-classes won't work because the action of the listing flag needs the value of the configuration flag, so the parsing must complete (at least partially) for the listing to begin.
Finally, the absence of the otherwise required positional argument ACTION does not cause the parser to abort with an error.
Use-case 3
In the absence of -l|--list the parser works as expected:
$ mytool -c path/to/file # exits with error, positional argument missing
$ mytool some-action # ok
In simple words, I am trying to make the -l|--list flag "disable" the mandatory enforcing of the positional argument ACTION. Moreover I am looking for a solution that allows me to perform the listing (the action of the -l|--list flag) after the parsing has (at least partially) completed, so the value of the -c|--config flag is available.
-h works the way it does because it uses a special action, argparse._HelpAction. (Simiarly, -v uses argparse._VersionAction.) This action causes the script to exit while parsing, so no folllowing arguments are processed. (Note that any side effects produced while parsing previous arguments may still occur; the only notion of precedence parse_args has is that arguments are processed from left to right.)
If you want -l to similarly show available actions and exit, you need to define your own custom action. For example,
class ListAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string):
print("Allowable actions are...")
print("foo")
print("bar")
print("baz")
parser.exit()
p = argparse.ArgumentParser()
p.add_argument("-l", "--list", action=ListAction)
...
args = p.parse_args()
In particular, p.parse_args(["-l", "-h"]) will list actions without displaying the help message, and p.parse_args(["-h", "-l"]) will print the help message but not list actions. Which one gets processed and terminates your script depends solely on which one appears first in the argument list.
As shown in the usage, ACTION will be required, unless the user uses '-h' or '-v', which exit in the middle of parsing.
You could define ACTION as nargs='?', or as a flagged argument, in which case it is optional.
I was going to suggest giving ACTION choices, which will then appear in the help. But if they depend on '--config' value that could be more awkward (though not impossible).
'-l' could have a custom action class that behaves like 'version', but prints the desired list and exits. But then you can't provide an action as well.
Creating arguments that depend on each other in some way is awkward, though not impossible. It easiest to handle interdependencies after parsing, when all arguments have been read. Keep in mind that argparse tries to accept arguments in any order, '-c' could be before, or after '-l', or Action.
In theory though your custom list action, could check the Namespace for a 'config' value, and base its action on that value. That would work only if you can count on the user providing '-c' before '-l'; argparse won't enforce that for you.
As a general rule it's best to think of argparse as a tool for finding out what the user wants, with limited assistance in policing that, and an ability to automatically format usage and help. Don't expect it to help with complicated interactions and logical mixes of arguments.
Context:
I'm having several scripts with loads of sub commands that I'd like to convert to using click
At the moment all these commands do accept -h and --help in order to display help options. I'd like to keep this behavior.
Problem:
click accepts by default --help to display the help text, but not -h
for a click command this can be changed easily by adding.
#click.group()
#click.help_option("--help", "-h")
def cli():
""" the doc string """
enter code here
#cli.command()
#click.help_option("--help", "-h")
def mycommand()
pass
#cli.command()
#click.help_option("--help", "-h")
def mycommand1()
pass
...
However if I'm having tens of commands I have to reapply the decorator line
#click.help_option("--help", "-h")
fort each sub command.
Would there be any trick to avoid having to write this line everywhere?
You need to define a CONTEXT_SETTINGS and use it like this:
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.command(context_settings=CONTEXT_SETTINGS)
def cli():
pass
From the click documentation:
Help Parameter Customization Changelog The help parameter is
implemented in Click in a very special manner. Unlike regular
parameters it’s automatically added by Click for any command and it
performs automatic conflict resolution. By default it’s called --help,
but this can be changed. If a command itself implements a parameter
with the same name, the default help parameter stops accepting it.
There is a context setting that can be used to override the names of
the help parameters called help_option_names.
This example changes the default parameters to -h and --help instead
of just --help:
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.command(context_settings=CONTEXT_SETTINGS) def cli():
pass And what it looks like:
$ cli -h Usage: cli [OPTIONS]
Options: -h, --help Show this message and exit.
I'm developing a simple project with the purpose or learning Python, actually I have version 3.6 and I wanted to build a command line tool to generate password with specific criteria. So fare here is what I got:
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-a", "-auto", help="auto mode", action="store_true")
group.add_argument("-m", "-manual", help="manual mode", action="store_true")
parser.parse_args()
the dead end is I have no idea how to limit a command, example -l -lenght to the reqisite of having -m another stop is, how do I define -l so that it can accept a value, for example -l:8 to specify a password of 8 characters, I also want to put a range limit on -l values, example -l:8-256 and finally, I dont understand the difference between - and -- when defining the parameters.
I have already done all the part of generating passwords, just need a way to control its flow from outside so implementing parameters looked like a good way of doing this.
What you are looking for is the choices option.
add_argument('--length', type=int, choices=range(8,257)
This will only accept integer values between 8 and 256 (inclusive).
As for your second question, - indicates the shorthand for an option, which is common in most CLI tools, well -- indicates the long hand. It is common practice is CLI tools to provide both a long and a short hand for options.
You can define a custom type to check for valid values:
from argparse import ArgumentTypeError
def passwd_len(s):
try:
s = int(s)
except ValueError:
raise ArgumentTypeError("'%s' is not an integer" % (s,))
if not (8 <= s <= 256):
raise ArgumentTypeError("%d is not between 8 and 256, inclusive" % (s,))
return s
parser.add_argument("--length", "-l", type=passwd_len)
The difference between -- and - is one of conventions. Historically, options were single-letter arguments prefixed with a -, and groups of options could be combined: -ab was the same as -a -b. In order to
support that convention and allow longer option names, a new convention
of using -- to prefix multi-character names was devised. --length is a single option, not a group of six options -l, -e, -n, -g, -t, and -h. Long options cannot be grouped.
I would define the -a and -m options like this:
group = parser.add_mutually_exclusive_group()
group.add_argument("-a", "--auto", help="auto mode", action="store_const", const="auto", dest="mode")
group.add_argument("-m", "--manual", help="manual mode", action="store_const", const="manual", dest="mode")
group.add_argument("--mode", choices=["auto", "manual"])
Now instead of having two Boolean attributes that can never have the same value, you have just one attribute whose value you can check directly. Think of --mode as being the canonical way of choosing a mode, with -a and -m as shortcuts for selecting a specific mode.
I'm using the click library for a CLI application. I have various options that the user can specify, and most of them have prompts turned on. However, even if an option isn't required, if you set click to prompt for the option, it won't accept an empty response (like just hitting enter). For example:
#click.option('-n', '--name', required=True, prompt=True)
#click.option('-d', '--description', required=False, prompt=True)
>>> myscript -n Joe
>>> Description: [Enter pressed]
>>> Description: [Enter pressed; click doesn't accept an empty parameter]
Is there a way to get around this, or would this require a feature request?
when you add default="" then an empty string is also accepted:
#click.option('-d', '--description', prompt=True, default="")
Note that required is not a possible argument for option, at least according to the docs
I'm developing a management script that does a fairly large amount of work via a plethora of command-line options. The first few iterations of the script have used optparse to collect user input and then just run down the page, testing the value of each option in the appropriate order, and doing the action if necessary. This has resulted in a jungle of code that's really hard to read and maintain.
I'm looking for something better.
My hope is to have a system where I can write functions in more or less normal python fashion, and then when the script is run, have options (and help text) generated from my functions, parsed, and executed in the appropriate order. Additionally, I'd REALLY like to be able to build django-style sub-command interfaces, where myscript.py install works completely separately from myscript.py remove (separate options, help, etc.)
I've found simon willison's optfunc and it does a lot of this, but seems to just miss the mark — I want to write each OPTION as a function, rather than try to compress the whole option set into a huge string of options.
I imagine an architecture involving a set of classes for major functions, and each defined method of the class corresponding to a particular option in the command line. This structure provides the advantage of having each option reside near the functional code it modifies, easing maintenance. The thing I don't know quite how to deal with is the ordering of the commands, since the ordering of class methods is not deterministic.
Before I go reinventing the wheel: Are there any other existing bits of code that behave similarly? Other things that would be easy to modify? Asking the question has clarified my own thinking on what would be nice, but feedback on why this is a terrible idea, or how it should work would be welcome.
Don't waste time on "introspection".
Each "Command" or "Option" is an object with two sets of method functions or attributes.
Provide setup information to optparse.
Actually do the work.
Here's the superclass for all commands
class Command( object ):
name= "name"
def setup_opts( self, parser ):
"""Add any options to the parser that this command needs."""
pass
def execute( self, context, options, args ):
"""Execute the command in some application context with some options and args."""
raise NotImplemented
You create sublcasses for Install and Remove and every other command you need.
Your overall application looks something like this.
commands = [
Install(),
Remove(),
]
def main():
parser= optparse.OptionParser()
for c in commands:
c.setup_opts( parser )
options, args = parser.parse()
command= None
for c in commands:
if c.name.startswith(args[0].lower()):
command= c
break
if command:
status= command.execute( context, options, args[1:] )
else:
logger.error( "Command %r is unknown", args[0] )
status= 2
sys.exit( status )
The WSGI library werkzeug provides Management Script Utilities which may do what you want, or at least give you a hint how to do the introspection yourself.
from werkzeug import script
# actions go here
def action_test():
"sample with no args"
pass
def action_foo(name=2, value="test"):
"do some foo"
pass
if __name__ == '__main__':
script.run()
Which will generate the following help message:
$ python /tmp/test.py --help
usage: test.py <action> [<options>]
test.py --help
actions:
foo:
do some foo
--name integer 2
--value string test
test:
sample with no args
An action is a function in the same module starting with "action_" which takes a number of arguments where every argument has a default. The type of the default value specifies the type of the argument.
Arguments can then be passed by position or using --name=value from the shell.