shtab icon indicating copy to clipboard operation
shtab copied to clipboard

Give instructions to user with zero possible completions

Open mauvilsa opened this issue 2 years ago • 6 comments

A feature that jsonargparse+argcomplete supports is the print of help when there are zero possible completions. It would be great if the same feature can be implemented for shtab.

To explain what this feature does, the most simple case is an int argument. The value given to the argument could be already or not yet a valid int. In both cases nothing can be completed, only a message can be given to help the user. The following code snippet and behavior in the shell can illustrate this a bit better:

from typing import Dict
from jsonargparse import ArgumentParser
from jsonargparse.typing import PositiveFloat, Email

parser = ArgumentParser()
parser.add_argument('--int', type=int)
parser.add_argument('--dict', type=Dict[str, int])
parser.add_argument('--email', type=Email)
parser.add_argument('--positive', type=PositiveFloat)
parser.parse_args()
$ cli.py --int <TAB><TAB>
 value not yet valid, expected type int
$ cli.py --int 1<TAB><TAB>
 value already valid, expected type int
$ cli.py --int 1

$ cli.py --dict <TAB><TAB>
 value not yet valid, expected type Dict[str, int]
$ cli.py --dict '{"v": <TAB><TAB>
 value not yet valid, expected type Dict[str, int]
$ cli.py --dict '{"v": 1}'<TAB><TAB>
 value already valid, expected type Dict[str, int]
$ cli.py --dict '{"v": 1}'

$ cli.py --email <TAB><TAB>
 value not yet valid, expected type Email
$ cli.py --email abc@<TAB><TAB>
 value not yet valid, expected type Email
$ cli.py --email [email protected]<TAB><TAB>
 value already valid, expected type Email
$ cli.py --email [email protected]

$ cli.py --positive -1.5<TAB><TAB>
 value not yet valid, expected type PositiveFloat
$ cli.py --positive 1.5<TAB><TAB>
 value already valid, expected type PositiveFloat
$ cli.py --positive 1.5

Some details from when this was implemented for argcomplete can be seen in https://github.com/kislyuk/argcomplete/issues/328.

It is not clear to me how this could be implemented for shtab. Since there can be any number of data types that would have this behavior, this probably should be done with a completer that runs a python script. This is suggested in the last point in comment https://github.com/omni-us/jsonargparse/issues/108#issuecomment-986740522. However, neither the documentation of shtab or the customcomplete.py example make it clear how to do something like this.

mauvilsa avatar Mar 03 '22 18:03 mauvilsa

It is not clear to me how this could be implemented for shtab. Since there can be any number of data types that would have this behavior, this probably should be done with a completer that runs a python script.

I think this might be a bit difficult, as it can be complicated to implement using native shell complements, and I haven't seen any examples calling scripts in other languages.

tshu-w avatar Mar 04 '22 08:03 tshu-w

I don't think it is difficult. The documentation is missing how to implement a completer that runs a script. Having this clear, take for example that when completing it would run complete.py Email abc@. The python script could print to stderr the message and send a signal to the shell to redraw the prompt. The complete.py call would return zero completions and shtab should not add anything. It is the same as what is being done for argcomplete.

mauvilsa avatar Mar 04 '22 08:03 mauvilsa

Despite that, I really haven't seen any examples, at least in zsh completion, calling scripts in other languages as action, which involves how to tell the current command line state and then pass it back to the script, etc. It's hard for people who are not particularly familiar with each shell system completion to do that, which is probably why argcomplete only supports bash at the moment.

In addition, I think checking for legal variables on each input can be slow in a complex CLI, especially if I use a plugin like zsh-autocomplete that automatically generates completions. Speed is also a big problem for me when using argcomplete.

tshu-w avatar Mar 05 '22 07:03 tshu-w

The motivation for this feature is precisely to allow implementing automatic generation of completions. In particular, automatic generation from type hints.

Also note that it is not strictly necessary to call another script for this feature to be useful. So there would be no issues regarding speed. For the example above the completions could be:

$ cli.py --int <TAB><TAB>
 expected type int

$ cli.py --dict '{"v":<TAB><TAB>
 expected type Dict[str, int]

$ cli.py --email [email protected]<TAB><TAB>
 expected type Email

$ cli.py --positive -1.5<TAB><TAB>
 expected type PositiveFloat

The shell specific logic should be part of shtab, such that people using it don't need to worry about these details.

