Skip to content

Commit

Permalink
Basic zsh completion support
Browse files Browse the repository at this point in the history
- This adds basic zsh support similarly to
  existing bash completion
- New command "completion zsh"
- Fix internal recursive command completion model for
  cases with deep nested commands
- Backport #927
- Fixes #929
  • Loading branch information
jvalkeal committed Nov 16, 2023
1 parent e890d35 commit d475b96
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,7 @@
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.completion.BashCompletions;
import org.springframework.shell.standard.completion.ZshCompletions;

/**
* Command to create a shell completion files, i.e. for {@code bash}.
Expand Down Expand Up @@ -52,4 +53,10 @@ public String bash() {
BashCompletions bashCompletions = new BashCompletions(resourceLoader, getCommandCatalog());
return bashCompletions.generate(rootCommand);
}

@ShellMethod(key = "completion zsh", value = "Generate zsh completion script")
public String zsh() {
ZshCompletions zshCompletions = new ZshCompletions(resourceLoader, getCommandCatalog());
return zshCompletions.generate(rootCommand);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -88,8 +88,9 @@ protected CommandModel generateCommandModel() {
else {
commandKey = splitKeys[i];
}
String desc = i + 1 < splitKeys.length ? null : registration.getDescription();
DefaultCommandModelCommand command = commands.computeIfAbsent(commandKey,
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main));
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main, desc));

// TODO long vs short
List<CommandModelOption> options = registration.getOptions().stream()
Expand Down Expand Up @@ -147,8 +148,13 @@ interface CommandModel {
interface CommandModelCommand {

/**
* Gets sub-commands known to this command.
* Gets a description of a command.
* @return command description
*/
String getDescription();

/**
* Gets sub-commands known to this command.
* @return known sub-commands
*/
List<CommandModelCommand> getCommands();
Expand Down Expand Up @@ -240,12 +246,19 @@ class DefaultCommandModelCommand implements CommandModelCommand {

private String fullCommand;
private String mainCommand;
private String description;
private List<CommandModelCommand> commands = new ArrayList<>();
private List<CommandModelOption> options = new ArrayList<>();

DefaultCommandModelCommand(String fullCommand, String mainCommand) {
DefaultCommandModelCommand(String fullCommand, String mainCommand, String description) {
this.fullCommand = fullCommand;
this.mainCommand = mainCommand;
this.description = description;
}

@Override
public String getDescription() {
return description;
}

@Override
Expand Down Expand Up @@ -293,6 +306,9 @@ void addOptions(List<CommandModelOption> options) {
}

void addCommand(DefaultCommandModelCommand command) {
if (commands.contains(command)) {
return;
}
commands.add(command);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.standard.completion;

import org.springframework.core.io.ResourceLoader;
import org.springframework.shell.command.CommandCatalog;

/**
* Completion script generator for a {@code zsh}.
*
* @author Janne Valkealahti
*/
public class ZshCompletions extends AbstractCompletions {

public ZshCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
super(resourceLoader, commandCatalog);
}

public String generate(String rootCommand) {
CommandModel model = generateCommandModel();
return builder()
.attribute("name", rootCommand)
.attribute("model", model)
.group("classpath:completion/zsh.stg")
.appendGroup("main")
.build();
}
}
89 changes: 89 additions & 0 deletions spring-shell-standard/src/main/resources/completion/zsh.stg
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// pre content template before commands
// needs to escape some > characters
//
pre(name) ::= <<
#compdef _<name> <name>
>>

//
// commands section with command and description
//
cmd_and_desc(command) ::= <<
"<command.mainCommand>:<command.description>"
>>

//
// case for command to call function
//
cmd_func(name,command) ::= <<
<command.mainCommand>)
_<name>_<command.commandParts:{p | <p>}; separator="_">
;;
>>

//
// recursive sub commands
//
sub_command(name,command,commands) ::= <<
function _<name>_<command.commandParts:{p | <p>}; separator="_"> {
local -a commands

_arguments -C \
<command.flags:{f | "<f>" \\}; separator="\n">
"1: :->cmnds" \
"*::arg:->args"

case $state in
cmnds)
commands=(
<commands:{c | <cmd_and_desc(c)>}; separator="\n">
)
_describe "command" commands
;;
esac

case "$words[1]" in
<commands:{c | <cmd_func(name,c)>}; separator="\n">
esac
}

<commands:{c | <sub_command(name,c,c.commands)>}; separator="\n\n">
>>

//
// top level commands
//
top_commands(name,commands) ::= <<
function _<name> {
local -a commands

_arguments -C \
"1: :->cmnds" \
"*::arg:->args"

case $state in
cmnds)
commands=(
<commands:{c | <cmd_and_desc(c)>}; separator="\n">
)
_describe "command" commands
;;
esac

case "$words[1]" in
<commands:{c | <cmd_func(name,c)>}; separator="\n">
esac
}

<commands:{c | <sub_command(name,c,c.commands)>}; separator="\n\n">
>>

//
// main template to call from render
//
main(name, model) ::= <<
<pre(name)>

<top_commands(name,model.commands)>
>>
Loading

0 comments on commit d475b96

Please sign in to comment.