The Click package allows a range of values to be selected from a list using the click.Choice method.
In my case the values are relatively long strings, so using:
choice_names = [u'Vulnerable BMC (IPMI)', u'IoT Vulnerability', u'SMBv1', u'BadHTTPStatus', u'Compromised']
@click.option('--category', prompt='\nPlease enter the category of incident.\n\n -- Options:\n{}\n\n'.format(
format_choices(choice_names)), type=click.Choice(choice_names))
will list the values as:
-> Vulnerable BMC (IPMI)
-> IoT Vulnerability
-> SMBv1
-> BadHTTPStatus
-> Compromised
This requires the user to enter the full string, which is inconvenient. Does Click provide a functionality to select a value using only a numeric identifier? So, the above options could be listed as:
-> Vulnerable BMC (IPMI) [1]
-> IoT Vulnerability [2]
-> SMBv1 [3]
-> BadHTTPStatus [4]
-> Compromised [5]
and to select the first option, the user would need to enter 1
. This could be possible by defining a custom validation function, but I couldn't find any existing functionality offered by Click.
I came up with this:
class ChoiceOption(click.Option):
def __init__(self, param_decls=None, **attrs):
click.Option.__init__(self, param_decls, **attrs)
if not isinstance(self.type, click.Choice):
raise Exception('ChoiceOption type arg must be click.Choice')
if self.prompt:
prompt_text = '{}:\n{}\n'.format(
self.prompt,
'\n'.join(f'{idx: >4}: {c}' for idx, c in enumerate(self.type.choices, start=1))
)
self.prompt = prompt_text
def process_prompt_value(self, ctx, value, prompt_type):
if value is not None:
index = prompt_type(value, self, ctx)
return self.type.choices[index - 1]
def prompt_for_value(self, ctx):
# Calculate the default before prompting anything to be stable.
default = self.get_default(ctx)
prompt_type = click.IntRange(min=1, max=len(self.type.choices))
return click.prompt(
self.prompt, default=default, type=prompt_type,
hide_input=self.hide_input, show_choices=False,
confirmation_prompt=self.confirmation_prompt,
value_proc=lambda x: self.process_prompt_value(ctx, x, prompt_type))
@click.command()
@click.option('--hash-type', prompt='Hash', type=click.Choice(['MD5', 'SHA1'], case_sensitive=False), cls=ChoiceOption)
def cli(**kwargs):
print(kwargs)
Result:
> cli --help
Usage: cli [OPTIONS]
Options:
--hash-type [MD5|SHA1]
--help Show this message and exit.
> cli --hash-type MD5
{'hash_type': 'MD5'}
> cli
Hash:
1: MD5
2: SHA1
: 4
Error: 4 is not in the valid range of 1 to 2.
Hash:
1: MD5
2: SHA1
: 2
{'hash_type': 'SHA1'}
Edit May 25, 2020:
I recently came across questionary and integrated it with click
import click
import questionary
class QuestionaryOption(click.Option):
def __init__(self, param_decls=None, **attrs):
click.Option.__init__(self, param_decls, **attrs)
if not isinstance(self.type, click.Choice):
raise Exception('ChoiceOption type arg must be click.Choice')
def prompt_for_value(self, ctx):
val = questionary.select(self.prompt, choices=self.type.choices).unsafe_ask()
return val
@click.command()
@click.option('--hash-type', prompt='Hash', type=click.Choice(['MD5', 'SHA1'], case_sensitive=False), cls=QuestionaryOption)
def cli(**kwargs):
print(kwargs)
if __name__ == "__main__":
cli()
Since Click does not seem to provide a functionality of this kind, this custom validation function fulfills the purpose:
def validate_choice(ctx, param, value):
# Check if the passed value is an integer.
try:
index = int(value) - 1
# Return the value at the given index.
try:
return choice_names[index]
# If the index does not exist.
except IndexError:
click.echo('Please select a valid index.')
# If the value is of a different type, for example, String.
except (TypeError, ValueError):
# Return the value if it exists in the list of choices.
if value in choice_names:
return value
else:
click.echo('Please select a valid value from the choices {}.'.format(choice_names))
# Prompt the user for an input.
value = click.prompt(param.prompt)
return validate_choice(ctx, param, value)
@click.option('--category', prompt='\nPlease enter the category.\n\n -- Options:\n{}\n\n'.format(choice_names),
help='Category of the incident', callback=validate_category)
This allows a user to select a choice either by entering the choice name or by entering the index value. In case an invalid value is entered, the user is prompted again for an input.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With