Click: Get original command as string - python

I'm using the Click to build a command-line application. For audit purposes, need to get access full original command that the user executed. I had no luck getting the original user command before the Click parsing arguments. I couldn't find any similar use case in their documentation. something like :
#click.group()
#click.option('--debug/--no-debug', default=False)
#click.pass_context
def cli(ctx, debug):
if debug:
# print('Original unparssed command with arguments')
I wanted to check if anyone knows a trick to get that before opening an issue on the github.

Related

Invoking another command and getting user input from prompt

I'm trying to build an interactive menu using py-click. Basically just a structure that:
Lists the available commands from the current menu (using a click group)
Present user with a command that prompts for his selection (users enters a # / cmd name)
Invoke the command from within that selection menu
Those command could lead to either another menu or execute application code
Then return to main menu/previous menu as relevant once the code has run
My code:
import click
#click.group(invoke_without_command=True)
#click.pass_context
def main_group(ctx):
""" Lists all the submenu options available"""
cmds = main_group.list_commands(ctx)
click.echo(f"Available options:")
for idx, cmd_str in enumerate(cmds):
click.echo(f"{idx}:{cmd_str}")
click.echo(f"Now that you know all the options, let's make a selection:")
ctx.invoke(main_group.get_command(ctx, "selection"))
#main_group.command()
#click.option('--next_cmd', prompt='Next command:', help="Enter the number corresponding to the desired command")
#click.pass_context
def selection(ctx, next_cmd):
click.echo(f"You've selected {next_cmd}")
# check that selection is valid
# invoke the desired command
# return to parent previous command
#main_group.command()
def submenu_1():
click.echo('A submenu option ')
#main_group.command()
def submenu_2():
click.echo('Another option')
if __name__ == '__main__':
main_group()
However, the output from the above is:
Available options:
0:application-code
1:selection
2:submenu-1
3:submenu-2
Now that you know all the options, let's make a selection:
You've selected None
Process finished with exit code 0
Basically, the prompt from the selection command has no effect. But the selection command itself works, because if I run it directly:
if __name__ == '__main__':
# main_group()
selection()
then I am actually prompted for my selection. So... why is the prompt being ignored? Or is the basic premise behind my approach to building this the issue, e.g. Click isn't meant for that?
EDIT:
Going through a bunch of git repo that uses this library, including the examples they provide, I haven't been able to find any that build a structure somewhat similar to what I want. Basically the paradigm of building a click-application seems to be isolated command that perform action that modify the state of something, not so much a navigation through a menu offering different options. Not to say it's impossible, but it doesn't seem to be supported out of the box either.
No answers given to this question. I've spent some more time on this. I've made a little bit of progress on this but I am still not able to have something such as:
Main Menu
- Option 1
- Option 2
- Option 3
- Submenu 1
and then
Submenu 1
- Option 1
- Option 2
- Submenu 2
- Back to Main
etc. My conclusion is that click isn't the right tool to use for that. In fact, I'm not sure the alternative would necessarily make it much easier either (argparse, docopt, ...). Maybe to do that the best approach would be to just build a class structure yourself.
Or then, go with an approach that's closer to what the shell or docker use, don't bother with any menu navigation and just launch atomic commands that act on the state of the application.

Python click group: How to have -h/--help for all commands

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.

Automatically generate all help documentation for Click commands

Is there a way to generate (and export) help documentation using click for all commands and subcommands?
For example,
cli --help all --destination help-docs.txt
would generate help for commands and subcommands following the
cli command subcommand
format and put them into the help-docs.txt file.
The only way I can think that I would accomplish this is to use
cli command subcommand --help
on every subcommand that I wanted to generate help for and cat the output to a file, but it would be nice if there where an easier way to accomplish this using Click --help functionality.
This code will do for Click 7, using mostly documented APIs. You'd basically call recursive_help somewhere, e.g. as a separate subcommand, and pass it your top-level group object.
def recursive_help(cmd, parent=None):
ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
print(cmd.get_help(ctx))
print()
commands = getattr(cmd, 'commands', {})
for sub in commands.values():
recursive_help(sub, ctx)
Update 2019-10-05:
one way to use this, assuming cli is a click.group, would be:
#cli.command()
def dumphelp():
recursive_help(cli)
Since you are using click package, I know two cool solutions:
Is to use click-man to auto-generate Python click CLI man page.
Is to use md-click to auto-generate Python click CLI help in md file format.

Python click - allow a prompt to be empty

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

Passing unknown flags to command-line with click

Question
When using the python click library to create command-line tools, is it possible to pass an unknown number of arguments to a function? I am thinking of something similar to the *args command.
Usecase
I am trying to build a wrapper for catkin and would like to use click for all the nice utilities it comes with. This application should perform some tasks, like changing into the root of the workspace, before calling catkin with the specified command. E.g. catkin build to compile the code.
The problem with this is, that I do not want to explicitly declare every possible compile flag that you can pass to catkin but rather want to only look out for the actual command and pass all the arguments directly to catkin.
Example
What I have found so far, is the possibility to define a last argument with the option nargs=-1 which will collect all succeeding arguments in this variable. This way you can collect for example a couple of file names. This is almost what I am looking for, except that it wont take flags beginning with a dash -. It will from an error saying Error: no such option: -a
#!/usr/bin/python
import click
import subprocess
#click.command()
#click.argument('action', type=click.STRING)
#click.option('--debug', is_flag=True, help='Build in debug mode.')
#click.argument('catkin_args', nargs=-1, type=click.STRING)
def main(action, debug, catkin_args):
""" A wrapper for catkin """
# Do something here ...
if debug:
# Do some more special things...
subprocess.call(["catkin"] + catkin_args)
According to the docs it's possible in click 4.0+; you just need to set the type of your catkin_args to click.UNPROCESSED.
Documentation has an example wrapping timeit like you describe you want to do with catkin.

Categories