Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1346 path/host completion for custom types #1347

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

loicrouchon
Copy link

This is an early PR for issue #1346
The goal is to ask for feedback before going further.

I haven't found a CONTRIBUTING.md, therefore I have a few process oriented questions.

  • What is the java version to use? I saw some java 5 mentioned in the build.gradle, but it seems travis only builds on JDK 8 and above?
  • Is there a commit/branch/pr naming convention?
  • How to write tests/which level of testing is deemed acceptable?

Concerning the issue in itself, I used a slightly different approach to avoid to pass the extra argument all the way around in the AutoComplete command.
This has the advantage that in case the auto-complete is embed as a subcommand, it is possible to programmatically register the path completion types as done for the type converters.

The path completion types are directly stored in the CommandLine and not in the Interpreter as I thought it is does not belong to the interpreter. But I'm not 100% sure of what the interpreter is, so let me know if you think I should move it inside.

What remains to be done:

  • AutoComplete error handling in case the class is not found in the classpath
  • Writing unit tests for the additional CommandLine methods
  • Writing additional integration tests (subcommands for example)
  • Updating documentation to mention this new feature and how to use it

What does not work:

  • I did a quick test and the completion of path list only works for the first argument. There is no completion given for the second and I suspect it's also not working with a separator. But this is way out of scope of this issue/pr

Let me know what you think of the current approach and I'll move forward with your feedback

Regards,

@loicrouchon loicrouchon force-pushed the feature/1346-path-completion-for-custom-types branch from b8c7f06 to 9cb448b Compare March 16, 2021 20:29
@codecov
Copy link

codecov bot commented Mar 16, 2021

Codecov Report

Merging #1347 (0cb42e7) into master (7438ef4) will increase coverage by 0.04%.
The diff coverage is 97.82%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #1347      +/-   ##
============================================
+ Coverage     93.76%   93.81%   +0.04%     
- Complexity      474      476       +2     
============================================
  Files             2        2              
  Lines          6961     6998      +37     
  Branches       1869     1872       +3     
============================================
+ Hits           6527     6565      +38     
  Misses          147      147              
+ Partials        287      286       -1     
Impacted Files Coverage Δ Complexity Δ
src/main/java/picocli/AutoComplete.java 97.33% <97.82%> (+0.52%) 146.00 <8.00> (+2.00)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7438ef4...0cb42e7. Read the comment docs.

@remkop remkop added theme: auto-completion An issue or change related to auto-completion type: enhancement ✨ labels Mar 17, 2021
@remkop
Copy link
Owner

remkop commented Mar 17, 2021

First, thanks for pointing out the lack of a Contributing document. I just added one to the project. It's a first version, and suggestions for improvement this document are very welcome! 👍 😉

About the PR, my intention was to not reflect this in the CommandLine model, and keep the change limited to the AutoComplete class. If anything, this is a concern of the completion, not the command.

About the new option, I am not sure if there should be a short option. Providing a short option signals to end users that this is a common thing to do, common enough to provide a shortcut. Providing only a long option signals that this is possible but optional; this seems most appropriate.

Can I ask you to change the above?

I did a quick test and the completion of path list only works for the first argument. There is no completion given for the second and I suspect it's also not working with a separator. But this is way out of scope of this issue/pr

That is interesting. Looks like an existing bug. Would you mind raising a separate issue for this? (Ideally with how to reproduce it)

@loicrouchon
Copy link
Author

First, thanks for pointing out the lack of a Contributing document. I just added one to the project. It's a first version, and suggestions for improvement this document are very welcome! 👍 😉

👍 Great, I'll take a look

About the PR, my intention was to not reflect this in the CommandLine model, and keep the change limited to the AutoComplete class. If anything, this is a concern of the completion, not the command.

❓ I thought the same but then I wondered how to have this as part of the application when the application having picocli.AutoComplete#GenerateCompletion as a subcommand.

So far, I only see sub-classing picocli.AutoComplete#GenerateCompletion and overriding the run method to set the parameters, but I'm not sure it's a great solution. Do you have a better solution for this?

About the new option, I am not sure if there should be a short option. Providing a short option signals to end users that this is a common thing to do, common enough to provide a shortcut. Providing only a long option signals that this is possible but optional; this seems most appropriate.

👍 I will remove the short option.

I did a quick test and the completion of path list only works for the first argument. There is no completion given for the second and I suspect it's also not working with a separator. But this is way out of scope of this issue/pr

