Skip to content

Commit b7ad099

Browse files
committed
Basic zsh completion support
- 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 - Fixes #927
1 parent 2d29050 commit b7ad099

File tree

6 files changed

+486
-65
lines changed

6 files changed

+486
-65
lines changed

Diff for: spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Completion.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import org.springframework.shell.standard.ShellComponent;
2121
import org.springframework.shell.standard.ShellMethod;
2222
import org.springframework.shell.standard.completion.BashCompletions;
23+
import org.springframework.shell.standard.completion.ZshCompletions;
2324

2425
/**
2526
* Command to create a shell completion files, i.e. for {@code bash}.
@@ -52,4 +53,10 @@ public String bash() {
5253
BashCompletions bashCompletions = new BashCompletions(resourceLoader, getCommandCatalog());
5354
return bashCompletions.generate(rootCommand);
5455
}
56+
57+
@ShellMethod(key = "completion zsh", value = "Generate zsh completion script")
58+
public String zsh() {
59+
ZshCompletions zshCompletions = new ZshCompletions(resourceLoader, getCommandCatalog());
60+
return zshCompletions.generate(rootCommand);
61+
}
5562
}

Diff for: spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,8 +88,9 @@ protected CommandModel generateCommandModel() {
8888
else {
8989
commandKey = splitKeys[i];
9090
}
91+
String desc = i + 1 < splitKeys.length ? null : registration.getDescription();
9192
DefaultCommandModelCommand command = commands.computeIfAbsent(commandKey,
92-
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main));
93+
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main, desc));
9394

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

149150
/**
150-
* Gets sub-commands known to this command.
151+
* Gets a description of a command.
152+
* @return command description
153+
*/
154+
String getDescription();
151155

156+
/**
157+
* Gets sub-commands known to this command.
152158
* @return known sub-commands
153159
*/
154160
List<CommandModelCommand> getCommands();
@@ -240,12 +246,19 @@ class DefaultCommandModelCommand implements CommandModelCommand {
240246

241247
private String fullCommand;
242248
private String mainCommand;
249+
private String description;
243250
private List<CommandModelCommand> commands = new ArrayList<>();
244251
private List<CommandModelOption> options = new ArrayList<>();
245252

246-
DefaultCommandModelCommand(String fullCommand, String mainCommand) {
253+
DefaultCommandModelCommand(String fullCommand, String mainCommand, String description) {
247254
this.fullCommand = fullCommand;
248255
this.mainCommand = mainCommand;
256+
this.description = description;
257+
}
258+
259+
@Override
260+
public String getDescription() {
261+
return description;
249262
}
250263

251264
@Override
@@ -293,6 +306,9 @@ void addOptions(List<CommandModelOption> options) {
293306
}
294307

295308
void addCommand(DefaultCommandModelCommand command) {
309+
if (commands.contains(command)) {
310+
return;
311+
}
296312
commands.add(command);
297313
}
298314

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.standard.completion;
17+
18+
import org.springframework.core.io.ResourceLoader;
19+
import org.springframework.shell.command.CommandCatalog;
20+
21+
/**
22+
* Completion script generator for a {@code zsh}.
23+
*
24+
* @author Janne Valkealahti
25+
*/
26+
public class ZshCompletions extends AbstractCompletions {
27+
28+
public ZshCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
29+
super(resourceLoader, commandCatalog);
30+
}
31+
32+
public String generate(String rootCommand) {
33+
CommandModel model = generateCommandModel();
34+
return builder()
35+
.attribute("name", rootCommand)
36+
.attribute("model", model)
37+
.group("classpath:completion/zsh.stg")
38+
.appendGroup("main")
39+
.build();
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// pre content template before commands
3+
// needs to escape some > characters
4+
//
5+
pre(name) ::= <<
6+
#compdef _<name> <name>
7+
>>
8+
9+
//
10+
// commands section with command and description
11+
//
12+
cmd_and_desc(command) ::= <<
13+
"<command.mainCommand>:<command.description>"
14+
>>
15+
16+
//
17+
// case for command to call function
18+
//
19+
cmd_func(name,command) ::= <<
20+
<command.mainCommand>)
21+
_<name>_<command.commandParts:{p | <p>}; separator="_">
22+
;;
23+
>>
24+
25+
//
26+
// recursive sub commands
27+
//
28+
sub_command(name,command,commands) ::= <<
29+
function _<name>_<command.commandParts:{p | <p>}; separator="_"> {
30+
local -a commands
31+
32+
_arguments -C \
33+
<command.flags:{f | "<f>" \\}; separator="\n">
34+
"1: :->cmnds" \
35+
"*::arg:->args"
36+
37+
case $state in
38+
cmnds)
39+
commands=(
40+
<commands:{c | <cmd_and_desc(c)>}; separator="\n">
41+
)
42+
_describe "command" commands
43+
;;
44+
esac
45+
46+
case "$words[1]" in
47+
<commands:{c | <cmd_func(name,c)>}; separator="\n">
48+
esac
49+
}
50+
51+
<commands:{c | <sub_command(name,c,c.commands)>}; separator="\n\n">
52+
>>
53+
54+
//
55+
// top level commands
56+
//
57+
top_commands(name,commands) ::= <<
58+
function _<name> {
59+
local -a commands
60+
61+
_arguments -C \
62+
"1: :->cmnds" \
63+
"*::arg:->args"
64+
65+
case $state in
66+
cmnds)
67+
commands=(
68+
<commands:{c | <cmd_and_desc(c)>}; separator="\n">
69+
)
70+
_describe "command" commands
71+
;;
72+
esac
73+
74+
case "$words[1]" in
75+
<commands:{c | <cmd_func(name,c)>}; separator="\n">
76+
esac
77+
}
78+
79+
<commands:{c | <sub_command(name,c,c.commands)>}; separator="\n\n">
80+
>>
81+
82+
//
83+
// main template to call from render
84+
//
85+
main(name, model) ::= <<
86+
<pre(name)>
87+
88+
<top_commands(name,model.commands)>
89+
>>

0 commit comments

Comments
 (0)