How to tell shtab to do this could be like:

parser.add_argument('--int', type=int).complete = shtab.instructions("expected type int")

This would be if someone using argparse would want to do this. But my goal is that people using jsonargparse would have no need to specify .complete attributes. They just add arguments normally with any type, or add a bunch of arguments from signatures. The completions would get added automatically.

mauvilsa avatar Jul 08 '23 05:07 mauvilsa

Looks like 2 separate issues to me.

custom completion

Assuming you have this:

cli.py list-valid-ints # prints a finite (!) list of options
cli.py --int <int>     # should tab-complete based on the above

Then see https://github.com/iterative/dvc/blob/main/dvc/cli/completion.py which has this pattern. The docs indirectly mention this (https://docs.iterative.ai/shtab/use/#library-usage -> https://github.com/iterative/dvc/blob/main/dvc/commands/completion.py). PRs/suggestions (modifying the docs source) to make it clearer are welcome.

type validation

However if you have this:

cli.py --int <int>     # tab (rather than enter) should perform validation

I feel that's probably out of scope? Isn't the whole point of argparse.ArgumentParser.add_argument(type) to perform type validation? Seems odd to offload this to shtab.

casperdcl avatar Jul 08 '23 18:07 casperdcl

I feel that's probably out of scope? Isn't the whole point of argparse.ArgumentParser.add_argument(type) to perform type validation? Seems odd to offload this to shtab.

The point of a tool like shtab is to develop ways to help users write commands, before the command is executed. How much to help depends on the developer of the CLI. What would be good is that shtab provides several ways to help users. Using as example such a simple type as int does not illustrate the need for it. But when a type is quite complex, some help that depends on what value for the argument has been written up to a point, surely could be welcome. So, I would say that it is not out of scope.

As I said in my previous comment, there is no need for value validation for the proposed feature to be useful. So this github issue can focus on giving static instructions with zero possible completions.

Then see https://github.com/iterative/dvc/blob/main/dvc/cli/completion.py which has this pattern. The docs indirectly mention this ...

Being required to implement shell specific preambles is not developer friendly and is a big amount of boilerplate if a similar feature is to be added to different projects. What this github issue proposes is to add to shtab a simple way to configure this.

Regarding the documentation, it is quite far away from explaining about preambles and shell specific code. Regardless, I don't think this is about improving documentation of the current shtab features.

cli.py list-valid-ints # prints a finite (!) list of options
cli.py --int <int>     # should tab-complete based on the above

It is not clear to me what is the behavior with this. Let me give a different example. The behavior can be shell dependent, so assume the following is for bash. Assume an argument is added like

action = parser.add_argument('--lottery-numbers', type=int, nargs=5)

Note that there is no parser validation regarding the range of numbers. And since they are numbers, there is nothing that can be completed, i.e. the command written by the user won't be extended. With the proposed feature, a developer could do

action.complete = shtab.instructions("give five numbers between 1 and 56")

The tab behavior would be the following.

$ cli.py --lottery-numbers <TAB><TAB>
give five numbers between 1 and 56
$ cli.py --lottery-numbers 1 2<TAB><TAB>
give five numbers between 1 and 56
$ cli.py --lottery-numbers 1 2 3 4 5 6 7<TAB><TAB>
give five numbers between 1 and 56
$ cli.py --lottery-numbers 1 2 3 4 5 6 7<CURSOR>

Note that the different prompts $ are not different examples. This is how the terminal would look like. After a <TAB><TAB>, the instructions are printed and the prompt is redrawn with the same text the user had written. Then the user writes more and again does <TAB><TAB>. In this example first no value is written, then 1 2, then adds 3 4 5 6 7. On purpose I added more than 5 numbers because there would be no validation before running the command.

A final note. Have in mind what I wrote at the end of the description in https://github.com/iterative/shtab/issues/68. My idea is that later on the complete attribute could support being a list. For example, be possible something like

action = parser.add_argument("--style", type=str)
action.complete = [
    shtab.path_completer(["*.jpg"]),
    shtab.instructions("path to the image with the target style to apply"),
]

In this case <TAB><TAB> would print the instructions and the paths that match, redraw the prompt and if possible extend the command with a common prefix, e.g. the user wrote target and all images are like target_style_*.jpg, so _style_ would be appended.

mauvilsa avatar Jul 09 '23 06:07 mauvilsa