That is interesting. Looks like an existing bug. Would you mind raising a separate issue for this? (Ideally with how to reproduce it)

Sure, that was on my list to create an issue. I will first create a reproducer to ensure I did not had a weird setup leading to this behavor.

@remkop
Copy link
Owner

remkop commented Mar 18, 2021

❓ I thought the same but then I wondered how to have this as part of the application when the application having picocli.AutoComplete#GenerateCompletion as a subcommand.

So far, I only see sub-classing picocli.AutoComplete#GenerateCompletion and overriding the run method to set the parameters, but I'm not sure it's a great solution. Do you have a better solution for this?

One idea is to have a custom generate-completion command that (in its run method) invokes the built-in picocli.AutoComplete#GenerateCompletion command with the desired options.

@loicrouchon
Copy link
Author

One idea is to have a custom generate-completion command that (in its run method) invokes the built-in picocli.AutoComplete#GenerateCompletion command with the desired options.

👍 Ok, I'll write a test to for this too

@loicrouchon loicrouchon changed the title #1346 path completion for custom types #1346 path/host completion for custom types Mar 18, 2021
@loicrouchon
Copy link
Author

  • Short option removed
  • CommandLine modifications reverted
  • host custom completion added (it's the same mechanism than the path one)

Before moving forward with documentation and better test coverage (Sub-command mode, @Parameters use case), I'd like your opinion on the TypeCompletionRegistry I introduced in the AutoComplete command.

It only handles dynamic completions (path/host) and not static ones CommandLine#completionCandidates().
If you think it would make sense to do so, then the problem is more complicated as the notion of @Options and @Parameters have to be given to the TypeCompletionRegistry method which implies that the return of method TypeCompletionRegistry#forType is not an enum anymore, but an object that knows how to generate the shell autocomplete code.
I don't wanna go all this way for which I see little benefits now, but maybe you have another opinion.

Let me know.

@remkop
Copy link
Owner

remkop commented Mar 19, 2021

Your TypeCompletionRegistry has got me thinking...

Thank you for putting in all this effort!
This prototyping makes it very concrete and makes it easier to see pros and cons.

I feel a bit guilty that you are putting in all this effort but I don't feel we have arrived at the best solution yet.
Would you mind discussing a bit more and considering more options?

I was thinking... Perhaps it would be nicer to not have to specify type names on the command line. I can see that now.
At the same time, I still believe that this information should not live in the CommandLine or CommandSpec model. (And perhaps I am wrong about this, that is possible. It's just that I want to explore alternative approaches first.)

I thought of an alternative (there may be other ideas too, let me know).
Perhaps we can introduce an annotation like @picocli.AutoComplete.HostCompletion and similar for File/Path, something like that, exact name TBD...

Then applications can annotate the option, something like this:

class MyCommand {

    @picocli.AutoComplete.FileCompletion
    @Option(names = "...")
    List<Repository> repositories;

Not sure if that can be made to work without eventually pulling this into the ArgSpec model...

An alternative is to annotate the custom type itself:

@picocli.AutoComplete.FileCompletion
class Repository { ... }

This would be easier to implement (in the AutoComplete class without changing the ArgSpec model), but perhaps less convenient for users because this type may be not under their control (what if Repository is a library type)...

Thoughts?

@loicrouchon
Copy link
Author

Haha, don't worry about this, I totally understand your points and I know of experience things are easy until you get into details.
Also here, it will impact the public API of Picocli, so better put some thoughts into it 😉

I'm not a big fan of the second approach for the exact reason you mentioned. In case you don't have control over the type itself (third-party library). But also for a question of separation of concerns as it would make this type aware of being used in a CLI environment.

Regarding the extra annotation on the options/argument itself, it's nice as it would be possible to only read it in the AutoComplete itself, thus, not affecting the CommandLine nor ArgSpec. However, it forces you to perform this declaration everywhere the type is used as an @Option / @Parameters, which, I will for sure forget somewhere when using it.

An alternative approach which I'm not completely satisfied with, is to put this annotation on implementations of the TypeConverter. At first glance it looks weird, but on the other hand, the TypeConverter acts as a bridge between a custom type and a command line aware type. In some ways, adding the annotation there would indicate how to convert the type to its auto-complete counterpart.
It could also be a default method on it TypeConverter.

But then the AutoComplete command would need the TypeConverters to be given in input as they are not part of the main command.

I tried to summarize below the different approaches with their pros/cons.

  • CommandLine/CommandSpec:
    • ✅ : centralized
    • ❌ : extra args to the AutoComplete standalone command: --pathCompletionsType,--hostCompletionsType
    • ✅ : no need for extra args to AutoComplete in sub-command modes. The CommandLine is already configured
  • Independent TypeCompletionRegistry:
    • ✅ : centralized
    • ❌ : extra args to the AutoComplete standalone command: --pathCompletionsType,--hostCompletionsType
    • ❌ : complicated to be used as a sub-command mode (still need to figure out the proper usage pattern).
  • Arguments annotations (@picocli.AutoComplete.FileCompletion, ...)
    • ❌ : Not centralized. To be repeated for every single @Option/@Parameters of the given type.
    • ✅ : no need for extra args to AutoComplete in standalone command
    • ✅ : no need for extra args to AutoComplete in sub-command modes.
  • Annotations on the TypeConverter
    • ✅ : Centralized
    • ❌ : extra args to the AutoComplete standalone command: --typeConverters,
    • ✅ : no need for extra args to AutoComplete in sub-command modes. The TypeConverters are already registered
  • Other solution: ???

PS 1: I did not include the fact you don't like this info to be part of the CommandLine/CommandSpec model simply because I don't really know how to phrase it in the above summary. But we have to keep it in mind.

PS 2: If the solution we go for can also support future evolution (for example a dynamic option that would call the app itself with the current command line context) that would be amazing.

@loicrouchon loicrouchon force-pushed the feature/1346-path-completion-for-custom-types branch from 516b4b9 to 0cb42e7 Compare March 21, 2021 10:37
@loicrouchon
Copy link
Author

Above force push was to fix the author email in the commits.

Regarding the possibles solutions, I can't think of any other viable/acceptable ones for the moment.

Looking at the previously mentioned ones, my preference goes to the annotations on the TypeConverter approach which allows:

  • a single declaration of Type to completion mode
  • no extra fields in the CommandLine/CommandSpec model
  • a single extra parameter to the AutoComplete: --typeConverters
  • a compatible approach with the existing AutoComplete in sub-command mode

Regarding the semantics of the TypeConverter, I think it is acceptable to add completions annotations on it. In a way, a TypeConverter explains how to convert the CLI input into a custom type. Auto complete can be IMHO part of what we consider the CLI input in this context.

WDYT?

@remkop
Copy link
Owner

remkop commented Mar 22, 2021

Sorry for my late reply. It is getting hard recently to find time to sit down and really think about this and other picocli tasks... :-)

Annotations on the TypeConverter

One disadvantage of the TypeConverter idea is that I believe many type converters may be specified as a lambda, like this:

new CommandLine(new MyApp())
    .registerConverter(Cipher.class, s -> Cipher.getInstance(s))
    .execute(args);

...so, many type converters may not be a full-fledged class to begin with - meaning, no place to put an annotation.

It also "feels" a bit like we are conflating two separate concerns here; the job of a type converter is to convert user input Strings into some other type. I can see how it could be made to work, but it seems that there must be a cleaner way to do this that is still convenient for users.

Arguments annotations (@picocli.AutoComplete.FileCompletion, ...)

In your analysis of various approaches and their pros and cons (very helpful, by the way, thank you for that!), about the first point:

x : Not centralized. To be repeated for every single @Option/@Parameters of the given type.

I am not sure how much of a disadvantage this is: I imagine this annotation would only need to be specified on a handful of options (I could be wrong of course).

Your point can also be taken as an advantage: is it possible that some applications want the built-in shell file/host completion for some options and custom completionCandidates for other options of a given custom type? It may be rare, but this design does make it possible, at the cost of diminished convenience. (So I guess the question is: how heavy is this cost vs. how valuable is this flexibility.)


Out of the options we have considered so far, I like argument annotations (@picocli.AutoComplete.FileCompletion, ...) the best, but I am not sure how easy this would be to implement, or even whether there is another approach/design altogether that we haven't considered yet.

@loicrouchon
Copy link
Author

Hello, no problem 😉

Here is some context for: "Not centralized. To be repeated for every single @Option/@parameters of the given type".

I'm currently working on a project named symly (the name might change) where I have two important notions: Repository and SourceDirectory (here also the names might change).
They both are basically Path and have there type converters here: https://github.com/loicrouchon/symly/tree/main/src/main/java/org/symly/cli/converters.

The project would consist of several subcommands: link, status, add, remove, clean, unlink, ...
In all of those commands I would need to have Repository and SourceDirectory. But not always in the same form, sometimes a list, sometime just one, ... So it makes it not so easy to factorize it.

Having to repeat it is not the end of the world, but testing the auto-complete is properly generated is not super handy. So I fear I'll forget the annotations somewhere and have a not so great auto-complete script.
I know this is a "me" problem 🤣 as I could (and should) test the auto-complete generated script of my CLI.

To summarize: I like the annotations approach. Putting on @Option/@Parameters might be a bit more work for the developers when the type is used a lot in their CLI. But I agree it's not a significant one, so let's go for this approach 🙌

I will give it a few thoughts, this week, but I have a few other questions:

  1. What should we do about the completionCandidates attribute of the existing @Option/@Parameters. Should we mark it as deprecated and ask users to use a new custom annotation for auto-complete?

  2. Regarding the design, how many annotations should there be?

  • @AutoComplete(kind = FILE|HOST|STATIC, [candidates = {""}])
  • @AutoComplete.HostCompletion, @AutoComplete.FileCompletion(type = DIRECTORY|FILE, filter="*"), @AutoComplete.StaticCompletion(candidates={""}), [@AutoComplete.DynamicCompletion(cmd = "complete")]

From what I see the general approach in picocli is to have big annotations like @Option/@Parameters, but in this context, I feel the multi annotations approach would be the more flexible/extensible.

@remkop
Copy link
Owner

remkop commented Mar 23, 2021

I will give it a few thoughts, this week, but I have a few other questions:

  1. What should we do about the completionCandidates attribute of the existing @Option/@Parameters. Should we mark it as deprecated and ask users to use a new custom annotation for auto-complete?
  2. Regarding the design, how many annotations should there be?
  • @AutoComplete(kind = FILE|HOST|STATIC, [candidates = {""}])
  • @AutoComplete.HostCompletion, @AutoComplete.FileCompletion(type = DIRECTORY|FILE, filter="*"), @AutoComplete.StaticCompletion(candidates={""}), [@AutoComplete.DynamicCompletion(cmd = "complete")]

Reading your questions I got a strong sense that this approach (annotations on options/parameters) is the right approach. Still a lot to discover and flesh out, but this feels like the right direction!

From what I see the general approach in picocli is to have big annotations like @Option/@Parameters, but in this context, I feel the multi annotations approach would be the more flexible/extensible.

Picocli's @Option/@Parameters annotations are successful because they are simple and intuitive, and make it easy for users to get started and find more detail later as needed. There is still flexibility and extensibility since annotations allow new attributes to be added later (for example, completionCandidates was not there from the beginning). I would like to use the same approach here.

  1. What should we do about the completionCandidates attribute of the existing @Option/@Parameters. Should we mark it as deprecated and ask users to use a new custom annotation for auto-complete?

To me, marking something as deprecated expresses an intention to eventually remove it. I do not have such an intention (picocli is very big on being backwards compatible), so it is not necessary to deprecate the completionCandidates attribute. It is simple and works fine, there is no need to disrupt applications that are currently using it.

  1. Regarding the design, how many annotations should there be?

That question (of what shape the solution should take) will come up at some point, and we are not that far away from that point, but I would like to explore the problem space a bit further first.

(Thinking out loud)
What is the problem we are trying to solve? Perhaps to improve auto-completion in any and all ways we can think of. What kinds of auto-completion exist? I can think of built-in completion by the shell (bash, zsh, other unix shells, jline, ...); any static list of words that is known at compile time (Java enums are a special case of this); a "dynamic" list of words that is not known until runtime. (Is that all kinds of auto-completion? Can you think of another one?)

The "dynamic" one is interesting but needs more thought: This "runtime" we are speaking of, is this not actually auto-completion script generation time (as opposed to truly auto-completion script runtime)? What can we do at completion script generation time, and what can we do at completion script runtime? Can we/Would we want to support an annotation (or annotation attribute) where end users can specify a bash snippet that is executed at auto-completion script runtime to generate completion candidates? (Or zsh, or some other shell scripting language?)

@loicrouchon
Copy link
Author

Reading your questions I got a strong sense that this approach (annotations on options/parameters) is the right approach. Still a lot to discover and flesh out, but this feels like the right direction!

👍

The problem we are trying to solve:

Allow the specification of File/Host auto-complete for @Option/@Parameters of given custom types.

This can be generalized as:

Allow the specification of the auto-complete behavior for @Option/@Parameters of given custom types.

Which can then be generalized again as:

Allow the specification of the auto-complete behavior for @Option/@Parameters

This means:

  • It would be possible to override the auto-complete behavior of built-in types (for example, only a subset of enum values can be used in a particular sub-command)
  • The auto-complete behaviors list has to be defined. What is supported now, what can be in the future.

Possible behaviors

The possible auto-complete behaviors I can think of are:

  • Default shell completion
  • Static list of values
  • Path completion
  • Host completion
  • User completion
  • Group completion
  • Job completion
  • Service completion
  • Signal completion
  • Shell environment variable completion
  • Shell variable completion
  • Dynamic shell completion (executed on <TAB><TAB>)
  • Dynamic completion (executed on <TAB><TAB>)
  • Dynamic Picocli completion (executed on <TAB><TAB>)
  • Custom completion

Default completion

Disable the completion for the @Option/@Parameters.
Useful to disable the built-in completion.

Static list of values

This list could be built automatically (boolean, enum) or manually (completionCandidates).
This list could also be generated at compile time from a user defined source (file list, custom method call, ...).

Path completion

Path completion could be automatically generated for java.io.File and java.nio.file.Path or manually.
When manually specified, it is possible to configure the behavior more in details, it would need to be specified.
The path completion could be customized with the following options:

  • Specifying a file pattern filter (globpat vs filterpat)
  • Only match files
  • Only match directories

Host completion

Host completion could be automatically generated for java.net.InetAddress or manually.

User completion

User completion auto-complete with system user names

Group completion

Group completion auto-complete with system group names

Job completion

Job completion auto-complete with system jobs.
It has to be configured to specify either running or stopped ones.

Service completion

Service completion auto-complete with system service names.

Signal completion

Signal completion auto-complete with system signal names.

Environment variable completion

Environment variable completion auto-complete with shell environment variable names.

Variable completion

Variable completion auto-complete with system shell variable names.

Dynamic shell completion

Dynamic shell completion would allow to execute on <TAB><TAB>, a shell function in the current shell environment, and its output is used as the possible completions.
Part of the current state of the command being written would be passed as arguments to this function.

It also could be supported to allow the user to define shell functions that would be included in the auto-complete shell scripts, so that they can be referenced here.

Dynamic command completion

Dynamic completion would allow to execute on <TAB><TAB>, an arbitrary command in a subshell environment, and its output is used as the possible completions.
Part of the current state of the command being written could be passed as arguments to this function. (feasibility to be assessed).

Dynamic Picocli completion

Dynamic Picocli completion would allow to execute on <TAB><TAB>, an arbitrary picocli command of the current program in a subshell environment, and its output is used as the possible completions.
Part of the current state of the command being written could be passed as arguments to this function. (feasibility to be assessed).
This might be a tricky one to implement but it could give great flexibility in having a sub-command specialized in reacting to <TAB><TAB> events.

Custom completion

Custom completion would allow the user to specify the autocomplete command.
This would be implementation dependent.

Those proposition are based on what is currently possible to implement in bash as per the bash completion documentation.

The above list is for brainstorming only, they might(/should?) not all be implemented, at least, not in this ticket.

I hope this clarifies the list of things that could be supported

@cbeams
Copy link

cbeams commented Jul 3, 2021

@loicrouchon, glad to see this effort, as I'm in need of the "Custom completion" functionality you've described above.

The details of my use case are specific to my application, but for our purposes here, I'm looking to create completion functionality similar to the way git checkout works. It allows the user to tab-complete for available refs:

$ git checkout <tab><tab>
FETCH_HEAD         HEAD               ORIG_HEAD          REBASE_HEAD        main               v2.0.0-prototype

and it allows for tab-completing a partially written ref out to its full name:

$ git checkout v2.<tab>
  ... autocompletes to ...
$ git checkout v2.0.0.-prototype

This is obviously Git-specific functionality, and Git needs to look into its own object database to generate the completion candidates. I need to be able to do something similar in my own application.

Ideally, I'd like to be able to tell Picocli that auto-completion candidates for a given @Option value or for positional parameters to a given @Command should be produced by, let's say, an implementation of Picocli's (proposed new) IAutoCompletionCandidateProvider interface.

I would want to be able to run any arbitrary code in that implementation, and I would want to have access to the Picocli context it's running in, e.g. to have access to the current (sub)command's @Spec, ParseResult, etc.

I would think this most general kind of candidate provider hook would be the first thing to implement, and then more specific conveniences such as the ones @loicrouchon mentioned above could be built on top of it.

@remkop, do you generally agree? @loicrouchon, do you plan to return to work on this effort? Thanks to you both.

@loicrouchon
Copy link
Author

Hello @cbeams

Yes, I would like to continue to work on this. At least on the narrow scope of the path/host completion for custom types. But the dynamic completion will not be part of this pr as it has its own dedicated issue: #506.

However I feel both issues need some common design which we started to discuss above. As I'm not the owner of the project I can propose ideas, and submit a PR. But I would need some feedback from @remkop to be able to move forward and propose a solution in line with the picocli (great) way of doing things.

@remkop
Copy link
Owner

remkop commented Jul 13, 2021

Looking at recent comments by @gunnarmorling and @yschimke in #506, it may be interesting to tie these together.

(Thinking out loud) what if we introduce a new annotation, say AutoComplete.Generator that has a single String value:

public class AutoComplete { // existing class picocli.AutoComplete

    // new nested interface
    @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Generator { 
        String value(); // command to be executed by the shell to generate completion candidates

        String shell() default "bash";
    }
   //...

This annotation can be used on any element that is also annotated with @Command, @Option or @Parameters.

The value would be a command that can be executed by the shell when the user triggers command line completion (by pressing <TAB><TAB> in bash, for example). It is the responsibility of this command to print a list of completion candidates to the standard output stream. On Bash this would be a space-separated list of words.

This could then be implemented by a hidden subcommand.

Here is an example of how this annotation could be used:

@Command(name = "mycommand")
public class MyCommand implements Runnable {

    @AutoComplete.Generator("mycommand generateCompletionsForMyCustomType")
    @Option(names = {"-o", "--my-option"})
    MyCustomType myType;

    @Command(hidden = true) // hidden subcommand of mycommand
    public void generateCompletionsForMyCustomType(@Unmatched String[] ignored) {
        String[] myCompletions = dynamicallyGenerateCompletionsForMyCustomType();
        System.out.println(String.join(" ", myCompletions);
    }

    public void run() { // business logic of MyCommand
    }
}

The picocli model would need to be updated to get the CommandLine.Model.CompletionGeneratorSpecs for each command, option or positional parameter (there may be multiple, one for each shell). The CompletionGeneratorSpec's value would return the annotation value.

The AutoComplete class would need to be updated: if a CommandSpec, OptionSpec or PositionalParamSpec has a CompletionGeneratorSpec, then use the value from the GeneratorSpec in the completion script.

One idea (again, just thinking out loud, this may not work...) would be:

// AutoComplete.java
    private static void generateCompletionCandidates(StringBuilder buff, OptionSpec f) {
        CompletionGeneratorSpec custom = f.completionGenerator("bash");
        if (custom != null) {
            buff.append(format("  local %s_option_args=$(%s) # %s values\n",
                    bashify(f.paramLabel()),
                    custom.value(),
                    f.longestName()));
        } else {
            buff.append(format("  local %s_option_args=\"%s\" # %s values\n",
                    bashify(f.paramLabel()),
                    concat(" ", extract(f.completionCandidates())).trim(),
                    f.longestName()));
        }
    }

Implementation note: the value of the %s_option_args is still passed to the compgen builtin, so that partial user input is used to filter out completion candidates that do not match the user input. Additionally we should modify the AutoComplete.generateOptionsCases method so that our generator command is only invoked when the case matches that option. For example, the relevant portion of a generated completion script could look like this:

 case ${prev_word} in
   --my-option)
     local myoption_args=$(mycommand generateCompletionsForMyCustomType) # --my-option values
     COMPREPLY=( $( compgen -W "${myoption_args}" -- "${curr_word}" ) )
     return $?
     ;;
 esac

In the above example, the generator spec value is a hidden picocli subcommand, but it does not have to be, it can be any script or function. For example, for the use case where only a subset of an enum are valid completions for some subcommand, the value could simply use echo:

@Command
public class SomeCommand {
    @AutoComplete.Generator("echo HOURS MINUTES SECONDS")
    @Option(names = "--time-unit")
    TimeUnit timeUnit;
}

Or another use case: a Path option for which we only want to list files with the .txt extension in the current directory:

@Command
public class SomeCommand {
    @AutoComplete.Generator("ls . | grep *.txt")
    @Option(names = "--file")
    Path myFile;
}

This would allow dynamic completion, and developers would not need to modify the generated bash completion script (which is what I believe @gunnarmorling and @yschimke ended up doing in #506).

Thoughts?

@cbeams
Copy link

cbeams commented Jul 13, 2021

Hey @remkop, this looks like a good direction. A few thoughts and questions:

@AutoComplete.Generator("mycommand generateCompletionsForMyCustomType")

It's worth considering that mycommand may not be on the user's $PATH. They may be invoking the command relatively, e.g. with ./mycommand, and in that case, bash is going to error out with mycommand: command not found. Up until now, there's been nothing about the way Picocli's bash completion scripts work that has required the command to be on the $PATH, but that would change now. This may just be something to document as a caveat or limitation, but it would be great if there were some way to support the case of a relatively-invoked command. Something like accessing $0 within a bash script to get the relative path to the script being run.


Regarding shell-specific completion generators and the need for the @AutoCompletion.Generator#shell attribute: what would typically differ between bash and zsh? Would it become the case that hidden generator commands would need to be written for both shells in most / all cases? So far, picocli's autocompletion functionality has been blissfully shell-agnostic from the command author's side. It would be unfortunate to break this abstraction here, though perhaps necessary to do so. In the case where shell-specific generation logic is necessary, would the @Generator annotation be repeated? For example

@AutoComplete.Generator(value="mycommand generateBashCompletionsForMyCustomType", shell="bash")
@AutoComplete.Generator(value="mycommand generateZshCompletionsForMyCustomType", shell="zsh")
@Option(names = {"-o", "--my-option"})
MyCustomType myType;

In the declaration of the hidden generator command, what is the significance or necessity of the @Unmatched String[]here?

public void generateCompletionsForMyCustomType(@Unmatched String[] ignored) {...}

Regarding filtering autocompletion candidates based on partial user input:

Implementation note: the value of the %s_option_args is still passed to the compgen builtin, so that partial user input is used to filter out completion candidates that do not match the user input.

This part is very important to my own use cases. If I understand correctly, you're talking about making sure it's possible to implement filtered completions, a la the way git checkout is able to filter available branch names based on partial user input:

$ git branch foo
$ git branch bar
$ git branch baz
$ git checkout b<tab>
bar baz

You mention that this partial user input is passed to the compgen builtin on the bash side, but shouldn't it also be made available to the completion generator method on the Java side? It may be desirable to do the filtering up front instead of generating and returning all completion candidates to the shell.


Regarding @Generator#value being able to support any command, e.g. echo or ls, I agree that this makes sense and is nice to have, but it should probably come with at least a documentation caveat that doing so binds the implementation to a system that has those commands available. This is perhaps obvious enough, but perhaps worth mentioning that where portability matters, the best practice would be to wrap such functionality in a hidden command that can do the right thing in an os-agnostic way.


Finally, regarding the string value provided to the @Generator annotation:

@AutoComplete.Generator("mycommand generateCompletionsForMyCustomType")
@Option(names = {"-o", "--my-option"})
MyCustomType myType;

Perhaps the fragile string could be avoided by (optionally) allowing the user to provide an implemantion of, say, IAutoCompletionGenerator, e.g.:

@AutoComplete.Generator(type=MyCustomTypeAutoCompletionGenerator.class)
@Option(names = {"-o", "--my-option"})
MyCustomType myType;

where MyCustomTypeAutoCompletionGenerator implements a generate method that produces the same results as the generateCompletionsForMyCustomType method would have, and then under the hood, picocli will automatically register a hidden command whose name is based on convention. The overall functionality would be the same as you've proposed, but without depending on stringified method names.

@remkop
Copy link
Owner

remkop commented Jul 13, 2021

It's worth considering that mycommand may not be on the user's $PATH. (...)

Agreed. We should experiment with what gives the most robust result and document that.


Regarding shell-specific completion generators and the need for the @AutoCompletion.Generator#shell attribute: (...) Would it become the case that hidden generator commands would need to be written for both shells in most / all cases?

Possibly yes once support is added for additional completion scripts like Zsh (#294), Fish (#725), Yori (#1062), PowerShell (#922) and clink (#921). This may never happen, but it impacts the API of the model, so I want to build it in up front.

With API of the model I mean methods used by the AutoComplete class to get the generator for each option/positional param/command:

OptionSpec option (...)
CompletionGeneratorSpec custom = option.completionGenerators().get("bash");

(...) In the case where shell-specific generation logic is necessary, would the @Generator annotation be repeated? For example

@AutoComplete.Generator(value="mycommand generateBashCompletionsForMyCustomType", shell="bash")
@AutoComplete.Generator(value="mycommand generateZshCompletionsForMyCustomType", shell="zsh")
@Option(names = {"-o", "--my-option"})
MyCustomType myType;

Yes, although strictly speaking we may then need to wrap it in a Generators annotation whose value is an array of Generator annotations (repeatable annotations were not introduced until Java 8 and picocli supports Java 5). User code would then look like this:

 @AutoComplete.Generators({
     @AutoComplete.Generator(value="mycommand generateBashCompletionsForMyCustomType", shell="bash"),
     @AutoComplete.Generator(value="mycommand generateZshCompletionsForMyCustomType", shell="zsh")})
 @Option(names = {"-o", "--my-option"})
 MyCustomType myType;

Regarding filtering autocompletion candidates based on partial user input:
(...)
You mention that this partial user input is passed to the compgen builtin on the bash side, but shouldn't it also be made available to the completion generator method on the Java side? It may be desirable to do the filtering up front instead of generating and returning all completion candidates to the shell.

In Bash completion, this is possible by passing "${curr_word}" to your custom completion generator command:

@AutoComplete.Generator("mycommand generateMyCompletions -- \"${curr_word}\"")

We would need to document this. (See also the answer to your next question below.)


In the declaration of the hidden generator command, what is the significance or necessity of the @Unmatched String[]here?

@Command
public void generateMyCompletions(@Unmatched String[] ignored) {...}

You are right, this is usually not necessary, unless in the annotation we include ${curr_word}. I had not thought that through yet when I wrote it.

I was still debating on whether we always pass ${curr_word} to the command or not. However, that may not be desirable to support cases like the echo and ls command I mentioned.

If "${curr_word}" is passed to your custom completion generator command, then the partial user input would be passed as a positional parameter to the generateMyCompletions command. Your implementation could then use it to filter the output.

You could then write it as:

@Command
public void generateMyCompletions(@Parameters String[] partialInput) {...}

Regarding @Generator#value being able to support any command, e.g. echo or ls, I agree that this makes sense and is nice to have, but it should probably come with at least a documentation caveat that doing so binds the implementation to a system that has those commands available. This is perhaps obvious enough, but perhaps worth mentioning that where portability matters, the best practice would be to wrap such functionality in a hidden command that can do the right thing in an os-agnostic way.

I appreciate that, but another way to look at it is that, since shell completion will always be very shell-specific, we do have some additional freedom that the pure Java world does not have. :-)


Finally, regarding the string value provided to the @Generator annotation:

@AutoComplete.Generator("mycommand generateCompletionsForMyCustomType")
@Option(names = {"-o", "--my-option"})
MyCustomType myType;

Perhaps the fragile string could be avoided by (optionally) allowing the user to provide an implementation of, say, IAutoCompletionGenerator, e.g.:

@AutoComplete.Generator(type=MyCustomTypeAutoCompletionGenerator.class)
@Option(names = {"-o", "--my-option"})
MyCustomType myType;

where MyCustomTypeAutoCompletionGenerator implements a generate method that produces the same results as the generateCompletionsForMyCustomType method would have, and then under the hood, picocli will automatically register a hidden command whose name is based on convention. The overall functionality would be the same as you've proposed, but without depending on stringified method names.

Maybe yes, especially from the point of view of integrating this in a JLine shell. In that scenario, the completion generator will be invoked in the same Java process that is running JLine and in which the command will be executed. This means it is easier to share state between the completion logic and the command, without going to the file system or a database, for example. I have not thought this scenario through enough though.

I am not sure yet about the trade-off between string fragility on the one hand and the complexity of automatically generated hidden commands with some convention-based name on the other hand. The String-based solution seems simpler, which may be important since this stuff is already complex enough as it is... 😅


Great feedback! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: auto-completion An issue or change related to auto-completion type: enhancement ✨
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants